Rust Contracts
Storage

Storage

⚠️
The Rust SDK is experimental. Support and bug fixes are low priority compared to the Python SDK. See genlayer_sdk on crates.io for the latest published version.

Intelligent contracts store data on chain in persistent storage. The Rust SDK provides a type-safe storage system where each type maps to a handle that reads and writes directly from blockchain storage slots. Storage starts zero-initialized.

Storage Concepts

Storage is organized around two core traits:

  • StorageType -- maps a value type to its storage handle and size
  • StorageValue -- provides get() and set() on scalar handles

A Slot is a 32-byte storage region. Complex types derive child slots via SHA3-256 hashing.

Defining Contract State with record!

The record! macro defines a struct whose fields are laid out in storage:

use genlayer_sdk::storage::{Root, DynArray};
use genlayer_sdk::calldata::Address;
 
genlayer_sdk::record!(MyState {
    owner: Address,
    counter: u32,
    items: DynArray<u64>,
});

Each field becomes a method that returns the field's storage handle. You use .get() and .set() on scalar handles:

let root = Root::<MyState>::get();
let state = root.contract_instance().get();
 
// Read
let owner = state.owner().get();
let count = state.counter().get();
 
// Write
state.counter().set(count + 1);
⚠️

Root::<T>::get() returns the contract root, which wraps your state type T along with internal fields (contract code, locked slots, upgraders). Always access your state via root.contract_instance().get().

Scalar Types

Rust TypeStorage HandleSize
u8..u128StorageU8..StorageU1281..16 bytes
i8..i128StorageI8..StorageI1281..16 bytes
f32, f64StorageF32, StorageF644, 8 bytes
boolStorageBool1 byte
AddressStorageAddress20 bytes
U256StorageU25632 bytes
StringStorageStrindirect
Vec<u8>StorageBytesindirect
()--0 bytes

Scalar handles have .get() and .set() methods:

state.counter().set(42);
let val = state.counter().get(); // 42

StorageStr and StorageBytes have additional methods:

state.name().store("Alice");
let name = state.name().load(); // "Alice"
let len = state.name().len();

Fixed-Size Arrays

Use [T; N] for fixed-size arrays. The handle is StorageArray<T, N>:

genlayer_sdk::record!(Board {
    cells: [u8; 9],
});
 
let board = /* ... */;
board.cells().index(0).set(1);
let val = board.cells().index(4).get();

Dynamic Arrays (DynArray)

DynArray<T> is a variable-length array stored indirectly (like Python's DynArray):

use genlayer_sdk::storage::{DynArray, Slot, StorageType};
 
let arr = <DynArray<u32>>::handle_at(slot, 0);
 
// Append elements
arr.append_slot().set(10);
arr.append_slot().set(20);
 
// Read
let len = arr.len();          // 2
let val = arr.index(0).get(); // 10
 
// Remove
arr.pop();
arr.clear();

Tree Maps (TreeMap)

TreeMap<K, V> is a sorted key-value map backed by an AVL tree (like Python's TreeMap):

use genlayer_sdk::storage::TreeMap;
 
genlayer_sdk::record!(Balances {
    balances: TreeMap<Address, U256>,
});
 
let state = /* ... */;
let balances = state.balances();
 
// Insert / update (returns the value handle)
if let Some(handle) = balances.get(&addr) {
    let cur = handle.get();
    handle.set(cur + amount);
} else {
    balances.insert(&addr).set(amount);
}
 
// Remove
balances.remove(&addr);

Nested Records

Records can be nested and generic:

genlayer_sdk::record!(Pair[K, V] {
    first: K,
    second: V,
});
 
genlayer_sdk::record!(MyState {
    pair: Pair[u32, u64],
});

Indirection

Indirection<T> stores the data at a derived slot rather than inline. This is used internally by Root and can be used for large nested structures:

use genlayer_sdk::storage::Indirection;
 
genlayer_sdk::record!(MyState {
    big_data: Indirection<DynArray<u8>>,
});

Default Values

Storage is zero-initialized:

TypeDefault value
u*, i*0
f32/f640.0
boolfalse
Address[0; 20]
U2560
String""
DynArrayempty (len 0)
TreeMapempty

Records are zero-initialized recursively.

Full Example

use genlayer_sdk::abi::entry::MessageData;
use genlayer_sdk::abi::entry::contract_def::Contract;
use genlayer_sdk::calldata::Value;
use genlayer_sdk::storage::Root;
 
genlayer_sdk::record!(CounterState {
    counter: u32,
});
 
#[derive(Default)]
pub struct CounterContract;
 
impl Contract for CounterContract {
    fn handle_main(
        &mut self,
        _message: MessageData,
        _data: bytes::Bytes,
    ) -> Result<Value, String> {
        let root = Root::<CounterState>::get();
        let state = root.contract_instance().get();
 
        let current = state.counter().get();
        state.counter().set(current + 1);
 
        Ok(Value::Map(std::collections::BTreeMap::from([(
            "counter".to_owned(),
            Value::Int(num_bigint::BigInt::from(current + 1)),
        )])))
    }
 
    fn handle_sandbox(
        &mut self,
        _message: MessageData,
        _data: bytes::Bytes,
    ) -> Result<Vec<u8>, String> {
        unimplemented!()
    }
 
    fn handle_consensus_stage(
        &mut self,
        _message: MessageData,
        _data: bytes::Bytes,
        _stage_data: genlayer_sdk::abi::entry::contract_def::ConsensusStageData,
    ) -> Result<Value, String> {
        unimplemented!()
    }
}
 
genlayer_sdk::contract_main!(CounterContract);