diff --git a/docusaurus/docs/operate/quickstart/gateway_cheatsheet.md b/docusaurus/docs/operate/quickstart/gateway_cheatsheet.md index 9ab3601fe..741966d13 100644 --- a/docusaurus/docs/operate/quickstart/gateway_cheatsheet.md +++ b/docusaurus/docs/operate/quickstart/gateway_cheatsheet.md @@ -3,16 +3,20 @@ sidebar_position: 7 title: Gateway Cheat Sheet --- -# Gateway Cheat Sheet +## Gateway Cheat Sheet -This guide provides quick reference commands for setting up and running a Gateway +This guide provides quick reference commands for setting up and running a **Gateway** on Pocket Network. -:::warning +For detailed instructions, troubleshooting, and observability setup, see the +[Gateway Walkthrough](./../run_a_node/gateway_walkthrough.md). + +:::note These instructions are intended to run on a Linux machine. -TODO_TECHDEBT(@olshansky): Adapt the instructions to be macOS friendly. +TODO_TECHDEBT(@olshansky): Adapt instructions to be macOS friendly in order to +streamline development and reduce friction for any new potential contributor. ::: @@ -32,15 +36,22 @@ TODO_TECHDEBT(@olshansky): Adapt the instructions to be macOS friendly. - [\[TODO\] Run the `PATH` Gateway using Docker](#todo-run-the-path-gateway-using-docker) - [Check the `PATH Gateway` is serving relays](#check-the-path-gateway-is-serving-relays) -:::note -For detailed instructions, troubleshooting, and observability setup, see the [Gateway Walkthrough](./../run_a_node/gateway_walkthrough.md). -::: - ## Pre-Requisites 1. Make sure to [install the `poktrolld` CLI](../user_guide/install.md). 2. Make sure you know how to [create and fund a new account](../user_guide/create-new-wallet.md). +:::warning + +You can append `--keyring-backend test` to all the `poktrolld` commands throughout +this guide to avoid entering the password each time. + +This is not recommended but provided for convenience for NON PRODUCTION USE ONLY. + +⚠️ Use at your own risk. ⚠️ + +::: + ## Account Setup ### Create and fund the `Gateway` and `Application` accounts @@ -49,27 +60,14 @@ Create a new key pair for the delegating `Application`: ```bash poktrolld keys add application - -# Optionally, to avoid entering the password each time: -# poktrolld keys add application --keyring-backend test ``` Create a new key pair for the `Gateway`: ```bash poktrolld keys add gateway - -# Optionally, to avoid entering the password each time: -# poktrolld keys add gateway --keyring-backend test ``` -:::tip - -You can set the `--keyring-backend` flag to `test` to avoid entering the password -each time. Learn more about [cosmos keyring backends here](https://docs.cosmos.network/v0.46/run-node/keyring.html). - -::: - ### Prepare your environment For convenience, we're setting several environment variables to streamline @@ -78,21 +76,18 @@ the process of interacting with the Shannon network: We recommend you put these in your `~/.bashrc` file: ```bash -export NODE="https://shannon-testnet-grove-rpc.beta.poktroll.com" -export NODE_FLAGS="--node=https://shannon-testnet-grove-rpc.beta.poktroll.com" +export POCKET_NODE="https://shannon-testnet-grove-rpc.beta.poktroll.com" +export NODE_FLAGS="--node=$POCKET_NODE" export TX_PARAM_FLAGS="--gas=auto --gas-prices=1upokt --gas-adjustment=1.5 --chain-id=pocket-beta --yes" export GATEWAY_ADDR=$(poktrolld keys show gateway -a) export APP_ADDR=$(poktrolld keys show application -a) - -# Optionally, to avoid entering the password each time: -# export GATEWAY_ADDR=$(poktrolld keys show gateway -a --keyring-backend test) -# export APP_ADDR=$(poktrolld keys show application -a --keyring-backend test) ``` :::tip -You can put the above in a special `~/.poktrollrc` and add `source ~/.poktrollrc` to -your `~/.profile` file for a cleaner organization. +As an alternative to appending directly to `~/.bashrc`, you can put the above +in a special `~/.poktrollrc` and add `source ~/.poktrollrc` to +your `~/.profile` (or `~/.bashrc`) file for a cleaner organization. ::: @@ -116,7 +111,9 @@ poktrolld query bank balances $APP_ADDR $NODE_FLAGS ``` :::tip + You can find all the explorers, faucets and tools at the [tools page](../../explore/tools.md). + ::: ### Stake the `Gateway` @@ -124,18 +121,15 @@ You can find all the explorers, faucets and tools at the [tools page](../../expl Create a Gateway stake configuration file: ```bash -cat < /tmp/stake_gateway_config.yaml +cat <<🚀 > /tmp/stake_gateway_config.yaml stake_amount: 1000000upokt -EOF +🚀 ``` And run the following command to stake the `Gateway`: ```bash poktrolld tx gateway stake-gateway --config=/tmp/stake_gateway_config.yaml --from=$GATEWAY_ADDR $TX_PARAM_FLAGS $NODE_FLAGS - -# Optionally, to avoid entering the password each time: -# poktrolld tx gateway stake-gateway --config=/tmp/stake_gateway_config.yaml --from=$GATEWAY_ADDR $TX_PARAM_FLAGS $NODE_FLAGS --keyring-backend test ``` After about a minute, you can check the `Gateway`'s status like so: @@ -149,20 +143,17 @@ poktrolld query gateway show-gateway $GATEWAY_ADDR $NODE_FLAGS Create an Application stake configuration file: ```bash -cat < /tmp/stake_app_config.yaml +cat <<🚀 > /tmp/stake_app_config.yaml stake_amount: 100000000upokt service_ids: - "F00C" -EOF +🚀 ``` And run the following command to stake the `Application`: ```bash poktrolld tx application stake-application --config=/tmp/stake_app_config.yaml --from=$APP_ADDR $TX_PARAM_FLAGS $NODE_FLAGS - -# Optionally, to avoid entering the password each time: -# poktrolld tx application stake-application --config=/tmp/stake_app_config.yaml --from=$APP_ADDR $TX_PARAM_FLAGS $NODE_FLAGS --keyring-backend test ``` After about a minute, you can check the `Application`'s status like so: @@ -175,9 +166,6 @@ poktrolld query application show-application $APP_ADDR $NODE_FLAGS ```bash poktrolld tx application delegate-to-gateway $GATEWAY_ADDR --from=$APP_ADDR $TX_PARAM_FLAGS $NODE_FLAGS - -# Optionally, to avoid entering the password each time: -# poktrolld tx application delegate-to-gateway $GATEWAY_ADDR --from=$APP_ADDR $TX_PARAM_FLAGS $NODE_FLAGS --keyring-backend test ``` After about a minute, you can check the `Application`'s status like so: @@ -233,10 +221,6 @@ sed -i "s|host_port: ".*"|host_port: shannon-testnet-grove-grpc.beta.poktroll.co sed -i "s|gateway_address: .*|gateway_address: $GATEWAY_ADDR|" config/.config.yaml sed -i "s|gateway_private_key_hex: .*|gateway_private_key_hex: $(export_priv_key_hex gateway)|" config/.config.yaml sed -i '/owned_apps_private_keys_hex:/!b;n;c\ - '"$(export_priv_key_hex application)" config/.config.yaml - -# If you're using the test keyring-backend: -# sed -i "s|gateway_private_key_hex: .*|gateway_private_key_hex: $(export_priv_key_hex gateway)|" config/.config.yaml -# sed -i '/owned_apps_private_keys_hex:/!b;n;c\ - '"$(export_priv_key_hex application)" config/.config.yaml ``` When you're done, run `cat config/.config.yaml` to view the updated config file. diff --git a/docusaurus/docs/operate/quickstart/supplier_cheatsheet.md b/docusaurus/docs/operate/quickstart/supplier_cheatsheet.md new file mode 100644 index 000000000..0b5f7115b --- /dev/null +++ b/docusaurus/docs/operate/quickstart/supplier_cheatsheet.md @@ -0,0 +1,281 @@ +--- +sidebar_position: 6 +title: Supplier (RelayMiner) Cheat Sheet +--- + +## Supplier Cheat Sheet + +This guide provides quick reference commands for setting up a **Supplier** and +running a **RelayMiner** on Pocket Network. + +For detailed instructions, troubleshooting, and observability setup, see the +[Supplier Walkthrough](./../run_a_node/supplier_walkthrough.md). + +:::note + +These instructions are intended to run on a Linux machine. + +TODO_TECHDEBT(@olshansky): Adapt instructions to be macOS friendly in order to +streamline development and reduce friction for any new potential contributor. + +::: + +- [Pre-Requisites](#pre-requisites) + - [Context](#context) +- [Account Setup](#account-setup) + - [Create and fund the `Supplier` account](#create-and-fund-the-supplier-account) + - [Prepare your environment](#prepare-your-environment) +- [Supplier Configuration](#supplier-configuration) + - [Fund the Supplier account](#fund-the-supplier-account) + - [Stake the Supplier](#stake-the-supplier) +- [RelayMiner Configuration](#relayminer-configuration) + - [Configure the RelayMiner](#configure-the-relayminer) + - [Start the RelayMiner](#start-the-relayminer) + - [Secure vs Non-Secure `query_node_grpc_url`](#secure-vs-non-secure-query_node_grpc_url) +- [Supplier FAQ](#supplier-faq) + - [What Supplier operations are available?](#what-supplier-operations-are-available) + - [What Supplier queries are available?](#what-supplier-queries-are-available) + - [How do I query for all existing onchain Suppliers?](#how-do-i-query-for-all-existing-onchain-suppliers) + +## Pre-Requisites + +1. Make sure to [install the `poktrolld` CLI](../user_guide/install.md). +2. Make sure you know how to [create and fund a new account](../user_guide/create-new-wallet.md). +3. You have either [staked a new `service` or found an existing one](./service_cheatsheet.md). +4. `[Optional]` You can run things locally or have dedicated long-running hardware. See the [Docker Compose Cheat Sheet](./docker_compose_debian_cheatsheet#deploy-your-server) if you're interested in the latter. + +:::warning + +You can append `--keyring-backend test` to all the `poktrolld` commands throughout +this guide to avoid entering the password each time. + +This is not recommended but provided for convenience for NON PRODUCTION USE ONLY. + +⚠️ Use at your own risk. ⚠️ + +::: + +### Context + +This document is a cheat sheet to get you quickly started with two things: + +1. Staking an onchain `Supplier` +2. Deploying an offchain `RelayMiner` + +By the end of it, you should be able to serve Relays off-chain, and claim on-chain rewards. + +## Account Setup + +### Create and fund the `Supplier` account + +Create a new key pair for the `Supplier` + +```bash +poktrolld keys add supplier +``` + +### Prepare your environment + +For convenience, we're setting several environment variables to streamline +the process of interacting with the Shannon network: + +We recommend you put these in your `~/.bashrc` file: + +```bash +export NODE="https://shannon-testnet-grove-rpc.beta.poktroll.com" +export NODE_FLAGS="--node=https://shannon-testnet-grove-rpc.beta.poktroll.com" +export TX_PARAM_FLAGS="--gas=auto --gas-prices=1upokt --gas-adjustment=1.5 --chain-id=pocket-beta --yes" +export SUPPLIER_ADDR=$(poktrolld keys show supplier -a) +``` + +:::tip + +As an alternative to appending directly to `~/.bashrc`, you can put the above +in a special `~/.poktrollrc` and add `source ~/.poktrollrc` to +your `~/.profile` (or `~/.bashrc`) file for a cleaner organization. + +::: + +## Supplier Configuration + +### Fund the Supplier account + +Run the following command to get the `Supplier`: + +```bash +echo "Supplier address: $SUPPLIER_ADDR" +``` + +Then use the [Shannon Beta TestNet faucet](https://faucet.beta.testnet.pokt.network/) to fund the (supplier owner address) account. +See [Non-Custodial Staking](https://dev.poktroll.com/operate/configs/supplier_staking_config#non-custodial-staking) for more information about supplier owner vs operator and non-custodial staking. + +Afterwards, you can query the balance using the following command: + +```bash +poktrolld query bank balances $SUPPLIER_ADDR $NODE_FLAGS +``` + +:::tip + +You can find all the explorers, faucets and tools at the [tools page](../../explore/tools.md). + +::: + +### Stake the Supplier + +:::info + +For an in-depth look at how to stake a supplier, see the [Supplier configuration docs](./../configs/supplier_staking_config.md). + +These instructions help you stake a supplier for a specific service (POKT Morse) +using a pre-configured RPC endpoint ([Liquify](https://liquify.com/) public RPC endpoint). + +::: + +Retrieve your external IP address: + +```bash +EXTERNAL_IP=$(curl -4 ifconfig.me/ip) +``` + +Choose a port that'll be publicly accessible from the internet (e.g. `8545`) +and expose it. + +You can use the following command for OSs that use `ufw` (learn more [here](https://wiki.archlinux.org/title/Uncomplicated_Firewall)): + +```bash +sudo ufw allow 8545/tcp +``` + +Create a Supplier stake configuration file: + +```bash +cat <<🚀 > /tmp/stake_supplier_config.yaml +owner_address: $SUPPLIER_ADDR +operator_address: $SUPPLIER_ADDR +stake_amount: 1000069upokt +default_rev_share_percent: + $SUPPLIER_ADDR: 100 +services: + - service_id: "morse" + endpoints: + - publicly_exposed_url: http://$EXTERNAL_IP:8545 + rpc_type: JSON_RPC +🚀 +``` + +And run the following command to stake the `Supplier`: + +```bash +poktrolld tx supplier stake-supplier --config /tmp/stake_supplier_config.yaml --from=$SUPPLIER_ADDR $TX_PARAM_FLAGS $NODE_FLAGS +``` + +After about a minute, you can check the `Supplier`'s status like so: + +```bash +poktrolld query supplier show-supplier $SUPPLIER_ADDR $NODE_FLAGS +``` + +## RelayMiner Configuration + +### Configure the RelayMiner + +```bash +cat <<🚀 > /tmp/relayminer_config.yaml +default_signing_key_names: + - supplier +smt_store_path: /home/pocket/.poktroll/smt +pocket_node: + query_node_rpc_url: https://shannon-testnet-grove-rpc.beta.poktroll.com + query_node_grpc_url: https://shannon-testnet-grove-grpc.beta.poktroll.com:443 + tx_node_rpc_url: https://shannon-testnet-grove-rpc.beta.poktroll.com +suppliers: + - service_id: "morse" + service_config: + backend_url: "https://pocket-rpc.liquify.com" + publicly_exposed_endpoints: + - $EXTERNAL_IP + listen_url: http://0.0.0.0:8545 +metrics: + enabled: false + addr: :9090 +pprof: + enabled: false + addr: :6060 +🚀 +``` + +### Start the RelayMiner + +```bash +poktrolld \ + relayminer \ + --grpc-insecure=false \ + --log_level=debug \ + --config=/tmp/relayminer_config.yaml +``` + +### Secure vs Non-Secure `query_node_grpc_url` + +In `/tmp/relayminer_config.yaml`, you'll see that we specify an endpoint +for `query_node_grpc_url` which is TLS terminated. + +If `grpc-insecure=true` then it **MUST** be an HTTP port, no TLS. Once you have +an endpoint exposed, it can be validated like so: + +```bash +grpcurl -plaintext : list +``` + + + +If `grpc-insecure=false`, then it **MUST** be an HTTPS port, with TLS. + +The Grove team exposed one such endpoint on one of our validators for Beta Testnet +at `https://shannon-testnet-grove-grpc.beta.poktroll.com:443`. + +It can be validated with: + +```bash +grpcurl shannon-testnet-grove-grpc.beta.poktroll.com:443 list +``` + +Note that no `-plaintext` flag is required when an endpoint is TLS terminated and +must be omitted if it is not. + +:::tip + +You can replace both `http` and `https` with `tcp` and it should work the same way. + +::: + +## Supplier FAQ + +### What Supplier operations are available? + +```bash +poktrolld tx supplier -h +``` + +### What Supplier queries are available? + +```bash +poktrolld query supplier -h +``` + +### How do I query for all existing onchain Suppliers? + +Then, you can query for all services like so: + +```bash +poktrolld query supplier list-supplier --node https://shannon-testnet-grove-rpc.beta.poktroll.com --output json | jq +``` diff --git a/pkg/client/interface.go b/pkg/client/interface.go index 7045073f6..f5ee598f7 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -376,10 +376,10 @@ type QueryCache[T any] interface { // at multiple heights for a given key. type HistoricalQueryCache[T any] interface { QueryCache[T] - // GetAtHeight retrieves the nearest value <= the specified height - GetAtHeight(key string, height int64) (T, error) - // SetAtHeight adds or updates a value at a specific height - SetAtHeight(key string, value T, height int64) error + // GetAsOfVersion retrieves the nearest value <= the specified version number. + GetAsOfVersion(key string, version int64) (T, error) + // SetAsOfVersion adds or updates a value at a specific version number. + SetAsOfVersion(key string, value T, version int64) error } // ParamsQuerier represents a generic querier for module parameters. diff --git a/pkg/client/query/cache/config.go b/pkg/client/query/cache/config.go index 49d80435e..3881bbf5e 100644 --- a/pkg/client/query/cache/config.go +++ b/pkg/client/query/cache/config.go @@ -4,7 +4,7 @@ import ( "time" ) -// EvictionPolicy determines which items are removed when number of keys in the cache reaches maxKeys. +// EvictionPolicy determines which values are removed when number of keys in the cache reaches maxKeys. type EvictionPolicy int64 const ( @@ -16,7 +16,7 @@ const ( // queryCacheConfig is the configuration for query caches. // It is intended to be configured via QueryCacheOptionFn functions. type queryCacheConfig struct { - // maxKeys is the maximum number of items (key/value pairs) the cache can + // maxKeys is the maximum number of key/value pairs the cache can // hold before it starts evicting. maxKeys int64 @@ -27,18 +27,19 @@ type queryCacheConfig struct { // maxCacheSize is the maximum cumulative size of all keys AND values in the cache. // maxCacheSize int64 - // evictionPolicy determines which items are removed when number of keys in the cache reaches maxKeys. + // evictionPolicy determines which values are removed when number of keys in the cache reaches maxKeys. evictionPolicy EvictionPolicy - // ttl is how long items should remain in the cache. Items older than the ttl - // MAY NOT be evicted immediately, but are NEVER considered as cache hits. + // ttl is how long values should remain valid in the cache. Items older than the + // ttl MAY NOT be evicted immediately, but are NEVER considered as cache hits. ttl time.Duration // historical determines whether each key will point to a single values (false) // or a history (i.e. reverse chronological list) of values (true). historical bool - // numHistoricalValues is the number of past blocks for which to keep historical - // values. If 0, no historical pruning is performed. It only applies when - // historical is true. - numHistoricalValues int64 + // maxVersionAge is the max difference between the latest known version and + // any other version, below which value versions are retained, and above which + // value versions are pruned. If 0, no historical pruning is performed. + // It only applies when historical is true. + maxVersionAge int64 } // QueryCacheOptionFn is a function which receives a queryCacheConfig for configuration. @@ -53,15 +54,23 @@ func (cfg *queryCacheConfig) Validate() error { return ErrQueryCacheConfigValidation.Wrapf("eviction policy %d not imlemented", cfg.evictionPolicy) } + if cfg.maxVersionAge > 0 && !cfg.historical { + return ErrQueryCacheConfigValidation.Wrap("maxVersionAge > 0 requires historical mode to be enabled") + } + + if cfg.historical && cfg.maxVersionAge < 0 { + return ErrQueryCacheConfigValidation.Wrapf("maxVersionAge MUST be >= 0, got: %d", cfg.maxVersionAge) + } + return nil } -// WithHistoricalMode enables historical caching with the given numHistoricalValues +// WithHistoricalMode enables historical caching with the given maxVersionAge // configuration; if 0, no historical pruning is performed. -func WithHistoricalMode(numHistoricalBlocks int64) QueryCacheOptionFn { +func WithHistoricalMode(numRetainedVersions int64) QueryCacheOptionFn { return func(cfg *queryCacheConfig) { cfg.historical = true - cfg.numHistoricalValues = numHistoricalBlocks + cfg.maxVersionAge = numRetainedVersions } } @@ -80,7 +89,7 @@ func WithEvictionPolicy(policy EvictionPolicy) QueryCacheOptionFn { } } -// WithTTL sets the time-to-live for cached items. Items older than the TTL +// WithTTL sets the time-to-live for cached values. Values older than the TTL // MAY NOT be evicted immediately, but are NEVER considered as cache hits. func WithTTL(ttl time.Duration) QueryCacheOptionFn { return func(cfg *queryCacheConfig) { diff --git a/pkg/client/query/cache/errors.go b/pkg/client/query/cache/errors.go index b80342999..b12bb5cac 100644 --- a/pkg/client/query/cache/errors.go +++ b/pkg/client/query/cache/errors.go @@ -5,8 +5,9 @@ import "cosmossdk.io/errors" const codesace = "client/query/cache" var ( - ErrCacheMiss = errors.Register(codesace, 1, "cache miss") - ErrHistoricalModeNotEnabled = errors.Register(codesace, 2, "historical mode not enabled") - ErrQueryCacheConfigValidation = errors.Register(codesace, 3, "invalid query cache config") - ErrCacheInternal = errors.Register(codesace, 4, "cache internal error") + ErrCacheMiss = errors.Register(codesace, 1, "cache miss") + ErrHistoricalModeNotEnabled = errors.Register(codesace, 2, "historical mode not enabled") + ErrQueryCacheConfigValidation = errors.Register(codesace, 3, "invalid query cache config") + ErrCacheInternal = errors.Register(codesace, 4, "cache internal error") + ErrUnsupportedHistoricalModeOp = errors.Register(codesace, 5, "operation not supported in historical mode") ) diff --git a/pkg/client/query/cache/memory.go b/pkg/client/query/cache/memory.go index 414efb3a9..088f3f5d1 100644 --- a/pkg/client/query/cache/memory.go +++ b/pkg/client/query/cache/memory.go @@ -25,8 +25,8 @@ var ( // inMemoryCache provides a concurrency-safe in-memory cache implementation with // optional historical value support. type inMemoryCache[T any] struct { - config queryCacheConfig - latestHeight atomic.Int64 + config queryCacheConfig + latestVersion atomic.Int64 // valuesMu is used to protect values AND valueHistories from concurrent access. valuesMu sync.RWMutex @@ -43,14 +43,17 @@ type cacheValue[T any] struct { cachedAt time.Time } -// cacheValueHistory stores cachedItems by height and maintains a sorted list of -// heights for which cached items exist. This list is sorted in descending order -// to improve performance characteristics by positively correlating index with age. +// cacheValueHistory stores cachedValues by version number and maintains a sorted +// list of version numbers for which cached values exist. This list is sorted in +// descending order to improve performance characteristics by positively correlating +// index with age. type cacheValueHistory[T any] struct { - // sortedDescHeights is a list of the heights for which values are cached. - // It is sorted in descending order. - sortedDescHeights []int64 - heightMap map[int64]cacheValue[T] + // sortedDescVersions is a list of the version numbers for which values are + // cached. It is sorted in descending order. + sortedDescVersions []int64 + // versionToValueMap is a map from a version number to the cached value at + // that version number, if present. + versionToValueMap map[int64]cacheValue[T] } // NewInMemoryCache creates a new inMemoryCache with the configuration generated @@ -75,11 +78,11 @@ func NewInMemoryCache[T any](opts ...QueryCacheOptionFn) (*inMemoryCache[T], err // Get retrieves the value from the cache with the given key. If the cache is // configured for historical mode, it will return the value at the latest **known** -// height, which is only updated on calls to SetAtHeight, and therefore is not -// guaranteed to be the current height w.r.t the blockchain. +// version, which is only updated on calls to SetAsOfVersion, and therefore is not +// guaranteed to be the current version w.r.t the blockchain. func (c *inMemoryCache[T]) Get(key string) (T, error) { if c.config.historical { - return c.GetAtHeight(key, c.latestHeight.Load()) + return c.GetAsOfVersion(key, c.latestVersion.Load()) } c.valuesMu.RLock() @@ -87,14 +90,14 @@ func (c *inMemoryCache[T]) Get(key string) (T, error) { var zero T - cachedItem, exists := c.values[key] + cachedValue, exists := c.values[key] if !exists { return zero, ErrCacheMiss.Wrapf("key: %s", key) } isTTLEnabled := c.config.ttl > 0 - isCacheItemExpired := time.Since(cachedItem.cachedAt) > c.config.ttl - if isTTLEnabled && isCacheItemExpired { + isCacheValueExpired := time.Since(cachedValue.cachedAt) > c.config.ttl + if isTTLEnabled && isCacheValueExpired { // DEV_NOTE: Intentionally not pruning here to improve concurrent speed; // otherwise, the read lock would be insufficient. The value will be // overwritten by the next call to Set(). If usage is such that values @@ -103,14 +106,14 @@ func (c *inMemoryCache[T]) Get(key string) (T, error) { return zero, ErrCacheMiss.Wrapf("key: %s", key) } - return cachedItem.value, nil + return cachedValue.value, nil } -// GetAtHeight retrieves the value from the cache with the given key, at the given -// height. If a value is not found for that height, the value at the nearest previous -// height is returned. If the cache is not configured for historical mode, it returns -// an error. -func (c *inMemoryCache[T]) GetAtHeight(key string, getHeight int64) (T, error) { +// GetAsOfVersion retrieves the value from the cache with the given key, as of the +// given version. If a value is not found for that version, the value at the nearest +// previous version is returned. If the cache is not configured for historical mode, +// it returns an error. +func (c *inMemoryCache[T]) GetAsOfVersion(key string, version int64) (T, error) { var zero T if !c.config.historical { @@ -120,36 +123,43 @@ func (c *inMemoryCache[T]) GetAtHeight(key string, getHeight int64) (T, error) { c.valuesMu.RLock() defer c.valuesMu.RUnlock() - valueHistory := c.valueHistories[key] - var nearestCachedHeight int64 = -1 - for _, cachedHeight := range valueHistory.sortedDescHeights { - if cachedHeight <= getHeight { - nearestCachedHeight = cachedHeight + valueHistory, exists := c.valueHistories[key] + if !exists { + return zero, ErrCacheMiss.Wrapf("key: %s", key) + } + + var nearestCachedVersion int64 = -1 + for _, cachedVersion := range valueHistory.sortedDescVersions { + if cachedVersion <= version { + nearestCachedVersion = cachedVersion // DEV_NOTE: Since the list is sorted in descending order, once we - // encounter a cachedHeight that is less than or equal to getHeight, - // all subsequent cachedHeights SHOULD also be less than or equal to - // getHeight. + // encounter a cachedVersion that is less than or equal to version, + // all subsequent cachedVersions SHOULD also be less than or equal to + // version. break } } - if nearestCachedHeight == -1 { - return zero, ErrCacheMiss.Wrapf("key: %s, height: %d", key, getHeight) + if nearestCachedVersion == -1 { + return zero, ErrCacheMiss.Wrapf("key: %s, version: %d", key, version) } - value, exists := valueHistory.heightMap[nearestCachedHeight] + value, exists := valueHistory.versionToValueMap[nearestCachedVersion] if !exists { - return zero, ErrCacheInternal.Wrapf("failed to load historical value for key: %s, height: %d", key, getHeight) + // DEV_NOTE: This SHOULD NEVER happen. If it does, it means that the cache has been corrupted. + return zero, ErrCacheInternal.Wrapf("failed to load historical value for key: %s, version: %d", key, version) } - if c.config.ttl > 0 && time.Since(value.cachedAt) > c.config.ttl { + isTTLEnabled := c.config.ttl > 0 + isCacheValueExpired := time.Since(value.cachedAt) > c.config.ttl + if isTTLEnabled && isCacheValueExpired { // DEV_NOTE: Intentionally not pruning here to improve concurrent speed; // otherwise, the read lock would be insufficient. The value will be pruned - // in the subsequent call to SetAtHeight() after c.config.numHistoricalValues + // in the subsequent call to SetAsOfVersion() after c.config.maxVersionAge // blocks have elapsed. If usage is such that historical values aren't being // subsequently set, numHistoricalBlocks (if configured) will eventually // cause the pruning of historical values with expired TTLs. - return zero, ErrCacheMiss.Wrapf("key: %s, height: %d", key, getHeight) + return zero, ErrCacheMiss.Wrapf("key: %s, version: %d", key, version) } return value.value, nil @@ -157,24 +167,24 @@ func (c *inMemoryCache[T]) GetAtHeight(key string, getHeight int64) (T, error) { // Set adds or updates the value in the cache for the given key. If the cache is // configured for historical mode, it will store the value at the latest **known** -// height, which is only updated on calls to SetAtHeight, and therefore is not -// guaranteed to be the current height. +// version, which is only updated on calls to SetAsOfVersion, and therefore is not +// guaranteed to be the current version w.r.t. the blockchain. func (c *inMemoryCache[T]) Set(key string, value T) error { if c.config.historical { - return c.SetAtHeight(key, value, c.latestHeight.Load()) + return ErrUnsupportedHistoricalModeOp.Wrap("inMemoryCache#Set() is not supported in historical mode") } + c.valuesMu.Lock() + defer c.valuesMu.Unlock() + isMaxKeysConfigured := c.config.maxKeys > 0 - cacheHasMaxKeys := int64(len(c.values)) >= c.config.maxKeys - if isMaxKeysConfigured && cacheHasMaxKeys { + cacheMaxKeysReached := int64(len(c.values)) >= c.config.maxKeys + if isMaxKeysConfigured && cacheMaxKeysReached { if err := c.evict(); err != nil { return err } } - c.valuesMu.Lock() - defer c.valuesMu.Unlock() - c.values[key] = cacheValue[T]{ value: value, cachedAt: time.Now(), @@ -183,19 +193,24 @@ func (c *inMemoryCache[T]) Set(key string, value T) error { return nil } -// SetAtHeight adds or updates the historical value in the cache for the given key, -// and at the given height. If the cache is not configured for historical mode, it +// SetAsOfVersion adds or updates the historical value in the cache for the given key, +// and at the version number. If the cache is not configured for historical mode, it // returns an error. -func (c *inMemoryCache[T]) SetAtHeight(key string, value T, setHeight int64) error { +func (c *inMemoryCache[T]) SetAsOfVersion(key string, value T, version int64) error { if !c.config.historical { return ErrHistoricalModeNotEnabled } - // Update c.latestHeight if the given setHeight is newer (higher). - latestHeight := c.latestHeight.Load() - if setHeight > latestHeight { - // NB: Only update if c.latestHeight hasn't changed since we loaded it above. - c.latestHeight.CompareAndSwap(latestHeight, setHeight) + // Update c.latestVersion if the given version is newer (higher). + latestVersion := c.latestVersion.Load() + if version > latestVersion { + // NB: Only update if c.latestVersion hasn't changed since we loaded it above. + if c.latestVersion.CompareAndSwap(latestVersion, version) { + latestVersion = version + } else { + // Reload the latestVersion if it did change. + latestVersion = c.latestVersion.Load() + } } c.valuesMu.Lock() @@ -203,42 +218,43 @@ func (c *inMemoryCache[T]) SetAtHeight(key string, value T, setHeight int64) err valueHistory, exists := c.valueHistories[key] if !exists { - heightMap := make(map[int64]cacheValue[T]) + versionToValueMap := make(map[int64]cacheValue[T]) valueHistory = cacheValueHistory[T]{ - sortedDescHeights: make([]int64, 0), - heightMap: heightMap, + sortedDescVersions: make([]int64, 0), + versionToValueMap: versionToValueMap, } } - // Update sortedDescHeights and ensure the list is sorted in descending order. - if _, setHeightExists := valueHistory.heightMap[setHeight]; !setHeightExists { - valueHistory.sortedDescHeights = append(valueHistory.sortedDescHeights, setHeight) - sort.Slice(valueHistory.sortedDescHeights, func(i, j int) bool { - return valueHistory.sortedDescHeights[i] > valueHistory.sortedDescHeights[j] + // Update sortedDescVersions and ensure the list is sorted in descending order. + if _, versionExists := valueHistory.versionToValueMap[version]; !versionExists { + valueHistory.sortedDescVersions = append(valueHistory.sortedDescVersions, version) + sort.Slice(valueHistory.sortedDescVersions, func(i, j int) bool { + return valueHistory.sortedDescVersions[i] > valueHistory.sortedDescVersions[j] }) } - // Prune historical values for this key, where the setHeight - // is oder than the configured numHistoricalValues. - if c.config.numHistoricalValues > 0 { - lenCachedHeights := int64(len(valueHistory.sortedDescHeights)) - for heightIdx := lenCachedHeights - 1; heightIdx >= 0; heightIdx-- { - cachedHeight := valueHistory.sortedDescHeights[heightIdx] + // Prune historical values for this key, where the version + // is older than the configured maxVersionAge. + if c.config.maxVersionAge > 0 { + lenCachedVersions := int64(len(valueHistory.sortedDescVersions)) + for versionIdx := lenCachedVersions - 1; versionIdx >= 0; versionIdx-- { + cachedVersion := valueHistory.sortedDescVersions[versionIdx] // DEV_NOTE: Since the list is sorted, and we're iterating from lowest - // (oldest) to highest (youngest) height, once we encounter a cachedHeight - // that is younger than the configured numHistoricalValues, ALL subsequent - // heights SHOULD also be younger than the configured numHistoricalValues. - if setHeight-cachedHeight <= c.config.numHistoricalValues { - valueHistory.sortedDescHeights = valueHistory.sortedDescHeights[:heightIdx+1] + // (oldest) to highest (newest) version, once we encounter a cachedVersion + // that is newer than the configured maxVersionAge, ALL subsequent + // heights SHOULD also be newer than the configured maxVersionAge. + cachedVersionAge := latestVersion - cachedVersion + if cachedVersionAge <= c.config.maxVersionAge { + valueHistory.sortedDescVersions = valueHistory.sortedDescVersions[:versionIdx+1] break } - delete(valueHistory.heightMap, cachedHeight) + delete(valueHistory.versionToValueMap, cachedVersion) } } - valueHistory.heightMap[setHeight] = cacheValue[T]{ + valueHistory.versionToValueMap[version] = cacheValue[T]{ value: value, cachedAt: time.Now(), } @@ -248,7 +264,7 @@ func (c *inMemoryCache[T]) SetAtHeight(key string, value T, setHeight int64) err return nil } -// Delete removes an item from the cache. +// Delete removes a value from the cache. func (c *inMemoryCache[T]) Delete(key string) { c.valuesMu.Lock() defer c.valuesMu.Unlock() @@ -260,7 +276,7 @@ func (c *inMemoryCache[T]) Delete(key string) { } } -// Clear removes all items from the cache. +// Clear removes all values from the cache. func (c *inMemoryCache[T]) Clear() { c.valuesMu.Lock() defer c.valuesMu.Unlock() @@ -271,10 +287,10 @@ func (c *inMemoryCache[T]) Clear() { c.values = make(map[string]cacheValue[T]) } - c.latestHeight.Store(0) + c.latestVersion.Store(0) } -// evict removes one item from the cache, to make space for a new one, +// evict removes one value from the cache, to make space for a new one, // according to the configured eviction policy func (c *inMemoryCache[T]) evict() error { if c.config.historical { @@ -284,20 +300,20 @@ func (c *inMemoryCache[T]) evict() error { } } -// evictHistorical removes one item from the cache, to make space for a new one, -// according to the configured eviction policy. +// evictHistorical removes one value (and all its versions) from the cache, +// to make space for a new one, according to the configured eviction policy. func (c *inMemoryCache[T]) evictHistorical() error { switch c.config.evictionPolicy { case FirstInFirstOut: var oldestKey string var oldestTime time.Time for key, valueHistory := range c.valueHistories { - mostRecentHeight := valueHistory.sortedDescHeights[0] - value, exists := valueHistory.heightMap[mostRecentHeight] + mostRecentVersion := valueHistory.sortedDescVersions[0] + value, exists := valueHistory.versionToValueMap[mostRecentVersion] if !exists { return ErrCacheInternal.Wrapf( - "expected value history for key %s to contain height %d but it did not 💣", - key, mostRecentHeight, + "expected value history for key %s to contain version %d but it did not 💣", + key, mostRecentVersion, ) } diff --git a/pkg/client/query/cache/memory_test.go b/pkg/client/query/cache/memory_test.go index 844f47637..ac5a742ad 100644 --- a/pkg/client/query/cache/memory_test.go +++ b/pkg/client/query/cache/memory_test.go @@ -68,21 +68,21 @@ func TestInMemoryCache_NonHistorical(t *testing.T) { ) require.NoError(t, err) - // Add items up to max size + // Add values up to max keys err = cache.Set("key1", "value1") require.NoError(t, err) err = cache.Set("key2", "value2") require.NoError(t, err) - // Add one more item, should trigger eviction + // Add one more value, should trigger eviction err = cache.Set("key3", "value3") require.NoError(t, err) - // First item should be evicted + // First value should be evicted _, err = cache.Get("key1") require.ErrorIs(t, err, ErrCacheMiss) - // Other items should still be present + // Other values should still be present val, err := cache.Get("key2") require.NoError(t, err) require.Equal(t, "value2", val) @@ -101,34 +101,38 @@ func TestInMemoryCache_Historical(t *testing.T) { ) require.NoError(t, err) - // Test SetAtHeight and GetAtHeight - err = cache.SetAtHeight("key", "value1", 10) + // Test SetAsOfVersion and GetAsOfVersion + err = cache.SetAsOfVersion("key", "value1", 10) require.NoError(t, err) - err = cache.SetAtHeight("key", "value2", 20) + err = cache.SetAsOfVersion("key", "value2", 20) require.NoError(t, err) - // Test getting exact heights - val, err := cache.GetAtHeight("key", 10) + // Test getting exact versions + val, err := cache.GetAsOfVersion("key", 10) require.NoError(t, err) require.Equal(t, "value1", val) - val, err = cache.GetAtHeight("key", 20) + val, err = cache.GetAsOfVersion("key", 20) require.NoError(t, err) require.Equal(t, "value2", val) - // Test getting intermediate height (should return nearest lower height) - val, err = cache.GetAtHeight("key", 15) + // Test getting intermediate version (should return nearest lower version) + val, err = cache.GetAsOfVersion("key", 15) require.NoError(t, err) require.Equal(t, "value1", val) - // Test getting height before first entry - _, err = cache.GetAtHeight("key", 5) + // Test getting version before first entry + _, err = cache.GetAsOfVersion("key", 5) require.ErrorIs(t, err, ErrCacheMiss) - // Test getting height after last entry - val, err = cache.GetAtHeight("key", 25) + // Test getting version after last entry + val, err = cache.GetAsOfVersion("key", 25) require.NoError(t, err) require.Equal(t, "value2", val) + + // Test getting a version for a key that isn't cached + _, err = cache.GetAsOfVersion("key2", 20) + require.ErrorIs(t, err, ErrCacheMiss) }) t.Run("historical TTL expiration", func(t *testing.T) { @@ -138,11 +142,11 @@ func TestInMemoryCache_Historical(t *testing.T) { ) require.NoError(t, err) - err = cache.SetAtHeight("key", "value1", 10) + err = cache.SetAsOfVersion("key", "value1", 10) require.NoError(t, err) // Value should be available immediately - val, err := cache.GetAtHeight("key", 10) + val, err := cache.GetAsOfVersion("key", 10) require.NoError(t, err) require.Equal(t, "value1", val) @@ -150,40 +154,40 @@ func TestInMemoryCache_Historical(t *testing.T) { time.Sleep(150 * time.Millisecond) // Value should now be expired - _, err = cache.GetAtHeight("key", 10) + _, err = cache.GetAsOfVersion("key", 10) require.ErrorIs(t, err, ErrCacheMiss) }) - t.Run("pruning old heights", func(t *testing.T) { + t.Run("pruning old versions", func(t *testing.T) { cache, err := NewInMemoryCache[string]( WithHistoricalMode(10), // Prune entries older than 10 blocks ) require.NoError(t, err) - // Add entries at different heights - err = cache.SetAtHeight("key", "value1", 10) + // Add entries at different versions + err = cache.SetAsOfVersion("key", "value1", 10) require.NoError(t, err) - err = cache.SetAtHeight("key", "value2", 20) + err = cache.SetAsOfVersion("key", "value2", 20) require.NoError(t, err) - err = cache.SetAtHeight("key", "value3", 30) + err = cache.SetAsOfVersion("key", "value3", 30) require.NoError(t, err) // Add a new entry that should trigger pruning - err = cache.SetAtHeight("key", "value4", 40) + err = cache.SetAsOfVersion("key", "value4", 40) require.NoError(t, err) // Entries more than 10 blocks old should be pruned - _, err = cache.GetAtHeight("key", 10) + _, err = cache.GetAsOfVersion("key", 10) require.ErrorIs(t, err, ErrCacheMiss) - _, err = cache.GetAtHeight("key", 20) + _, err = cache.GetAsOfVersion("key", 20) require.ErrorIs(t, err, ErrCacheMiss) // Recent entries should still be available - val, err := cache.GetAtHeight("key", 30) + val, err := cache.GetAsOfVersion("key", 30) require.NoError(t, err) require.Equal(t, "value3", val) - val, err = cache.GetAtHeight("key", 40) + val, err = cache.GetAsOfVersion("key", 40) require.NoError(t, err) require.Equal(t, "value4", val) }) @@ -195,25 +199,25 @@ func TestInMemoryCache_Historical(t *testing.T) { require.NoError(t, err) // Set some historical values - err = cache.SetAtHeight("key", "value1", 10) + err = cache.SetAsOfVersion("key", "value1", 10) require.NoError(t, err) - err = cache.SetAtHeight("key", "value2", 20) + err = cache.SetAsOfVersion("key", "value2", 20) require.NoError(t, err) - // Regular Set should work with latest height + // Regular Set should work with latest version err = cache.Set("key", "value3") - require.NoError(t, err) + require.ErrorIs(t, err, ErrUnsupportedHistoricalModeOp) // Regular Get should return the latest value val, err := cache.Get("key") require.NoError(t, err) - require.Equal(t, "value3", val) + require.Equal(t, "value2", val) // Delete should remove all historical values cache.Delete("key") - _, err = cache.GetAtHeight("key", 10) + _, err = cache.GetAsOfVersion("key", 10) require.ErrorIs(t, err, ErrCacheMiss) - _, err = cache.GetAtHeight("key", 20) + _, err = cache.GetAsOfVersion("key", 20) require.ErrorIs(t, err, ErrCacheMiss) _, err = cache.Get("key") require.ErrorIs(t, err, ErrCacheMiss) @@ -227,10 +231,10 @@ func TestInMemoryCache_ErrorCases(t *testing.T) { require.NoError(t, err) // Attempting historical operations should return error - err = cache.SetAtHeight("key", "value", 10) + err = cache.SetAsOfVersion("key", "value", 10) require.ErrorIs(t, err, ErrHistoricalModeNotEnabled) - _, err = cache.GetAtHeight("key", 10) + _, err = cache.GetAsOfVersion("key", 10) require.ErrorIs(t, err, ErrHistoricalModeNotEnabled) }) @@ -327,9 +331,9 @@ func TestInMemoryCache_ConcurrentAccess(t *testing.T) { return default: key := "key" - err := cache.SetAtHeight(key, j, int64(j)) + err := cache.SetAsOfVersion(key, j, int64(j)) require.NoError(t, err) - _, _ = cache.GetAtHeight(key, int64(j)) + _, _ = cache.GetAsOfVersion(key, int64(j)) } } }()