Skip to main content
Version: 4.x

Metadata Format

The storage layout of a contract is reflected inside the metadata. It allows third-party tooling to work with contract storage and can also help to better understand the storage layout of any given contract.

Given a contract with the following storage:

#[ink(storage)]
pub struct MyContract {
balance: Balance,
block: BlockNumber,
lazy: Lazy<bool>,
}

The storage will be reflected inside the metadata as like follows:

"root": {
"layout": {
"struct": {
"fields": [
{
"layout": {
"leaf": {
"key": "0x00000000",
"ty": 0
}
},
"name": "balance"
},
{
"layout": {
"leaf": {
"key": "0x00000000",
"ty": 1
}
},
"name": "block"
},
{
"layout": {
"root": {
"layout": {
"leaf": {
"key": "0xb1f4904e",
"ty": 2
}
},
"root_key": "0xb1f4904e"
}
},
"name": "lazy"
}
],
"name": "MyContract"
}
},
"root_key": "0x00000000"
}

We observe that the storage layout is represented as a tree, where tangible storage values end up inside a leaf. Because of Packed encoding, leafs can share the same storage key, and in order to reach them you'd need to fetch and decode the whole storage cell under this key.

A root_key is meant to either be used to directly access a Packed storage field or to serve as the base key for calculating the actual keys needed to access values in non-Packed fields (such as Mappings).

Storage key calculation for ink! Mapping values

Base storage keys are always 4 bytes in size. However, the storage API of the contracts pallet supports keys of arbitrary length. In order to reach a mapping value, the storage key of said value is calculated at runtime.

The formula to calculate the base storage key S used to access a mapping value under the key K for a mapping with base key B can be expressed as follows:

S = scale::encode(B) + scale::encode(K)

Where the base key B is the root_key (of type u32) found in the contract metadata.

In words, SCALE encode the base (root) key of the mapping and concatenate it with the SCALE encoded key of the mapped value to obtain the actual storage key used to access the mapped value.

Given the following contract storage, which maps accounts to a balance:

#[ink(storage)]
pub struct Contract {
roles: Mapping<AccountId, Balance, ManualKey<0x12345678>>,
}

Now let's suppose we are interested in finding the balance for the account 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY. The storage key is calculated as follows:

  1. SCALE encode the base key of the mapping (0x12345678u32), resulting in 0x78563412
  2. SCALE encode the AccountId, which will be 0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d. Note that you'll need to convert the SS58 into a AccountId32 first.
  3. Concatenating those two will result in the key 0x78563412d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d.
let account_id = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY";
let account: AccountId32 = Ss58Codec::from_string(account_id).unwrap();
let storage_key = &(0x12345678u32, account).encode();
println!("0x{}", hex::encode(storage_key));
// 0x78563412d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d

Accessing storage items with the contractsApi runtime call API

There are two ways to query for storage fields of smart contracts from outside a contract. Both methods are accessible via the polkadot-js web UI.

The straight forward way to query a contracts storage is via a runtime API call, using the contractsApi endpoint provided by the contracts pallet. The endpoint provides a getStorage method, which just expects a contract address and a storage key as arguments.

For example, to access the root storage struct under the key 0x00000000 of a contract, just specify the contract's address and the storage key 0x00000000 as-is. The API call will return the scale-encoded root storage struct of the contract.

Accessing storage items with the childState RPC call API

Under the hood, each contract gets its own child trie, where its storage items are actually stored.

Additionally, the contracts pallet uses the Blake2 128 Concat Transparent hashing algorithm to calculate storage keys for any stored item inside the child trie. You'll need to account for that as well.

With that in mind, to directly access storage items of any on-chain contract using a childState RPC call, you'll need the following:

  • The child trie ID of the contract, represented as a PrefixedStorageKey
  • The hashed storage key of the storage field

Finding the contracts child trie ID

The child trie ID is the Blake2_256 hash of the contracts instantiation nonce concatenated to it's AccountId. You can find it in polkadot-js chainstate query interface, where you need to execute the contracts_contractInfoOf state query.

It can also be calculate manually according to the following code snippet. The instantiation note of the contract must be still be known. You can get it using the contracts_nonce chain state query in polkadot-js UI.

use sp_core::crypto::Ss58Codec;
use parity_scale_codec::Encode;

// Given our contract ID is 5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4
let account: AccountId32 =
Ss58Codec::from_string("5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4").unwrap();
// Given our instantiation nonce was 1
let nonce: u64 = 1;

// The child trie ID can be calculated as follows:
let trie_id = (&account, nonce).using_encoded(Blake2_256::hash);

Calculate the PrefixedStorageKey from the child trie ID

A PrefixedStorageKey based on the child trie ID can be constructed using the ChildInfo primitive as follows:

use sp_core::storage::ChildInfo;
let prefixed_storage_key = ChildInfo::new_default(&trie_id).into_prefixed_storage_key();

Calculate the storage key using transparent hashing

Finally, we calculate the hashed storage key of the storage item we are wanting to access. The algorithm is simple: Blake2_128 hash the storage key and then concatenate the unhashed key to the hash. Given you want to access the storage item under the 0x00000000, it will look like this in code:

use frame_support::Blake2_128Concat;

// The base key is 0x00000000
let storage_key = Blake2_128Concat::hash(&[0, 0, 0, 0]);

A full example

Let's recap the last few paragraphs into a full example. Given:

  • A contract at address 5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4
  • With instantiation nonce of 1
  • The root storage struct is to be found at base key 0x00000000

The following Rust program demonstrates how to calculate the PrefixedStorageKey of the contracts child trie, as well as the hashed key for the storage struct, which can then be used with the chilstate RPC endpoint function getStorage in polkadot-js to receive the root storage struct of the contract:

use frame_support::{sp_runtime::AccountId32, Blake2_128Concat, Blake2_256, StorageHasher};
use parity_scale_codec::Encode;
use sp_core::{crypto::Ss58Codec, storage::ChildInfo};
use std::ops::Deref;

fn main() {
// Find the child storage trie ID
let account_id = "5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4";
let account: AccountId32 = Ss58Codec::from_string(account_id).unwrap();
let instantiation_nonce = 1u64;
let trie_id = (account, instantiation_nonce).using_encoded(Blake2_256::hash);
assert_eq!(
hex::encode(trie_id),
"2fa252b7f996d28fd5d8b11098c09e56295eaf564299c6974421aa5ed887803b"
);

// Calculate the PrefixedStorageKey based on the trie ID
let prefixed_storage_key = ChildInfo::new_default(&trie_id).into_prefixed_storage_key();
println!("0x{}", hex::encode(prefixed_storage_key.deref()));
// 0x3a6368696c645f73746f726167653a64656661756c743a2fa252b7f996d28fd5d8b11098c09e56295eaf564299c6974421aa5ed887803b

// Calculate the storage key using transparent hashing
let storage_key = Blake2_128Concat::hash(&[0, 0, 0, 0]);
println!("0x{}", hex::encode(&storage_key));
// 0x11d2df4e979aa105cf552e9544ebd2b500000000
}