Skip to content

Commit

Permalink
Core Lib Documentation: storage_node module (#7043)
Browse files Browse the repository at this point in the history
Co-authored-by: enitrat <msaug@protonmail.com>
  • Loading branch information
TAdev0 and enitrat authored Jan 11, 2025
1 parent dff35c0 commit a2dd9e0
Showing 1 changed file with 104 additions and 80 deletions.
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

0 comments on commit a2dd9e0

Please sign in to comment.