Formato de la Metadata
La estructura del storage de un contrato se ve reflejada dentro de la metadata. Permite a herramientas de terceros trabajar con un contrato y poder mejorar la comprensión de la estructura del storage de cualquier contrato.
Dado un contrato con el siguiente storage:
#[ink(storage)]
pub struct MyContract {
balance: Balance,
block: BlockNumber,
lazy: Lazy<bool>,
}
El storage se verá reflejado dentro de la metadata de la siguiente manera:
"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"
}
Observamos que la estructura del storage se representa con un árbol, en el cual los valores tangibles de storage terminan dentro de una leaf
. A causa de la codificación Packed
, las hojas pueden compartir la misma key del storage, y para acceder a ellas es necesario buscar y decodificar toda la celda que corresponde a esa key.
Una root_key
está destinada ya sea a ser utilizada para acceder directamente a un campo de storage Packed
o para servir como la key base para calcular las keys reales necesarias para acceder a los valores de los campos non-Packed
(por ejemplo Mapping
s)
Cálculo de la key del storage para valores de un ink! Mapping
Las keys base del storage siempre tienen un tamaño de 4 bytes. Sin embargo, la API de storage del pallet contracts
soporta keys de longitudes arbitrarias. Para acceder a un valor de un mapping, la key del storage de dicho valor se calcula en tiempo de ejecución.
La fórmula para calcular la key base de un storage S
utilizada para acceder a un valor de mapping que corresponde a la key K
para un mapping con una key base B
puede expresarse de la siguiente manera:
S = scale::encode(B) + scale::encode(K)
En donde la key base B
es la root_key
(de tipo u32
) encontrada en la metadata del contrato.
En otras palabras, codificar con SCALE la key base (root) del mapping y concatenarla con la key codificada con SCALE del valor de mapping para obtener la key del storage real utilizada para acceder al valor mapeado.
Dado el siguiente storage de un contrato, que mapea cuentas a un balance:
#[ink(storage)]
pub struct Contract {
roles: Mapping<AccountId, Balance, ManualKey<0x12345678>>,
}
Ahora supongamos que estamos interesados en encontrar el balance para la cuenta 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
. La key del storage se calcula así:
- Codificar en SCALE la key base del mapping (
0x12345678u32
), da como resultado0x78563412
- Codificar en SCALE la
AccountId
, que será0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d
. Tener en cuenta que primero será necesario convertir el SS58 en unaAccountId32
. - Concatenar estas dos dará como resultado la 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
Accediendo a elementos del storage con la llamada de runtime contractsApi
Hay dos maneras de consultar los campos del storage de los smart contracts desde afuera de un contrato. Ambos métodos son accesibles a través de la UI web de polkadot-js
.
La manera directa de consultar el storage de contratos es a través de una llamada runtime API
, utilizando el endpoint contractsApi
provisto por el contracts pallet. El endpoint provee un método getStorage
, que sólo espera una dirección de contrato y una key del storage como argumentos.
Por ejemplo, para acceder al struct del root storage correspondiente a la key 0x00000000
de un contrato, sólo especifique la dirección el contrato y la key del storage 0x00000000
. La llamada a la API devolverá el struct del root del contrato codificado en SCALE.
Accediendo a elementos del storage con la llamada RPC childState
Por detrás, cada contrato tiene su propio child trie, donde los elementos del storage son almacenados.
Además, el pallet contracts
usa Blake2 128 Concat
Algoritmo de hashing transparente
para calcular las keys de storage para cada item almacenado dentro del child trie. Deberá tener eso en cuenta.
Con eso en mente, para acceder directamente a lo items de storage de cualquier contrato on-chain usando una llamada RPC
childState, necesitará lo siguiente:
- El ID del child trie del contrato, representado como una
PrefixedStorageKey
- La key hasheada del campo del storage
Encontrando el ID del child trie de los contratos
El ID del child trie es el hash Blake2_256
del nonce de instanciación del contrato concatenado a su AccountId
. Puede encontrarlo en polkadot-js chainstate query interface
, donde debe ejecutar la query de estado contracts_contractInfoOf
.
También puede ser calculado manualmente de acuerdo al siguiente snippet de código. El nonce de instanciación del contrato debe ser conocido. Puede obtenerlo usando la query de estado contracts_nonce
en la UI de polkadot-js.
use sp_core::crypto::Ss58Codec;
use parity_scale_codec::Encode;
// Dado que nuestro contract ID es 5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4
let account: AccountId32 =
Ss58Codec::from_string("5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4").unwrap();
// Dado que nuestro nonce de instanciación fue 1
let nonce: u64 = 1;
// El ID del child trie puede ser calculado así:
let trie_id = (&account, nonce).using_encoded(Blake2_256::hash);
Calcular el PrefixedStorageKey
del ID del child trie
Una PrefixedStorageKey
basada en el ID del child trie puede ser construida usando la primitiva ChildInfo
así:
use sp_core::storage::ChildInfo;
let prefixed_storage_key = ChildInfo::new_default(&trie_id).into_prefixed_storage_key();
Calcular la key del storage usando hasheo transparente
Finalmente, calculamos la key del storage hasheada del item de storage al que queremos acceder. El algoritmo es simple: Hashear la storage con Blake2_128
y luego concatenarla con la key sin hashear. Dado que quiera acceder al item de storage correspondiente a 0x00000000
, el código se verá así:
use frame_support::Blake2_128Concat;
// La key base es 0x00000000
let storage_key = Blake2_128Concat::hash(&[0, 0, 0, 0]);
Ejemplo completo
Repasemos los últimos párrafos a través de un ejemplo completo. Dado:
- Un contrato en la dirección
5DjcHxSfjAgCTSF9mp6wQBJWBgj9h8uh57c7TNx1mL5hdQp4
- Con un nonce de instanciación de
1
- La struct del root storage será encontrado en la key base
0x00000000
El siguiente programa Rust demuestra como calcular la PrefixedStorageKey
del child trie del contrato, así como también la key hasheada del struct del storage, la cual puede ser usada con la función getStorage
del endpoint RPC childstate
en polkadot-js para recibir el struct del storage root del contrato:
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() {
// Encontrar el ID trie del storage hijo
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"
);
// Calcular la PrefixedStorageKey basada en el trie ID
let prefixed_storage_key = ChildInfo::new_default(&trie_id).into_prefixed_storage_key();
println!("0x{}", hex::encode(prefixed_storage_key.deref()));
// 0x3a6368696c645f73746f726167653a64656661756c743a2fa252b7f996d28fd5d8b11098c09e56295eaf564299c6974421aa5ed887803b
// Calcular la key del storage usando hashing transparente
let storage_key = Blake2_128Concat::hash(&[0, 0, 0, 0]);
println!("0x{}", hex::encode(&storage_key));
// 0x11d2df4e979aa105cf552e9544ebd2b500000000
}