Skip to main content

Querying Contract State

There are many cases where you want to view the state of a contract. Both as an external client (using the cli), but also while executing a contract. For example, we discussed resolving names like "Alice" or "Bob" in the last section, which would require a query to another contract. We will first cover the two types of queries - raw and custom - then look at the semantics of querying via an external client, as well as an internal client (another contract). We will pay special attention not only to how it works practically but also to the design and security issues of executing queries from one contract to another.

Raw Queries

The simplest query to implement is just raw read access to the key-value store. If the caller (either an external client or another contract) passes in the raw binary key that is used in the contract's storage, we can easily return the raw binary value. The benefit of this approach is that it is very easy to implement and universal. The downside is that it links the caller to the implementation of the storage and requires knowledge of the exact contract being executed.

This is implemented inside the wasmd runtime and circumvents the VM. As a consequence, it requires no support from the CosmWasm contract and all contract state is visible. Such a query_raw function is exposed to all callers (external and internal).

Custom Queries

There are many cases where it is undesirable to couple tightly to implementation, and we would rather depend on an interface. For example, we will define a standard for "ERC20" ExecuteMsg for calling the contract and we would want to define such a standard for a QueryMsg. For example, query balance by address, query allowance via granter + grantee, query token info (ticker, decimals, etc). By defining a standard interface, we allow many implementations, including complex contracts, where the "ERC20" interface is only a small subset of their functionality.

To enable custom queries, we allow each contract to expose a query function, that can access its data store in read-only mode. It can load any data it wishes and even performs calculations on it. This method is exposed as query_custom to all callers (external and internal). The data format (both query and response) is anything the contract desires and should be documented in the public schema, along with ExecuteMsg and InitMsg.

Note that executing a contract may consume an unbounded amount of gas. Whereas query_raw will read one key and has a small, mostly fixed cost, we need to enforce a gas limit on these queries. This is done differently for external and internal calls and is discussed below.

External Queries

External queries are the typical way all web and cli clients work with the blockchain. They call Tendermint RPC, which calls into abci_query in the Cosmos SDK, which delegates down to the module to handle it. As far as I know, there is an infinite gas limit on queries, as they are only executed on one node, and cannot slow down the entire blockchain. This functionality is generally not exposed on validating nodes. The query functionality exposed in the current SDK is hard coded and has execution time limits designed by the developers. This limits abuse. But what about someone uploading a wasm contract with an infinite loop, and then using that to DoS any public RPC node that exposes querying?

To avoid such issues, we need to define some fixed gas limit for all query_custom transactions called externally. This will not charge a fee but is used to limit abuse. However, it is difficult to define a standard value, for a free public node would prefer a small amount, but I may want to sync my archive node and perform complex queries. Thus, a gas limit for all query_custom calls can be defined in an app-specific configuration file, which can be customized by each node operator, with a sensible default limit. This will allow public nodes to protect themselves from complex queries, while still allowing optional queries to perform a large aggregation over all contract data in specially-configured nodes.

Note that the abci_query call never reads the current "in-progress" state of the modules, but uses a read-only snapshot of the state after the last committed block.

Internal Queries

While many interactions between contracts can easily be modeled by sending messages, there are some cases where we would like to synchronously query other modules, without altering their state. For example, if I want to resolve a name to a Address, or if I want to check the KYC status of some account (in another contract) before enabling an action. One could model this as a series of messages, but it is quite complex and makes such simple use-cases almost unusable in the system.

However, this design violates one of the basic principles of the actor model, namely that each contract has exclusive access to its own internal state. (Both query_raw and query_custom fail in this regard). Far from just being a theoretical issue, this may lead to concurrency and reentrancy issues if not handled correctly. We do not want to push such safety-critical reasoning into the laps of the contract developers, but rather provide these security guarantees in the platform. However, providing old data also leads to many possible errors and bugs, especially since we use the same Querier interface to interact with the native SDK modules, including querying the contract's own balance.

As such, we provide the Querier with read-only access to the state snapshot right before the execution of the current CosmWasm message. Since we take a snapshot and both the executing contract and the queried contract have read-only access to the data before the contract execution, this is still safe with Rust's borrowing rules (as a placeholder for secure design). The current contract only writes to a cache, which is flushed afterward on success.

Another issue is to avoid reentrancy. Since these queries are called synchronously, they can call back into the calling contract and possibly cause issues. Since queries only have read-only access and cannot have side effects, this is not nearly as dangerous as executing a remote contract synchronously, but still may be an issue to consider. Notably, it will only have access to the state before the current execution. I cannot see a place where this could cause more errors than a query function intentionally returning false data, but it is a place to investigate more.

Since all queries are performed as part of a transaction, that already has a strongly enforced gas limit, we don't need extra work here. All storage reads and data processing performed as part of a query are deducted from the same gas meter as the rest of the transaction and thus limit processing time. We consider adding explicit guards against re-entrancy or max query depth, but have not enforced them yet in wasmd. As more work on cross-contract queries comes to fruition, this is another place to investigate.