Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
An explainer of the query file inside of the CosmWasm code framework
Query messages retrieve the relevant smart contract and execute the query message. The results of the query are then returned to the sender of the query message, providing users with information about the state of the blockchain and the smart contracts running on it.
Contracts can define query functions, or read-only operations meant for data-retrieval. Doing so allows contracts to expose rich, custom data endpoints with JSON responses instead of raw bytes from the low-level key-value store. Because the blockchain state cannot be changed, the node can directly run the query without a transaction.
Users can specify which query function alongside any arguments with a JSON QueryMsg
. Even though there is no gas fee, the query function’s execution is capped by gas determined by metered execution, which is not charged, as a form of spam protection.
This overview examines the core components of Secret Network smart contracts
An explainer of the Instantiate file inside of the CosmWasm code framework
The Instantiate function is the function that will run once (and only once) immediately upon initializing your smart contract after uploading it to the network. It is meant to set up the smart contract with all vital code that must be run first before any users have the ability to execute the contract. For those familiar with a solidity constructor on Ethereum, Instantiate serves the exact same purpose.
If you have any vital info to save to the state such as Config parameters or an admin/operator address to name a few options, those should probably be specified here. In addition, if your contract requires entropy for randomization or needs to register to receive a SNIP-20 token for payment, those operations should also be performed here, as other execution messages will rely on that data to be already instantiated.
Instantiate takes four arguments:
"deps"
: allows the contract to interact with the outside world by reading and updating the contract state, accessing other contract states, and using helper functions to work with contract addresses.
"env":
is an object that represents the current state of the blockchain when the contract is executed, including the blockchain's height, timestamp, and the address of the contract being called.
"info"
: metadata about the message that triggered the contract's execution, such as the sender's address and the native tokens that were sent with the message.
"msg":
is the message that triggers the contract's execution, which is currently represented by the "Empty" type in the codeblock above.
An explainer of the dependecies inside of the CosmWasm code framework
Deps
(or the mutable version DepsMut
) holds all external dependencies of the contract, and is designed to allow easy dependency injection at runtime. Those external dependencies are as follows:
As the name suggests, storage allows reading and writing data in a contract's memory. Secret Network contracts use a key-value store for reading and writing data and provide three easy-to-use methods to do so:
get(&self, key: &[u8]) -> Option<Vec<u8>>
reads data stored in the specified key
set(&self, key: &[u8], value: &[u8])
which writes data to the storage in the specified key
remove(&self, key: &[u8])
which deletes a key alongside its data.
You may have noticed that storage uses byte arrays exclusively to read and write data, so to make it more user-friendly there are certain libraries that the community has developed. You can learn more about this here.
Often the question of, "How much data can be stored in a contract?" is asked. The answer is that it’s complicated. Technically there’s no upper-bound on how much data a contract can store, but there are practical per-transaction limits.
Reading one byte of data “uses” 3 gas and writing one byte of data “uses” 30 gas, so with the current limit of 6_000_000 gas per block, the practical limit is that a contract can read — at most — 13.33Mb of data per transaction and can write — at most — 1.3Mb of data per transaction. This assumes that the contract doesn’t do anything but read and write data, the more complex the contract, the lower the amount of data you can read and write. Note that the limits are per transaction, you could write several gigabytes of data if you wanted to but it would take several transactions.
Api consists of a collection of callbacks to system functions defined outside of the wasm modules. Those are functions that are defined in the chain’s code, this allows calling useful functions without needing to include them in the contract’s code and assures a smaller binary size as well as a higher certainty that they’re going to be implemented correctly.
canonical_address(&self, human: &HumanAddr) -> StdResult<CanonicalAddr>
Takes a human readable address and returns a binary representation of it.
human_address(&self, canonical: &CanonicalAddr) -> StdResult<HumanAddr>
Takes a canonical address and returns a human readble address. This is the inverse of canonical_address
secp256k1_verify(&self, message_hash: &[u8], signature: &[u8], public_key: &[u8]) -> Result<bool, VerificationError>
Verifies message hashes (hashed unsing SHA-256) against a signature, with the public key of the signer, using the secp256k1 elliptic curve digital signature parametrization / algorithm.
secp256k1_recover_pubkey( &self, message_hash: &[u8], signature: &[u8], recovery_param: u8 ) -> Result<Vec, RecoverPubkeyError>
Recovers a public key from a message hash and a signature.
ed25519_verify( &self, message: &[u8], signature: &[u8], public_key: &[u8] ) -> Result<bool, VerificationError>
Verifies messages against a signature, with the public key of the signer, using the ed25519 elliptic curve digital signature parametrization / algorithm.
ed25519_batch_verify( &self, messages: &[&[u8]], signatures: &[&[u8]], public_keys: &[&[u8]] ) -> Result<bool, VerificationError>
Performs batch Ed25519 signature verification.
Batch verification asks whether all signatures in some set are valid, rather than asking whether each of them is valid. This allows sharing computations among all signature verifications, performing less work overall, at the cost of higher latency (the entire batch must complete), complexity of caller code (which must assemble a batch of signatures across work-items), and loss of the ability to easily pinpoint failing signatures.
This batch verification implementation is adaptive, in the sense that it detects multiple signatures created with the same verification key, and automatically coalesces terms in the final verification equation.
secp256k1_sign( &self, message: &[u8], private_key: &[u8] ) -> Result<Vec, SigningError>
Signs a message with a private key using the secp256k1 elliptic curve digital signature parametrization / algorithm.
ed25519_sign( &self, message: &[u8], private_key: &[u8] ) -> Result<Vec, SigningError>
Signs a message with a private key using the ed25519 elliptic curve digital signature parametrisation / algorithm.
(CosmWasm 1.0) debug
- a function that allows for debug prints, which can be used during development to print to STDOUT in a testnet or LocalSecret environment (replaces debug_print
from CosmWasm v0.10)
(New in Secret-Cosmwasm v1.1.10) gas_evaporate(evaporate: u32) -> u32
- this API function allows a contract to consume a set amount of gas. Use together with check_gas
in order to create contract calls that consume an exact amount of gas regardless of the code path taken. Documentation is here.
(New in Secret-Cosmwasm v1.1.10) check_gas() -> u64
- this API returns the current amount of gas consumed by a contract call.
An explainer on the varying storage frameworks for Secret contracts
CosmWasm uses a key-value storage design. Smart contracts can store data in binary, access it through a storage key, edit it, and save it. Similar to a HashMap, each storage key is associated with a specific piece of data stored in binary. The storage keys are formatted as references to byte arrays (&[u8]
).
One advantage of the key-value design is that a particular data value is only loaded when the user explicitly loads it using its storage key. This prevents any unnecessary data from being processed, saving resources.
Any type of data may be stored this way as long as the user can serialize/deserialize (serde) the data to/from binary. Doing this manually every single time is cumbersome and repetitive, this is why we have wrapper functions that does this serde process for us.
All the data is actually stored in deps.storage
, and the examples below show how to save/load data to/from there with a storage key.
Creating a storage key is simple, any way of generating a constant &[u8]
suffices. People often prefer generating these keys from strings as shown in the example below.
For example, the above key is likely used to store some data related to core configuration values of the contract. The convention is that storage keys are often all created in state.rs
, and then imported to contract.rs
. However, since storage keys are just constants, they could be declared anywhere in the contract.
The example above also highlights that storage keys are not meant to be secret nor hard to guess. Anyone who has the open source code can see what the storage keys are (and of course this is not enough for a user to load any data from the smart contract).
As mentioned above, serializing/deserializing data while loading/saving it with a key is cumbersome. This is why we often use wrapper functions written by community members. There are three common wrapper functions that are included in the state.rs
of most secret contract templates.
A commonly used wrapper function to save data is the following. This function overwrites previously saved data associated to that storage key.
Note that value
can be of any Struct type. The only condition is that this Struct must derive the Serialize and Deserialize traits from serde with the following line above its Struct declaration.
There are some structs that cannot be serialized/deserialized by the bincode2 struct these wrapper functions use! See JSON Storage Wrapper Functions section below to see what happens in this case.
The wrapper may be used to save data in the following manner:
A commonly used wrapper function to load data from storage is the following:
Note that this function throws an error if there is no data previously saved with that storage key.
When loading data, rust must be told what Struct to expect after deserializing.
In some instances you may be unsure whether there is any data stored with a particular key. In this case you want to use may_load()
which wraps any data inside within an option. Returning None
if there is no value saved with that key, and returning Some(value)
if there is some value saved. An example function for this is:
One of the most common use cases of this function is when retrieving viewing keys for users. However, viewing keys use PrefixedStorage, and we will see this in the next section. So instead, I will show a line of code that retrieves a list of minters for an NFT.
This line of code returns the list of minters if it is saved to MINTERS_KEY, otherwise, it returns an empty list.
A commonly used wrapper function to remove saved data from storage is the following. This might be the only wrapper function that does not make anything more convenient, because there is no serialize/deserialize implemented.
The following code removes minters. This code does not let you know if there was any previously saved data to that storage key.
The wrapper functions we learned above use bincode2 struct (from secret-toolkit
) to serde the data being saved/read on the smart contract. However, bincode2 uses floats when deserializing rust enum variants, thus, bincode2 cannot serde enum variants in cosmwasm. This is why cosmwasm uses Json serde by default, not bincode2.
The following is an example, from the reference SNIP-721 implementation, of a struct that cannot be saved/loaded by the wrapper functions we saw above because it uses an enum.
In these cases, we can use the Json struct from secret-toolkit
to serde structs that use enums. This also creates the need for for new wrapper functions
The usage of this function is extremely similar to the save
wrapper function we discussed above.
bincode2 serde is more efficient than json serde
The usage of this function is extremely similar to the load
wrapper function we discussed above.
The usage of this function is extremely similar to the may_load
wrapper function we discussed above.
The remove
wrapper function above works the same because it doesn't serde
An explainer of the Execute file inside of the CosmWasm code framework
Execution Messages are contract messages that trigger the execution of a smart contract and perform specific operations, such as updating the contract state or transferring tokens. If you’re familiar with RPC or AJAX, you can think of execution messages as the code that runs when a remote procedure is called. On Secret Network, execution messages are usually designed to be functions that run as quickly as possible and exit as early as possible when any errors are encountered, as this will help save gas. You can learn more about contract optimization here.
The standard practice on Secret Network is to have an enum
with all the valid message types and reject all messages that don’t follow the usage pattern dictated in that enum; this enum is conventionally called ExecuteMsg
and is usually in a file called msg.rs
As mentioned earlier, the standard way to choose what the contract should execute is to look at which ExecuteMsg
is passed to the function. Let’s look at an example.
In this simple example, the code looks at the message passed, if the passage is the Increment
message, it calls the function try_increment
, otherwise, if the message is the Reset
message, it calls the function try_reset
with the count.
Remember, Rust has implicit returns for lines that don’t end in a semicolon, so the result of the two functions above is returned as the result of the execution message.
You may have noticed that the execution message takes two arguments in addition to the msg: deps
and env
. Let’s look at those more closely.
As the name perhaps implies, env
contains all the information about the environment the contract is running in, but what does that mean exactly? On Secret Network the properties available in the Env struct are as follows:
block: this contains all the information about the current block. This is the block height (height
), the current time as a unix timestamp (time
) and the chain id (chain_id
) such as secret-4 or pulsar-3.
message: contains information that was sent as part of the payload for the execution. This is the sender, or the wallet address of the person that called the handle function and sent_funds which contains a vector of native funds sent by the caller (SNIP-20s are not included).
contract: contains a property with the contract’s address.
contract_code_hash: this is a String containing the code hash of the current contract, it’s useful when registering with other contracts such as SNIP20s or SNIP721s or when working on a factory contract.
Now that you have an overview on what all those properties are, let’s take a look at a simple Execution Message.
The execution message above reads the state from the storage and increments the count property by one, then prints out the new count, if successful. But wait — where’s the storage hiding, what’s config
? What’s .update
?
As mentioned previously, developers don’t really like working with raw binary data, so many resort to using more human friendly ways, you can find out more on the page about storage, here, but for the sake of this example let’s look at what that config function is doing; the developers of this contract, opted for using storage singletons. A storage singleton can be thought as a prefixed typed storage solution with simple-to-use methods. This allows the developer to define a structure for the data that needs to be stored and handles encoding end decoding it, so the developer doesn’t have to think about it. This is how it’s implemented in this contract:
The config function returns a singleton over the config key using the State
struct as its type, this means that when reading and writing data from the storage, the singleton automatically serializes or deserializes the State struct. The update method is a shorthand that will load the data, perform the specified action, and store the result in the storage, which makes for a very intuitive experience.
You may have noticed that the return type for a handle message is Response
. Let's take a look at how Response
is defined:
It contains a vector of CosmosMsg
(messages), an optional vector of type LogAttribute
(log), and an optional Binary field (data).
LogAttribute
is defined as a simple struct with two String
fields: key
and value
. Binary is just a Vec<u8>
.
CosmosMsg is an enum
with a transaction that will be executed once the execution message returns a response.
There are three types of messages:
BankMsg
: sends SCRT from one place to another (such as from the contract's wallet to the caller's wallet)
StakingMsg
: includes messages for Delegating, Redelegating, Undelegating, and withdrawing rewards to/from a delegator.
WasmMsg
: includes two messages: Execute
to execute another contract, and Instantiate
to instantiate a contract.
AppendStore is meant to replicate the functionality of an append list in a cosmwasm efficient manner. The length of the list is stored and used to pop/push items to the list. It also has a method to create a read only iterator.
This storage object also has the method remove
to remove a stored object from an arbitrary position in the list, but this can be extremely inefficient.
âť— Removing a storage object further from the tail gets increasingly inefficient. We recommend you use
pop
andpush
whenever possible.
The same conventions from Item
also apply here, that is:
AppendStore has to be told the type of the stored objects. And the serde optionally.
Every methods needs it's own reference to deps.storage
.
To import and initialize this storage object as a static constant in state.rs
, do the following:
âť— Initializing the object as const instead of static will also work but be less efficient since the variable won't be able to cache length data.
Often times we need these storage objects to be associated to a user address or some other key that is variable. In this case, you need not initialize a completely new AppendStore inside contract.rs
. Instead, you can create a new AppendStore by adding a suffix to an already existing AppendStore. This has the benefit of preventing you from having to rewrite the signature of the AppendStore. For example
Sometimes when iterating these objects, we may want to load the next n
objects at once. This may be prefered if the objects we are iterating over are cheap to store or if we know that multiple objects will need to be accessed back to back. In such cases we may want to change the internal indexing size (default of 1). We do this in state.rs
:
The main user facing methods to read/write to AppendStore are pop
, push
, get_len
, set_at
(which replaces data at a position within the length bound), clear
(which deletes all data in the storage), remove
(which removes an item in an arbitrary position, this is very inefficient). An extensive list of examples of these being used can be found inside the unit tests of AppendStore found in append_store.rs
.
AppendStore also implements a readonly iterator feature. This feature is also used to create a paging wrapper method called paging
. The way you create the iterator is:
More examples can be found in the unit tests. And the paging wrapper is used in the following manner:
This hashmap-like storage structure uses generic typed keys to store objects.
An example use-case for a keymap is if you want to contain a large amount of votes and iterate over them in the future. Since iterating over large amounts of data at once may be prohibitive, a keymap allows you to specify the amount of data that will be returned in each page. We will implement this voting example below to show how keymaps can be utilized in your own projects.
To import this package (and also the packages that we will be using for unit tests), add the following dependencies to your Cargo.toml
file
To import and initialize a keymap, use the following packages in your test environment:
Now let's write our first test!
Let's start by creating a function that inserts key-value pairs into a keymap. This code defines a test function called test_keymap_perf_vote_insert()
that creates a new Keymap
that maps Vec<u8>
keys to String
values, and then inserts 1000 key-value pairs into the Keymap
. Each key is a Vec<u8>
containing an integer from 0 to 999, and each value is a String
containing the text "I vote yes".
This test is passing! Which means that it asserts that the Keymap
contains 1000 key-value pairs, which map a number from 0 - 999 with the string "I vote yes."
Iterating with Keymaps
There are two methods that create an iterator in Keymap. These are .iter
and .iter_keys
. iter_keys
only iterates over the keys whereas iter
iterates over (key, item) pairs. Let's use iter
to test the iterator functionality of aKeymap
that maps Vec<u8>
keys to a struct Vote.
We are going to create a new Keymap
that maps Vec<u8>
keys to a struct Vote
that has two fields, vote
and person
. It then inserts two key-value pairs into the Keymap
, where the keys are the byte vectors b"key1".to_vec()
and b"key2".to_vec()
, and the values are Vote
structs containing information about the vote and the person who cast it.
We then use iter()
to check that the size of the iterator is 2, which means there are two key-value pairs in the Keymap:
Our test is passing! This means that the first element returned by the iterator matches the expected key-value pair for key1
, and that the second element returned by the iterator matches the expected key-value pair for key2
.
Our first approach might be to use a hashmap that stores all the passwords, and save it as binary using the methods we already know. So we would add the following lines to the init
And then we can proceed to load, and add each users' password whenever a user sends the create_password
HandleMsg. Note that I'm assuming we would generate the &[u8]
key for the hashmap from the user's wallet address. This approach has a huge problem! That is whenever we want to add another wallet's password to the hashmap, we must load the entire hashmap with all the passwords stored inside it. This will increase gas costs as we gain thousands of users. Moreover, loading the hashmap for queries will strain the node.
This method does work! However, what if we want to save additional things that is associated to each user, such as token balances. Then using save on the same key would overwrite our previous data. The solution is to add a prefix to the storage key so that we know it belongs to the password property. One way to implement this is the following:
This works but can be cumbersome and ugly, especially if you need to build more functionalities around this idea. This is exactly what prefixed storage does in the background! Prefixed Storage is built to solve this problem while hiding away all the ugliness.
The compiler only allows one mutable reference to the underlying storage to be valid at one point. Thus, you cannot have more than one mutable Prefixed Storage objects at the same time.
We would instantiate a mutable PrefixedStorage object using the following lines of code to save a new password
The reason why we can use the same wrapper functions is because Prefixed Storage implements the Storage trait, even though most of what it's doing is to add a prefix on top of the keys we provide to it.
We may use both load
and may_load
We may load data from a mutable Prefixed Storage instance with the following lines of code
In queries, we don't want to use PrefixedStorage struct. Instead we use ReadonlyPrefixedStorage struct. It works in the exact same way, except that you cannot save to it.
You can have as many ReadonlyPrefixedStorage objects as you want at the same time, unlike PrefixedStorage.
We can use the remove
wrapper function for this.
A Keymap is hashmap-like storage structure that uses generic typed keys to store objects. It allows iteration with paging over keys and/or items without guaranteed ordering, although the order of insertion is preserved until you remove objects.
For further examples demonstrating the usage of keymaps, refer to the .
Before we understand what the PrefixedStorage struct is, we should first understand the problem they attempt to solve. We may be tempted to think that the key-value model as described in solves all our storage needs. And we'd technically be correct in thinking that. It is possible to efficiently store vast amounts of user data by just using the methods described in that page. However, we will see that doing this would be cumbersome for the developer in many cases. That's why there are many additional storage structures built on top of the methods described in . Prefixed storage is an additional structure built on top of these methods for the convenience of the developer.
We will go over a storage problem and discuss how we might tackle it with the methods we learnt in to get to the root of what prefixed storage actually is. Suppose that we have contract that stores a different password (String) for each wallet address that interacts with it (so that the wallets can later give that password when querying the contract). How would we want to store all these passwords so that they can be checked when it is time for queries?
We now realize that we can actually generate&[u8]
storage keys for each user wallet and save the password directly to deps.storage
as described in . This way, we can use may_load
to check if a user has a password, and if he does, learn what it is without loading all the other passwords. Then we would use the following lines of code to save a password
PrefixedStorage is a struct that keeps track of all the storage keys that are used to store various data under a shared namespace similar to described . Prefixed storage is often used to keep track of storage keys of deps.storage
, but it can in fact be used to store the keys of other storage structures.
Let's solve that was mentioned above with prefixed storage. The solution to this problem resembles . You will learn more about this in the coming sections. We first define a namespace (prefix) in state.rs
.
The idea then is that all the wrapper functions that we've learnt in also work on prefixed storage. We will only demonstrate save
and may_load
by example.
Notice that we are using the same storage wrapper functions from on prefixed storage.
Optimizing your code is especially important on blockchain due to the fact that inefficient code costs your users money and can jam up the entire network. With any function you make, try to complete it in as few steps as possible and avoid unnecessary computation. One of the most costly actions you can make is loading and saving data, thus you should ensure you are only saving what is needed. Aside from this, there are a few other tips we can provide.
When dealing with large sets of data, vectors and arrays can be especially problematic because in order to save load a single item within them, you must effectively load the entire vector and all of its contents, which naturally can amount to an enormous amount of inefficiency when you only need one item! There are several ways around this: Keymap, AppendStore, or even deconstructing vectors into a set of data keyed to a counter variable. Research each options pros and cons, and choose which one is best for your particular contract.