Storing Key-Value Pairs with Mappings
Storage mappings in Cairo provide a way to associate keys with values and persist them in the contract's storage. Unlike traditional hash tables, storage mappings do not store the key data itself; instead, they use the hash of the key to compute an address that corresponds to the storage slot where the corresponding value is stored. Therefore, it is not possible to iterate over the keys of a storage mapping.
Mappings do not have a concept of length or whether a key-value pair is set. All values are by default set to 0. As such, the only way to remove an entry from a mapping is to set its value to the default value for the type, which would be 0
for the u64
type.
The Map
type, provided by the Cairo core library, inside the core::starknet::storage
module, is used to declare mappings in contracts.
To declare a mapping, use the Map
type enclosed in angle brackets <>
, specifying the key and value types. In Listing 14-3, we create a simple contract that stores values mapped to the caller's address.
The
Felt252Dict
type is a memory type that cannot be stored in contract storage. For persistent storage of key-value pairs, use theMap
type, which is a [phantom type][phantom types] designed specifically for contract storage. However,Map
has limitations: it can't be instantiated as a regular variable, used as a function parameter, or included as a member in regular structs.Map
can only be used as a storage variable within a contract's storage struct. To work with the contents of aMap
in memory or perform complex operations, you'll need to copy its elements to and from aFelt252Dict
or other suitable data structure.
Declaring and Using Storage Mappings
use core::starknet::ContractAddress;
#[starknet::interface]
trait IUserValues<TState> {
fn set(ref self: TState, amount: u64);
fn get(self: @TState, address: ContractAddress) -> u64;
}
#[starknet::contract]
mod UserValues {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Map
};
use core::starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_values: Map<ContractAddress, u64>,
}
impl UserValuesImpl of super::IUserValues<ContractState> {
fn set(ref self: ContractState, amount: u64) {
let caller = get_caller_address();
self.user_values.entry(caller).write(amount);
}
fn get(self: @ContractState, address: ContractAddress) -> u64 {
self.user_values.entry(address).read()
}
}
}
To read the value corresponding to a key in a mapping, you first need to retrieve the storage pointer associated with that key. This is done by calling the entry
method on the storage mapping variable, passing in the key as a parameter. Once you have the entry path, you can call the read
function on it to retrieve the stored value.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IUserValues<TState> {
fn set(ref self: TState, amount: u64);
fn get(self: @TState, address: ContractAddress) -> u64;
}
#[starknet::contract]
mod UserValues {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Map
};
use core::starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_values: Map<ContractAddress, u64>,
}
impl UserValuesImpl of super::IUserValues<ContractState> {
fn set(ref self: ContractState, amount: u64) {
let caller = get_caller_address();
self.user_values.entry(caller).write(amount);
}
fn get(self: @ContractState, address: ContractAddress) -> u64 {
self.user_values.entry(address).read()
}
}
}
Similarly, to write a value in a storage mapping, you need to retrieve the storage pointer corresponding to the key. Once you have this storage pointer, you can call the write
function on it with the value to write.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IUserValues<TState> {
fn set(ref self: TState, amount: u64);
fn get(self: @TState, address: ContractAddress) -> u64;
}
#[starknet::contract]
mod UserValues {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Map
};
use core::starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_values: Map<ContractAddress, u64>,
}
impl UserValuesImpl of super::IUserValues<ContractState> {
fn set(ref self: ContractState, amount: u64) {
let caller = get_caller_address();
self.user_values.entry(caller).write(amount);
}
fn get(self: @ContractState, address: ContractAddress) -> u64 {
self.user_values.entry(address).read()
}
}
}
Nested Mappings
You can also create more complex mappings with multiple keys. To illustrate this, we'll implement a contract representing warehouses assigned to users, where each user can store multiple items with their respective quantities.
The user_warehouse
mapping is a storage mapping that maps ContractAddress
to another mapping that maps u64
(item ID) to u64
(quantity). This can be implemented by declaring a Map<ContractAddress, Map<u64, u64>>
in the storage struct. Each ContractAddress
key in the user_warehouse
mapping corresponds to a user's warehouse, and each user's warehouse contains a mapping of item IDs to their respective quantities.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IWarehouseContract<TState> {
fn set_quantity(ref self: TState, item_id: u64, quantity: u64);
fn get_item_quantity(self: @TState, address: ContractAddress, item_id: u64) -> u64;
}
#[starknet::contract]
mod WarehouseContract {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Map
};
use core::starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_warehouse: Map<ContractAddress, Map<u64, u64>>,
}
impl WarehouseContractImpl of super::IWarehouseContract<ContractState> {
fn set_quantity(ref self: ContractState, item_id: u64, quantity: u64) {
let caller = get_caller_address();
self.user_warehouse.entry(caller).entry(item_id).write(quantity);
}
fn get_item_quantity(self: @ContractState, address: ContractAddress, item_id: u64) -> u64 {
self.user_warehouse.entry(address).entry(item_id).read()
}
}
}
In this case, the same principle applies for accessing the stored values. You need to traverse the keys step by step, using the entry
method to get the storage path to the next key in the sequence, and finally calling read
or write
on the innermost mapping.
use core::starknet::ContractAddress;
#[starknet::interface]
trait IWarehouseContract<TState> {
fn set_quantity(ref self: TState, item_id: u64, quantity: u64);
fn get_item_quantity(self: @TState, address: ContractAddress, item_id: u64) -> u64;
}
#[starknet::contract]
mod WarehouseContract {
use starknet::storage::{
StoragePointerReadAccess, StoragePointerWriteAccess, StoragePathEntry, Map
};
use core::starknet::{ContractAddress, get_caller_address};
#[storage]
struct Storage {
user_warehouse: Map<ContractAddress, Map<u64, u64>>,
}
impl WarehouseContractImpl of super::IWarehouseContract<ContractState> {
fn set_quantity(ref self: ContractState, item_id: u64, quantity: u64) {
let caller = get_caller_address();
self.user_warehouse.entry(caller).entry(item_id).write(quantity);
}
fn get_item_quantity(self: @ContractState, address: ContractAddress, item_id: u64) -> u64 {
self.user_warehouse.entry(address).entry(item_id).read()
}
}
}
Storage Address Computation for Mappings
The address in storage of a variable stored in a mapping is computed according to the following rules:
- For a single key
k
, the address of the value at keyk
ish(sn_keccak(variable_name), k)
, whereh
is the Pedersen hash and the final value is taken modulo \( {2^{251}} - 256\). - For multiple keys, the address is computed as
h(...h(h(sn_keccak(variable_name), k_1), k_2), ..., k_n)
, withk_1, ..., k_n
being all keys that constitute the mapping.
If the key of a mapping is a struct, each element of the struct constitutes a key. Moreover, the struct should implement the Hash
trait, which can be derived with the #[derive(Hash)]
attribute.
Summary
- Storage mappings allow you to map keys to values in contract storage.
- Use the
Map
type to declare mappings. - Access mappings using the
entry
method andread
/write
functions. - Mappings can contain other mappings, creating nested storage mappings.
- The address of a mapping variable is computed using the
sn_keccak
and the Pedersen hash functions.