From a2dd9e07a5eef844573c16748c2ffb0bea56b975 Mon Sep 17 00:00:00 2001 From: Tristan <122918260+TAdev0@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:54:33 +0100 Subject: [PATCH] Core Lib Documentation: `storage_node` module (#7043) Co-authored-by: enitrat --- .../src/starknet/storage/storage_node.cairo | 184 ++++++++++-------- 1 file changed, 104 insertions(+), 80 deletions(-) diff --git a/corelib/src/starknet/storage/storage_node.cairo b/corelib/src/starknet/storage/storage_node.cairo index 78caa727b03..190a7ff0bca 100644 --- a/corelib/src/starknet/storage/storage_node.cairo +++ b/corelib/src/starknet/storage/storage_node.cairo @@ -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, +//! } +//! ``` +//! +//! For the struct above, the following storage node struct and impl will be generated: +//! +//! ``` +//! struct MyStructStorageNode { +//! a: PendingStoragePath, +//! b: PendingStoragePath>, +//! } +//! +//! impl MyStructStorageNodeImpl of StorageNode { +//! fn storage_node(self: StoragePath) -> 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>` 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, -/// } -/// ``` -/// -/// The following storage node struct and impl will be generated: -/// ``` -/// struct MyStructStorageNode { -/// a: PendingStoragePath, -/// b: PendingStoragePath>, -/// } -/// -/// impl MyStructStorageNodeImpl of StorageNode { -/// fn storage_node(self: StoragePath) -> 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>` 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 { type NodeType; fn storage_node(self: StoragePath) -> Self::NodeType;