Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core Lib Documentation: storage_node module #7043

Merged
merged 6 commits into from
Jan 11, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 104 additions & 80 deletions corelib/src/starknet/storage/storage_node.cairo
Original file line number Diff line number Diff line change
@@ -1,86 +1,110 @@
use super::{Mutable, StoragePath};
//! Storage nodes provide a way to structure contract storage data, reflecting their structure in
//! the storage address computation of their members. They are special structs that can contain any
//! storable type and are marked with the `#[starknet::storage_node]` attribute.
//!
//! # Purpose and Benefits
//!
//! Storage nodes provide a flexible way to structure storage data by allowing non-sequential
//! storage layouts. They allow the creation of storage-only types, that can contain both
//! storage-specific types (like `Map` and `Vec`) and regular types - so long as these types are
//! storable.
//!
//! Storage nodes are particularly valuable when defining structs containing phantom types like
//! `Map` and `Vec`. When a struct is marked with `#[starknet::storage_node]`, it automatically
//! becomes a phantom type.
//!
//! While you can declare any struct as a storage node (even those without phantom types), doing so
//! will make that struct a phantom type. For structs that don't contain phantom types, it's often
//! more appropriate to make them storable using `#[derive(Store)]`. This alternative approach
//! still enables access to individual struct members through `SubPointers` without imposing the
//! phantom type behavior.
//!
//! The storage layout differs significantly between these two approaches:
//! * `#[derive(Store)]`: Members are stored continuously in the same variable space, with a limit
//! of 256 field elements.
//! * Storage node: Each member is stored at a different location. For a storage node member `m`
//! within a storage variable `variable_name`, the path to that member is computed as
//! `h(sn_keccak(variable_name), sn_keccak(m))`, where `h` is the Pedersen hash.
//!
//! # Examples
//!
//! Here's how to define a storage node:
//!
//! ```
//! #[starknet::storage_node]
//! struct MyStruct {
//! a: felt252,
//! b: Map<felt252, felt52>,
//! }
//! ```
//!
//! For the struct above, the following storage node struct and impl will be generated:
//!
//! ```
//! struct MyStructStorageNode {
//! a: PendingStoragePath<felt252>,
//! b: PendingStoragePath<Map<felt252, felt52>>,
//! }
//!
//! impl MyStructStorageNodeImpl of StorageNode<MyStruct> {
//! fn storage_node(self: StoragePath<MyStruct>) -> MyStructStorageNode {
//! MyStructStorageNode {
//! a: PendingStoragePathTrait::new(@self, selector!("a")),
//! b: PendingStoragePathTrait::new(@self, selector!("b")),
//! }
//! }
//! }
//! ```
//!
//! For a type `T` that implements `StorageNode` (e.g. `MyStruct` in the example above),
//! `Deref<StoragePath<T>>` is implemented as simply calling `storage_node`, and thus exposing the
//! members of the storage node (`a` and `b` in the example above).
//! For example, given the following storage:
//!
//! ```
//! #[storage]
//! struct Storage {
//! my_struct: MyStruct,
//! a: felt52,
//! }
//!
//! We can access the members of the storage node as follows:
//!
//! ```
//! fn use_storage(self: @ContractState) {
//! let a_value = self.a.read();
//! let inner_a_value = self.my_struct.a.read();
//! let b_value = self.my_struct.b.entry(42).read();
//! }
//! ```
//!
//! # Flattening Storage Nodes
//!
//! Storage Nodes members can be annotated with `#[flat]` to flatten the storage hierarchy and not
//! use the member name in the computation of the storage address for its fields.
//!
//! ```
//! #[storage]
//! struct Storage {
//! #[flat]
//! my_struct: MyStruct,
//! a: felt52,
//! }
//! ```
//!
//! When flattened, the storage node's field name (e.g., `my_struct`) doesn't affect storage address
//! computation. In the example above, both `self.a` and `self.my_struct.a` will point to the same
//! address. Use `#[flat]` with caution as this behavior is rarely intended.
//!
//! # Performance Considerations
//!
//! Storage node members are implemented as `PendingStoragePath` instances, enabling lazy evaluation
//! of storage paths. This means storage addresses are only computed for members that are actually
//! accessed.

use super::{Mutable, StoragePath};

/// A trait that given a storage path of a struct, generates the storage node of this struct.
///
/// The storage node is a struct that is used to structure the storage of a struct, while taking
/// into account this structure when computing the address of the struct members in the storage.
/// The trigger for creating a storage node is the `#[starknet::storage_node]` attribute.
///
/// Storage nodes are used in order to structure the storage of a struct, while not enforcing the
/// struct to be sequential in the storage. This is useful for structs that contains phantom types
/// such as `Map` and `Vec`. As a result, structs attributed with `#[starknet::storage_node]` are
/// also considered to be phantom types, although not explicitly annotated as such.
/// Structs which do not contain phantom types, can still be declared a storage node, and it will
/// make them a phantom type.
/// However, it may be preferable to simply make this struct storable (i.e. `#[derive(Store)]')
/// instead. This will still allow accessing individual members of the struct (see `SubPointers`),
/// but will not make the struct a phantom type.
///
/// For example, given the following struct:
/// ```
/// #[starknet::storage_node]
/// struct MyStruct {
/// a: felt252,
/// b: Map<felt252, felt52>,
/// }
/// ```
///
/// The following storage node struct and impl will be generated:
/// ```
/// struct MyStructStorageNode {
/// a: PendingStoragePath<felt252>,
/// b: PendingStoragePath<Map<felt252, felt52>>,
/// }
///
/// impl MyStructStorageNodeImpl of StorageNode<MyStruct> {
/// fn storage_node(self: StoragePath<MyStruct>) -> MyStructStorageNode {
/// MyStructStorageNode {
/// a: PendingStoragePathTrait::new(@self, selector!("a")),
/// b: PendingStoragePathTrait::new(@self, selector!("b")),
/// }
/// }
/// }
/// ```
/// For a type `T` that implement `StorageNode` (e.g. `MyStruct` in the example above),
/// `Deref<StoragePath<T>>` is implemented as simply calling `storage_node`, and thus exposing the
/// members of the storage node (`a` and `b` in the example above).
/// For example, given the following storage:
/// ```
/// #[storage]
/// struct Storage {
/// my_struct: MyStruct,
/// a: felt52,
/// }
///
/// We can access the members of the storage node as follows:
/// ```
/// fn use_storage(self: @ContractState) {
/// let a_value = self.a.read();
/// let inner_a_value = self.my_struct.a.read();
/// let b_value = self.my_struct.b.entry(42).read();
/// }
/// ```
///
/// If a member is annotated with `#[flat]`, the storage node will be flattened, and the
/// member name (i.e. `my_struct`) will not affect the address of the storage object.
/// In the storage example above, it will look like:
/// ```
/// #[storage]
/// struct Storage {
/// #[flat]
/// my_struct: MyStruct,
/// a: felt52,
/// }
///
/// In this case, the storage node will be flattened, and both `self.a` and `self.my_struct.a` will
/// point to the same address. This behavior is rarely intended, and thus `#[flat]` should be used
/// with caution.
///
/// Notice that the members of the storage node are `PendingStoragePath` instances, which are used
/// to lazily get the updated storage path of the struct members, in this way only members that are
/// accessed are actually evaluated.
///
pub trait StorageNode<T> {
type NodeType;
fn storage_node(self: StoragePath<T>) -> Self::NodeType;
Expand Down
Loading