Cairo v2.7.0 is coming!

TL;DR

Cairo 2.7.0 will be released soon! At the moment, 2.7.0-rc.3 is available and can be tested by the community. This version involves a Sierra upgrade to v1.6.0, which means contracts written with v2.7.0 are only deployable on Starknet ≥ 0.13.2. The Testnet upgrade is planned for Aug 5, and the Mainnet upgrade is planned for Aug 26 (you can find more details here). As usual, you can continue using older compiler versions for your contracts and deploy them on Starknet.

v2.7.0 is a HUGE version with an abundance of new features & updates:

The above only includes the most significant new features, for a comprehensive list of changes see the release notes. We now proceed to dive into each of the changes mentioned above.

Breaking changes

While we’re trying to avoid breaking any existing code in minor versions, we may allow changing internal traits or generated code that could affect existing user code. While not affecting the majority of contracts, we cover the list of potential breaking changes below:

  • The compiler generated <storage_var_name>ContractMemberStateTrait traits, where storage_var_name is a name of a property of the Storage struct, no longer exist. If your code depended directly on one of these traits, then it will break.
  • The return type of contract_state_for_testing and component_state_for_testing will not have access to the storage members. For example:
let state = MyContract::contract_state_for_testing();
let value = state.storage_var.read(); // won't compile

To fix this, add mut or @ as follows:

let state = @MyContract::contract_state_for_testing();
state.storage_var.read(...);
let mut state = MyContract::contract_state_for_testing();
state.storage_var.write(...);

The 2024_07 edition

This new edition no longer auto adds pub modifiers to the Storage sturct and its members, this is now the responsibility of the developer. This is of particular importance for components, whose Storage struct is referred to by external contracts.

We’re also narrowing the prelude, removing several traits which are not common to every contract. For a full diff, you can compare the old and new preule (the old prelude is the same for 2023_10 and 2023_11): v2023_10.cairo, v2024_07.cairo. Note that in particular, all storage access traits were added to 2023_10 and 2023_11, but need to be imported with 2024_07.

Deref

This version adds the Deref and DerefMut traits to the compiler. Similarly to Rust, these are special traits known to the compiler:

pub trait Deref<T> {
    type Target;
    fn deref(self: T) -> Self::Target;
}

pub trait DerefMut<T> {
    type Target;
    fn deref_mut(ref self: T) -> Self::Target;
}

When type T can be dereferenced to type K, then we can access K’s members while holding an instance of T. Note that any time has at most one deref implementation. Additionally, for now only member access via deref is supported, i.e. impls with functions whose self argument is of type K will not be applicable when holding an instance of T. To illustrate how Deref works, consider the following example:

#[derive(Drop)]
struct Type1 {
    a: u8
}

#[derive(Drop)]
struct Type2 {
    b: u8
}

impl DerefType1ToType2 of Deref<Type1> {
    type Target = Type2;
    fn deref(self: Type1) -> Type2 {
        Type2 { b: self.a }
    }
}

Thanks to the above Deref implementation, we can write the following:

let t1 = Type1 { a: 3 };
let b = t1.b;

Note that for the following to work, we need an implementation of DerefMut rather than Deref:

fn foo(ref t1: Type1) {
    let b = t1.b;
}

impl DerefType1ToType2 of DerefMut<Type1> {
    type Target = Type2;
    fn deref_mut(ref self: Type1) -> Type2 {
        Type2 { b: self.a }
    }
}

Contract Storage

Up until now the storage mechanism solely relied on the Store trait. If a type T had a Store<T> implementation, then Storage could contain members of type T (the only exception being components, where the member type was the component’s Storage type). For example:

#[derive(starknet::Store)]
struct MyStruct {
    a: u128,
    b: u128
}

#[storage]
struct Storage {
    member: MyStruct
}

When reading or writing to member, it was only possible to read or write the struct in its entirety, i.e. via self.member.read(). In v2.7.0, this mechanism is generalized.

To allow this generalization, much of the code generation around contract storage was refactored. In particular, the ContractMemberState traits that were generated per each member of the Storage struct no longer exist. That is, if your code uses these generated traits explicitly, it will break. The role of these traits is now fulfilled by the ContractStorageBase and ContractStorageBaseMut generated structs. To understand those in more detail, read the “under the hood” sections below.

Individual member access

Consider the above contract; in Cairo v2.7.0 onwards, the following syntax is supported:

fn storage_access(ref self: ContractState) {
    let member: MyStruct = self.member.read() // old
    let a = self.member.a.read(); // new
    self.member.b.write(5); // new
}

Note that this is only supported for types that explicitly derive Store, and won’t work for custom Store implementation (e.g. ones that are obtained by packing).

Individual member access - under the hood

In the new version, derive(Store) will generate, in addition to a Store impl, a corresponding SubPointers type and an implementation of the SubPointers trait for this type. For our above example, the following code will be generated:

#[derive(Drop, Copy)]
struct MyStructSubPointers {
    a: starknet::storage::StoragePointer<u128>,
    b: starknet::storage::StoragePointer<u128>,
}

struct MutableMyStructSubPointers {
    a: starknet::storage::StoragePointer<Mutable<u128>>,
    b: starknet::storage::StoragePointer<Mutable<u128>>,
}

impl MyStructSubPointersImpl of starknet::storage::SubPointers<MyStruct> {
   type SubPointersType = MyStructSubPointers;

   fn sub_pointers(self: starknet::storage::StoragePointer<MyStruct>) -> MyStructSubPointers {
        let base_address = self.address;
        let mut current_offset = self.offset;
        let a_value = starknet::storage::StoragePointer {
            address: base_address,
            offset: current_offset,
        };
        current_offset = current_offset + starknet::Store::<u128>::size();
        let b_value = starknet::storage::StoragePointer {
            address: base_address,
            offset: current_offset,
        };
        MyStructSubPointers {
           a: a_value,
           b: b_value,
        }
    }
}

impl MutableMyStructSubPointersImpl of starknet::storage::MutableSubPointers<MyStruct> {
   type SubPointersType = MutableMyStructSubPointers;

   fn mutable_sub_pointers(self: starknet::storage::StoragePointer<Mutable<MyStruct>>) -> MutableMyStructSubPointers {
        let base_address = self.address;
        let mut current_offset = self.offset;
        let a_value = starknet::storage::StoragePointer {
            address: base_address,
            offset: current_offset,
        };
        current_offset = current_offset + starknet::Store::<u128>::size();
        let b_value = starknet::storage::StoragePointer {
            address: base_address,
            offset: current_offset,
        };
        MutableMyStructSubPointers {
           a: a_value,
           b: b_value,
        }
    }
}

StoragePointer is a new type defined in storage.cairo, it points to a concrete storage slot via a base address and an offset. The above generated code explains why only structs that explicitly derive Store allow individual member access. The generated code assumes that each member is stored via its type’s Store implementation, which is not necessarily the case for custom implementations, e.g. ones based on packing.

To understand how to get from self:ContractState to self.member.a.read(), we need to look into the types in question. The ContractState type is an empty struct generated by the compiler, that can be derefed to the ContractStorageBase or ContractStorageBaseMut types that are generated based on the Storage struct defined in the contract module:

#[derive(Drop, Copy)]
struct ContractStorageBase {
    member: starknet::storage::StorageBase<MyStruct>,
}

#[derive(Drop, Copy)]
struct ContractStorageBaseMut {
    member: starknet::storage::StorageBase<starknet::storage::Mutable<MyStruct>>,
}

pub struct ContractState {}

impl StorageBaseImpl of starknet::storage::StorageBaseTrait<ContractState> {
    type BaseType = ContractStorageBase;
    type BaseMutType = ContractStorageBaseMut;

    fn storage_base(self: @ContractState) -> ContractStorageBase {
        ContractStorageBase {
           member: starknet::storage::StorageBase{ address: selector!("balance") },
        }
    }
    fn storage_base_mut(ref self: ContractState) -> ContractStorageBaseMut {
        ContractStorageBaseMut {
           member: starknet::storage::StorageBase{ address: selector!("balance") },
        }
    }
}
 
impl ContractStateDeref of core::ops::SnapshotDeref<ContractState> {
    type Target = ContractStorageBase;
    fn snapshot_deref(self: @ContractState) -> ContractStorageBase {
        self.storage_base()
    }
}

impl ContractStateDerefMut of core::ops::DerefMut<ContractState> {
    type Target = ContractStorageBaseMut;
    fn deref_mut(ref self: ContractState) -> ContractStorageBaseMut {
        self.storage_base_mut()
    }
}

The above generated code shows us how self.member works, via the DerefMut implementation between ref ContractState and ContractStorageBaseMut that has member of type StorageBase<Mutable<MyStruct>>. To see how we can get from StorageBase<Mutable<MyStruct>> to MyStruct’s members, we need to follow the chain of derefs defined in storage.cairo:

// StorageBase<Mutable<MyStruct>> --> StoragePath<Mutable<MyStruct>>
impl StorageBaseDeref<T> of core::ops::Deref<StorageBase<T>> {
    type Target = StoragePath<T>;
    fn deref(self: StorageBase<T>) -> Self::Target {
        self.as_path()
    }
}

StorageBase represents the initial address of some type in storage. StoragePath is a struct that maintains a hash state that can be modified until we reach the storage location we need, and was devised mostly for Map accesses (or more complex storage accesses, such as storage nodes that we’ll see in subsequent sections).

We can now convert the StoragePath to a storage pointer by finalizing the hash state:

// StoragePath<Mutable<MyStruct>> --> StoragePointer0Offset<Mutable<MyStruct>>
impl StoragePathDeref<
    T, impl PointerImpl: StorageAsPointer<StoragePath<T>>
> of core::ops::Deref<StoragePath<T>> {
    type Target = StoragePointer0Offset<PointerImpl::Value>;
    fn deref(self: StoragePath<T>) -> StoragePointer0Offset<PointerImpl::Value> {
        self.as_ptr()
    }
}

// T is Mutable<MyStruct>, this impl gives us `as_ptr` that is used above
impl MutableStorableStoragePathAsPointer<
    T, +MutableTrait<T>, +starknet::Store<MutableTrait::<T>::InnerType>
> of StorageAsPointer<StoragePath<T>> {
    type Value = T;
    fn as_ptr(self: @StoragePath<T>) -> StoragePointer0Offset<T> {
        StoragePointer0Offset { address: (*self).finalize() }
    }
}

// StoragePointer0Offset<Mutable<MyStruct>> --> StoragePointer<Mutable<MyStruct>>
impl StoragePointer0OffsetDeref<T> of core::ops::Deref<StoragePointer0Offset<T>> {
    type Target = StoragePointer<T>;
    fn deref(self: StoragePointer0Offset<T>) -> StoragePointer<T> {
        StoragePointer::<T> { address: self.address, offset: 0 }
    }
}

pub struct Mutable<T> {}

trait MutableTrait<T> {
    type InnerType;
}

impl MutableImpl<T> of MutableTrait<Mutable<T>> {
    type InnerType = T;
}

The above derefs brought us to StoragePointer<Mutable<MyStruct>>. This is where the new sub pointers types kick in, via the following two derefs in storage.cairo:

// StoragePointer<MyStruct> -->  MyStructSubPointers
impl SubPointersDeref<T, +SubPointers<T>> of core::ops::Deref<StoragePointer<T>> {
    type Target = SubPointers::<T>::SubPointersType;
    fn deref(self: StoragePointer<T>) -> Self::Target {
        self.sub_pointers()
    }
}

// StoragePointer<Mutable<MyStruct> --> MutableMyStructSubPointers
impl MutableSubPointersDeref<
    T, +MutableSubPointers<T>
> of core::ops::Deref<StoragePointer<Mutable<T>>> {
    type Target = MutableSubPointers::<T>::SubPointersType;
    fn deref(self: StoragePointer<Mutable<T>>) -> Self::Target {
        self.mutable_sub_pointers()
    }
}

Now that we hold the MutableMyStructSubPointers type, to see why self.member.a works, we only need a reminder of the sub pointers type definition:

struct MutableMyStructSubPointers {
    a: starknet::storage::StoragePointer<Mutable<u128>>,
    b: starknet::storage::StoragePointer<Mutable<u128>>,
}

Finally, self.a.read() works since StoragePointer<T> exposes read, and self.a.write() works since StoragePointer<Mutable<T>> exposes write:

// Store-based read for StoragePointer<T>
pub impl StorableStoragePointerReadAccess<
    T, +starknet::Store<T>
> of StoragePointerReadAccess<StoragePointer<T>> {
    type Value = T;
    fn read(self: @StoragePointer<T>) -> T {
        starknet::SyscallResultTrait::unwrap_syscall(
            starknet::Store::<T>::read_at_offset(0, *self.address, *self.offset)
        )
    }
}

// When T is `Mutable<K>`, we need a separate "read" implementation in order to reach the internal type
impl MutableStorableStoragePointerReadAccess<
    T, +MutableTrait<T>, +starknet::Store<MutableTrait::<T>::InnerType>
> of StoragePointerReadAccess<StoragePointer<T>> {
    type Value = MutableTrait::<T>::InnerType;
    fn read(self: @StoragePointer<T>) -> MutableTrait::<T>::InnerType {
        starknet::SyscallResultTrait::unwrap_syscall(
            starknet::Store::<
                MutableTrait::<T>::InnerType
            >::read_at_offset(0, *self.address, *self.offset)
        )
    }
}

// Write implementation for StoragePointer<T> where T is Mutable<K>
impl MutableStorableStoragePointerWriteAccess<
    T, +MutableTrait<T>, +starknet::Store<MutableTrait::<T>::InnerType>
> of StoragePointerWriteAccess<StoragePointer<T>> {
    type Value = MutableTrait::<T>::InnerType;
    fn write(self: StoragePointer<T>, value: MutableTrait::<T>::InnerType) {
        starknet::SyscallResultTrait::unwrap_syscall(
            starknet::Store::<
                MutableTrait::<T>::InnerType
            >::write_at_offset(0, self.address, self.offset, value)
        )
    }
}

To summarize, we followed the path:

ref ContractState → (deref) → ContractStorageBaseMut → (self.member) → StorageBase<Mutable<MyStruct>> → (deref) → StoragePath<Mutable<MyStruct>> → (deref) → StoragePointer0Offset<Mutable<MyStruct>> → (deref) → StoragePointer<Mutable<MyStruct>> → (deref) → MutableMyStructSubPointers.

Maps

Say goodbye to LegacyMap. Map is a new and more flexible type for maintaining mappings in a contract storage. With Map<K,V> we can:

  • Have nested Maps (rather than having a tuple type key with LegacyMap)
  • Have Map as a member of a struct (we will dive into this in the following storage_node section)

Note that the storage layout of Map<K, V> is identical to that LegacyMap<K,V>, hence you can safely migrate to the new type.

The Map type can not be instantiated via user code.

To illustrate the use of the new type, consider the following simple contract:

#[starknet::contract]
mod simple_contract {
    use starknet::{ContractAddress, contract_address_const};
    use starknet::storage::{Map, StoragePathEntry};
    /// the latest edition 2024_07 also requires the following imports:
    use starknet::storage::{StorageMapReadAccess, StorageMapWriteAccess}

    #[storage]
    struct Storage {
        basic: u128,
        balances: Map<ContractAddress, u256>,
        nested_balances: Map<ContractAddress, Map<ContractAddress, u256>>,
    }

    #[abi(per_item)]
    #[generate_trait]
    pub impl SimpleContract of SimpleContractTrait {
        #[external(v0)]
        fn test_new_storage(ref self: ContractState) {
            let address = contract_address_const::<1>();
            self.balances.entry(address).read();
            self.nested_balances.entry(address).entry(address).read();   
            self.nested_balances.entry(address).entry(address).write(5);
        }
    }
}

Maps - under the hood

The basic types that the new maps require are Map and EntryInfo. The purpose of these types is for us to be able to refer to the key type and value type from a Map instance, as illustrated by the following code in the corelib:

#[phantom]
pub struct Map<K, V> {}

/// A trait for making a map like type support implement the `StoragePathEntry` trait.
trait EntryInfo<T> {
    type Key;
    type Value;
}

impl EntryInfoImpl<K, V> of EntryInfo<Map<K, V>> {
    type Key = K;
    type Value = V;
}

Accessing entires of a collection C is done via implementing the following trait:

pub trait StoragePathEntry<C> {
    type Key;
    type Value;
    fn entry(self: C, key: Self::Key) -> StoragePath<Self::Value>;
}

Now we can proceed to show how the code from our previous example works:

self.balances.entry(address).read()

We already know, from diving into the implementation of individual members access, that ContractState can be dereferenced to ContractStorageBase, where balances is of type StorageBase<Map<ContractAddress, u256>, which can then be dereferenced StoragePath<Map<ContractAddress, u256>> (to get write access, we would need to start from ref ContractState, which can be derefed to ContractStorageBaseMut, which in turn has a balances member of type StoragePath<Mutable<Map<ContractAddress, u256>>>).

From StoragePath<Map<ContractAddress, u256>> or StoragePath<Mutable<Map<ContractAddress, u256>>> we get the entry functionality via one of the following implementations:

impl EntryInfoStoragePathEntry<
    T, 
    +EntryInfo<T>, 
    +core::hash::Hash<EntryInfo::<T>::Key, 
     StoragePathHashState>
> of StoragePathEntry<StoragePath<T>> {
    type Key = EntryInfo::<T>::Key;
    type Value = EntryInfo::<T>::Value;
    fn entry(self: StoragePath<T>, key: EntryInfo::<T>::Key) -> StoragePath<EntryInfo::<T>::Value> {
        StoragePath::<
            EntryInfo::<T>::Value
        > {
            hash_state: core::hash::Hash::<
                EntryInfo::<T>::Key, StoragePathHashState
            >::update_state(self.hash_state, key)
        }
    }
}

impl MutableEntryStoragePathEntry<
    T,
    +MutableTrait<T>,
    impl EntryImpl: EntryInfo<MutableTrait::<T>::InnerType>,
    +core::hash::Hash<EntryImpl::Key,
    StoragePathHashState>
> of StoragePathEntry<StoragePath<T>> {
    type Key = EntryImpl::Key;
    type Value = Mutable<EntryImpl::Value>;
    fn entry(self: StoragePath<T>, key: EntryImpl::Key) -> StoragePath<Mutable<EntryImpl::Value>> {
        StoragePath::<
            Mutable<EntryImpl::Value>
        > {
            hash_state: core::hash::Hash::<
                EntryImpl::Key, StoragePathHashState
            >::update_state(self.hash_state, key)
        }
    }
}

Above, we can see that entry modifies the StoragePath with a single hash application (specifically, Pedersen hash, as StoragePathHashState is just an alias to PedersenHashState). After calling entry, we end up with StoragePath<EntryInfo::<T>::Value> or StoragePath<Mutable<EntryImpl::Value>>. Note that Value can either implement Store, or itself be another Map, over which we can again call entry. If Value implements Store, then read access will be give by one of the following impls:

impl StorableEntryReadAccess<
    T,
    +EntryInfo<T>,
    +core::hash::Hash<EntryInfo::<T>::Key,StoragePathHashState>,
    +starknet::Store<EntryInfo::<T>::Value>,
> of StorageMapReadAccessTrait<StoragePath<T>> {
    type Key = EntryInfo::<T>::Key;
    type Value = EntryInfo::<T>::Value;
    fn read(self: StoragePath<T>, key: EntryInfo::<T>::Key) -> EntryInfo::<T>::Value {
        self.entry(key).as_ptr().read()
    }
}

impl MutableStorableEntryReadAccess<
    T,
    +MutableTrait<T>,
    +EntryInfo<MutableTrait::<T>::InnerType>,
    +core::hash::Hash<EntryInfo::<MutableTrait::<T>::InnerType>::Key, StoragePathHashState>,
    +starknet::Store<EntryInfo::<MutableTrait::<T>::InnerType>::Value>,
> of StorageMapReadAccessTrait<StoragePath<T>> {
    type Key = EntryInfo::<MutableTrait::<T>::InnerType>::Key;
    type Value = EntryInfo::<MutableTrait::<T>::InnerType>::Value;
    #[inline(always)]
    fn read(
        self: StoragePath<T>, key: EntryInfo::<MutableTrait::<T>::InnerType>::Key
    ) -> EntryInfo::<MutableTrait::<T>::InnerType>::Value {
        self.entry(key).as_ptr().read()
    }
}

A similar impl will give us write capabilities with the type StoragePath<Mutable<EntryImpl::Value>>.

Storage vecs

So far, to have sequential access to a collection in storage, one needed to use maps or a custom implementation, e.g., as done by OpenZeppelin. This version introduces the Vec type, which allows keeping vectors in a contract’s storage.

Below is an example of how one can use storage vectors:

use starknet::storage::{Vec, VecTrait, MutableVecTrait};

#[storage]
struct Storage {
    var1: Vec<u256>,
    var2: Vec<Map<u8, Vec<u8>>>
}

fn foo(ref self: ContractState) {
  self.var1.append().write(1);
  self.var1.append().write(2);
  assert!(self.var1.len() == 2);
  assert!(self.var1[0].read() == 1 && self.var1[1].read() == 2);
  self.var1[0].write(4);
  self.var2.append().entry(5).append().write(1);
  assert!(self.var2[0].entry(5)[0].read() == 1);
  self.var1[3].read(); // panic (out of bounds)
}

Storage vecs - under the hood

The VecTrait and MutableVecTrait traits give us the ability to access vec indices and append new elements. These impls operate on StoragePath<Vec<T>> and StoragePath<Mutable<Vec<T>>>, which are the member types of the generated ContractStorageBase and ContractStorageBaseMut types, as we’ve seen in “individual storage access - under the hood”.

pub struct Vec<T> {}

pub trait VecTrait<T> {
    type ElementType;
    fn at(self: T, index: u64) -> StoragePath<Self::ElementType>;
    fn len(self: T) -> u64;
}

pub trait MutableVecTrait<T> {
    type ElementType;
    fn at(self: T, index: u64) -> StoragePath<Mutable<Self::ElementType>>;
    fn len(self: T) -> u64;
    fn append(self: T) -> StoragePath<Mutable<Self::ElementType>>;
}

impl VecImpl<T> of VecTrait<StoragePath<Vec<T>>> {
    type ElementType = T;
    fn get(self: StoragePath<Vec<T>>, index: u64) -> Option<StoragePath<T>> {
        let vec_len = self.len();
        if index < vec_len {
            Option::Some(self.update(index))
        } else {
            Option::None
        }
    }
    fn at(self: StoragePath<Vec<T>>, index: u64) -> StoragePath<T> {
        assert!(index < self.len(), "Index out of bounds");
        self.update(index)
    }
    fn len(self: StoragePath<Vec<T>>) -> u64 {
        self.as_ptr().read()
    }
}

impl MutableVecImpl<T> of MutableVecTrait<StoragePath<Mutable<Vec<T>>>> {
    type ElementType = T;
    fn get(self: StoragePath<Mutable<Vec<T>>>, index: u64) -> Option<StoragePath<Mutable<T>>> {
        let vec_len = self.len();
        if index < vec_len {
            Option::Some(self.update(index))
        } else {
            Option::None
        }
    }
    fn at(self: StoragePath<Mutable<Vec<T>>>, index: u64) -> StoragePath<Mutable<T>> {
        assert!(index < self.len(), "Index out of bounds");
        self.update(index)
    }
    fn len(self: StoragePath<Mutable<Vec<T>>>) -> u64 {
        self.as_ptr().read()
    }
    fn append(self: StoragePath<Mutable<Vec<T>>>) -> StoragePath<Mutable<T>> {
        let vec_len = self.len();
        self.as_ptr().write(vec_len + 1);
        self.update(vec_len)
    }
}

Note that the storage layout of Vec is determined by the get implementation, which computes the address of the n’th element as h(base_address, n), where h is the Pedersen hash function. The length of the array is held at base_address, where the base address is determined by the location of the array with respect to the Storage struct. For example, in the case of var1 above, the base address is sn_keccak("var1"), while the base address of self.var2[0].entry(5).read() is pedersen(sn_keccak("var2"), 5).

Storage nodes

Storage nodes are a new concept introduced in Cairo v2.7.0. The motivation behind them was to have Map, or other “special” types that do not implement Store but can appear inside Storage, as potential members of structs.

You can think of storage nodes as nodes in a tree that lie within the contract storage space. Content is stored in the leaves, and intermediate nodes allow you to point to other locations in storage. Consider the following example:

struct Storage {
    member: MyNode
}

#[starknet::storage_node]
struct MyNode {
    a: u8,
    b: u256,
    c: Map<u256, u256>
}

fn foo(self: @ContractState) {
   self.member.c.entry(5).read();
}

Thanks to the #[starknet::storage_node] attribute over MyNode, we can have storage variables of type MyNode, although it doesn’t implement Store.

The storage layout of MyNode will be based on the names of its members: a, b, and c. For example, the actual storage key read by self.member.c.entry(5).read() is:

h(sn_keccak("member"), h(sn_keccak("c"), 5)),

where h is the Pedersen hash function.

Maps in structs are not the only thing that you can do with storage nodes. To illustrate their strength, consider the following example:

struct Storage {
    root: StorageTreeNode
}

#[starknet::storage_node]
struct StorageTreeNode {
    value: u32,
    left: StorageTreeNode,
    right: StorageTreeNode
}

Without the #[starknet::storage_node] attribute, the above code would not have compiled as StorageTreeNode has infinite size. The #[starknet::storage_node] attribute tells the compiler that StorageTreeNode will never be instantiated directly and will only serve to point to storage locations.

Note that to make the above example interesting, you would need another property on StorageTreeNode which tells you whether or not you’re at a leaf (or, e.g., change value to Option<u32>, where None indicates that you hit a leaf). It’s important to emphasize that a storage node does not carry value by itself, it is essentially a path to some storage location from which I can read members. Hence, I cannot ask if self.root.left is None, it is a path to some location in storage, I can always point to it. Any actual information should be kept in storeable members of the storage node.

Storage nodes - under the hood

Throughout this section, we’ll use the following storage node as our example:

#[storage]
struct Storage {
    member: MyNode
}

#[starknet::storage_node]
struct MyNode {
    a: u8,
    b: Map<u256, u256>,
    c: MyNode
}

fn foo(self: @ContractState) {
   self.member.a.read();
   self.member.b.entry(5).read();
   self.c.c.a.read();
}

When trying to work out self.member.a.read(), without the new #[storage_node] attribute, trying to follow the path from “individual member access - under the hood”, we will break when trying to deref from
StoragePath<MyNode> to StoragePointer0Offset<MyNode>, since the deref implementation needs an impl of the StorageAsPointer trait, and the following generic implementation in the corelib won’t help since we don’t have Store:

pub trait StorageAsPointer<TMemberState> {
    type Value;
    fn as_ptr(self: @TMemberState) -> StoragePointer0Offset<Self::Value>;
}

impl StorableStoragePathAsPointer<T, +starknet::Store<T>> of StorageAsPointer<StoragePath<T>> {
    type Value = T;
    fn as_ptr(self: @StoragePath<T>) -> StoragePointer0Offset<T> {
        StoragePointer0Offset { address: (*self).finalize() }
    }
}

Thus, we need a new way to get read from StoragePath<MyNode> without relying on Store (we will ignore write in this section since it is a repetition of the same Mutable<T> trick we’ve seen in the previous sections).

When annotating MyNode with the #[starknet::storage_node] attribute, the following code is generated:

#[derive(Drop, Copy)]
struct MyNodeStorageNode {
    a: starknet::storage::PendingStoragePath<u8>,
    b: starknet::storage::PendingStoragePath<Map<u256, u256>>,
    c: starknet::storage::PendingStoragePath<MyNode>,
}

impl MyNodeStorageNodeImpl of StorageNode<MyNode> {
   
   type NodeType = BalancePairStorageNode;

   fn storage_node(self: StoragePath<MyNode>) -> MyNodeStorageNode {
        let a_value = PendingStoragePathTrait::new(
                        @self,
                        selector!("a")
                    );
        let b_value = PendingStoragePathTrait::new(
                        @self,
                        selector!("b")
                    );
        let c_value = PendingStoragePathTrait::new(
                        @self,
                        selector!("c")
                    );
        MyNodeStorageNode {
           a: a_value,
           b: b_value,
           c: c_value
        }
    }
}

The above is similar to the generated sub pointers type, except now we need to encode MyNode member names in the storage path somehow. To this end, the PendingStoragePath type was introduced:

struct PendingStoragePath<T> {
    hash_state: StoragePathHashState, // PedersenHashState
    pending_key: felt252
}

Now, the following deref implementation in the corelib completes the picture:

impl StorageNodeDeref<T, +StorageNode<T>> of core::ops::Deref<StoragePath<T>> {
    type Target = StorageNode::<T>::NodeType;
    fn deref(self: StoragePath<T>) -> Self::Target {
        self.storage_node()
    }
}

This is what gets us from StoragePath<MyNode> to MyNodeStorageNode, a struct whose members are a, b, and c (same as MyNode), but with the member types being wrapped by PendingStoragePath.

The read functionality on PendingStoragePath<u8> comes from the following impls in the corelib:

// PendingStoragePath --> StoragePath
impl PendingStoragePathAsPath<T> of StorageAsPath<PendingStoragePath<T>> {
    type Value = T;
    fn as_path(self: @PendingStoragePath<T>) -> StoragePath<T> {
        StoragePath::<
            T
        > { hash_state: core::hash::HashStateTrait::update(*self.hash_state, *self.pending_key) }
    }
}

// T is PendingStoragePath<u8>
impl StorablePathableStorageAsPointer<
    T,
    impl PathImpl: StorageAsPath<T>,
    // PathImpl::Value is u8, the StorageAsPointer<u8>  impl is given by `StorableStoragePathAsPointer`
    impl PtrImpl: StorageAsPointer<StoragePath<PathImpl::Value>>,
> of StorageAsPointer<T> {
    type Value = PtrImpl::Value;
    fn as_ptr(self: @T) -> StoragePointer0Offset<PtrImpl::Value> {
        let path = self.as_path();
        path.as_ptr()
    }
}

// This impl grants us `self.member.a.read()`
impl StorablePointerReadAccessImpl<
    T,
    impl PointerImpl: StorageAsPointer<T>,
    impl AccessImpl: StoragePointerReadAccess<StoragePointer0Offset<PointerImpl::Value>>,
    +Drop<T>,
    +Drop<AccessImpl::Value>,
> of StoragePointerReadAccess<T> {
    type Value = AccessImpl::Value;
    fn read(self: @T) -> Self::Value {
        self.as_ptr().read()
    }
}

The above shows us how can we get access to “storable” members of the storage node. But what about self.member.b.entry(5).read() or self.member.c.c.a.read().

To get access to the map member via

self.member.b.entry(5).read()

we need the following two impls:

// T is PendingStoragePath<Map<u256, u256>>`
impl PathableStorageEntryImpl<
    T,
    impl PathImpl: StorageAsPath<T>,
    impl EntryImpl: StoragePathEntry<StoragePath<PathImpl::Value>>,
    +Drop<T>,
    +Drop<EntryImpl::Key>,
> of StoragePathEntry<T> {
    type Key = EntryImpl::Key;
    type Value = EntryImpl::Value;
    fn entry(self: T, key: Self::Key) -> StoragePath<Self::Value> {
        let path = PathImpl::as_path(@self);
        EntryImpl::entry(path, key)
    }
}

// T is `StoragePath<Map<u256, u256>>`
impl StorageAsPathReadForward<
    T,
    impl PathImpl: StorageAsPath<T>,
    impl AccessImpl: StorageMapReadAccessTrait<StoragePath<PathImpl::Value>>,
    +Drop<T>,
    +Drop<AccessImpl::Key>,
> of StorageMapReadAccessTrait<T> {
    type Key = AccessImpl::Key;
    type Value = AccessImpl::Value;
    #[inline(always)]
    fn read(self: T, key: AccessImpl::Key) -> AccessImpl::Value {
        self.as_path().read(key)
    }
}

To keep reading the next storage node via e.g.

self.member.c.c.a.read()

we need to go back from PendingStoragePath<MyNode> back to StoragePath<MyNode>, from which we can again call the storage_node() function and obtain the PendingStoragePath<MyNode> type. This is achieve by the following deref impl:

impl PendingStoragePathDeref<T> of core::ops::Deref<PendingStoragePath<T>> {
    type Target = StoragePath<T>;
    fn deref(self: PendingStoragePath<T>) -> Self::Target {
        self.as_path()
    }
}

New storage design - examples

The new storage primitives that were discussed in the previous sections open new possibilities. We cover a few examples below.

+= for Storage variables

A common storage access pattern is reading a value, adding/subtracting, and then writing the new value. With StoragePath<T> we can get the += functionality for every storage variable whose type has an AddAsign implementation as follows:

#[starknet::contract]
mod numeric_contract {
    use core::ops::AddAssign;

    pub impl AddAsignStorage<
        Lhs, Rhs, +Drop<Lhs>, +Drop<Rhs>, 
        +AddAssign<Lhs, Rhs>,
        +Store<Lhs>
    > of AddAssign<StorageBase<Mutable<Lhs>>, Rhs> {
        fn add_assign(ref self: StorageBase<Mutable<Lhs>>, rhs: Rhs) {
            let mut value: Lhs = self.read();
            value += rhs;
            self.write(value);
        }
    }

    #[storage]
    struct Storage {
        numeric: u128,
    }

    #[abi(per_item)]
    #[generate_trait]
    pub impl NumericContract of NumericContractTrait {
        #[external(v0)]
        fn inc(ref self: ContractState) {
            let mut num = self.numeric;
            num += 1;        
        }
    }
}

By implementing AddAsign for StorageBase<Lhs> we were able to get += for storage variables. Do note that this is not always desirable, since += might hide expensive operations (storage write).

Note that the above will not work for structs members, maps, and storage nodes, as those are not of type StorageBase<T>. To extend += to all uints anywhere in storage, we need our impl to work on StoragePath, or more specifically anything that implements the StorageAsPath trait. This is illustrated below:

pub impl AddAsignStoragePath<
    Lhs, Rhs, +Drop<Lhs>, +Drop<Rhs>, 
    +AddAssign<Lhs, Rhs>,
    +Store<Lhs>
> of AddAssign<StoragePath<Mutable<Lhs>>, Rhs> {
    fn add_assign(ref self: StoragePath<Mutable<Lhs>>, rhs: Rhs) {
        let mut value: Lhs = self.read();
        value += rhs;
        self.write(value);
    }
}

// Lhs is StorageBase<Mutable<u8>>
// LhsAsPath's value is Mutable<u8>
pub impl AddAsignStorageAsPath<
    Lhs, Rhs, +Drop<Lhs>, +Drop<Rhs>, 
    impl LhsAsPath: StorageAsPath<Lhs>,
    +AddAssign<StoragePath<LhsAsPath::Value>, Rhs>
> of AddAssign<Lhs, Rhs> {
    fn add_assign(ref self: Lhs, rhs: Rhs) {
        let mut value = self.as_path();
        value += rhs;
    }
}

#[starknet::storage_node]
struct Numeric {
    num: u8,
    map: Map<u8, u8>
}

fn foo(ref self: ContractState) {
    let mut numeric = self.numeric.num;
    numeric += 1;
    let mut numeric = self.numeric.map.read(5);
    numeric += 6;
}

To avoid the risk of having += for storage variables that may have large hidden cost, one can ignore the AddAssign trait and implement a new trait, e.g. StorageInc, with an inc function that behaves similarly to the above

Giving access to storage variables

In previous versions, the only way to access storage outside the contract module was to use components or the unsafe_new_contract_state function. With the new storage types, we can call functions that accept StoragePath<T>, and thus allow them to modify our storage.

#[starknet::storage_node]
struct Numeric {
    num: u8,
    map: Map<u8, u8>
}

#[storage]
struct Storage {
    numeric: Numeric,
    map: Map<u8, u8>,
}

fn foo(ref self: ContractState) {
   modify_from_outside(self.numeric.deref(), self.map.deref());
}

pub fn modify_from_outside(
    numeric: StoragePath<Mutable<Numeric>>, 
    map: StoragePath<Mutable<Map<u8,u8>>> 
) {
    numeric.num.write(1);
    map.entry(5).write(3);
}

Note that we had to call deref explicitly to move from StorageBase to StoragePath.

This direction can yield a new way of writing components without much of the existing boilerplate. While still experimental, you can examine such an example in erc20_mini.cairo, and a contract using it in with_erc20_mini. With this approach, instead of using the component! macro and denoting a storage member by #[substorage(v0)], you can define the component’s storage struct as a storage node.

Phantom types

Types annotated by #[phantom] are types that cannot be instantiated. They are used to pass on information via traits that are implemented for them. For example, the following are phantom types:

#[phantom]
pub struct Map<K, V> {}

#[phantom]
pub struct Vec<T> {}

This shouldn’t be surprising as the above types are only used as the generic type of StoragePath. The Storage struct and any struct annotated by #[starknet::storage_node] are also phantom types.

Associated items

Associated items are items that are declared in traits and defined in implementations. They are defined similarly to the same concept in Rust. We proceed to dive into each the possible associated items.

Associated types

The addition trait is a good example of the potential advantage of associated types:

trait Add<Lhs, Rhs> {
    type Result;
    fn add(lhs: Lhs, rhs: Rhs) -> Self::Result;
}

suppose now that a function foo<A, B> needs the ability to add A and B. If we had defined the Add trait with an additional generic parameter that is used to describe the result, then our code would have looked like this:

fn foo<A,B,C, +Add<A,B,C>>(a:A, b:B) -> C {
   return a+b;
}

However, with associated types, we can get the result type from the impl of Add, and we don’t need to pollute foo with an additional generic argument:

fn foo<A,B, AddImpl: Add<A,B>>(a: A, b: B) -> AddImpl::Result {
    return a+b;
}

The point is that foo doesn’t necessarily need to generic on the addition result type, this information is associated with the impl of the Add trait.

Associated consts

Suppose that we have a game with multiple character types, e.g. Wizard and Warrior, and each character has some fixed hp based on its type. We can model this scenario as follows:

#[derive(Drop)]
struct Warrior { ... }
#[derive(Drop)]
struct Wizard { ... }

trait Character<T> {
    const hp: u32;
    fn fight(self: T);
}

impl WizardCharacter of Character<Wizard> {
    const hp: u32 = 70;
    fn fight(self: Wizard) { ... } 
}

impl WarriorCharacter of Character<Warrior> {
    const hp: u32 = 100;
    fn fight(self: Warrior) { ... } 
}

Since hp is fixed per character type, associated costs allowed us to bind this number to the character trait rather than adding it to the struct or just hardcoding the value in the implementation.

Associated impls

The new iterator traits are a good example of when one might need associated impls. Consider the new iterator traits from iterator.cairo:

// T is the collection type
pub trait Iterator<T> {
    type Item;
    fn next(ref self: T) -> Option<Self::Item>;
}

/// Turn a collection of values into an iterator.
pub trait IntoIterator<T> {
    /// The iterator type that will be created.
    type IntoIter;
    impl Iterator: Iterator<Self::IntoIter>;

    fn into_iter(self: T) -> Self::IntoIter;
}

An implementation of IntoIterator is expected to take a collection and return a corresponding iterator type IntoIter. How can we enforce that the returned type is indeed an iterator, i.e. something that implements that iterator trait? This is where the associated impl Iterator comes in, which is by definition an impl of the Iterator trait for the associated IntoIter iterator type.

The important observation is that any implementation of IntoIterator should return an iterator. Thus, while we can use generic impl params to enforce a specific implementation of IntoIterator returning an actual iterator, it should be already determined at the trait level. Associated impls are exactly the tool that allows us to do it.

Note that an implementation of IntoIterator does not necessarily need to specify the Iterator impl, if one exists in your context then it will be deduced, similarly to generic impl params. This is illustrated by the corelib’s implementation of IntoIterator<Array<T>>:

#[derive(Drop)]
pub struct ArrayIter<T> {
    array: Array<T>,
}

impl ArrayIterator<T> of Iterator<ArrayIter<T>> {
    type Item = T;
    fn next(ref self: ArrayIter<T>) -> Option<T> {
        self.array.pop_front()
    }
}

impl ArrayIntoIterator<T> of core::iter::IntoIterator<Array<T>> {
    type IntoIter = ArrayIter<T>;
    fn into_iter(self: Array<T>) -> ArrayIter<T> {
        ArrayIter { array: self }
    }
}

for loops

With iterators come for loops. You can now iterate over arrays and spans as follows:

fn test_for_loop_array_sum() {
    let mut sum = 0;
    for x in array![10, 11, 12] {
        sum += x;
    };
    assert_eq!(sum, 33);
}

The above loop is based on the ArrayIntoIterator implementation in the corelib. You can find more examples in for_test.cairo.

SHA256

The sha256_process_block_syscall system call is added. You’ll find high level functions for computing sha256 in sha256.cairo, specifically compute_sha256_u32_array and compute_sha256_byte_array.

Below is an example of computing sha256 in your contract:

#[external(v0)]
fn test_sha256(ref self: ContractState) {
    let mut input: Array::<u32> = Default::default();
    input.append('aaaa');

    // Test the sha256 syscall computation of the string 'aaaa'.
    let [res, _, _, _, _, _, _, _,] = compute_sha256_u32_array(input, 0, 0);
    assert(res == 0x61be55a8, 'Wrong hash value');
}

Each syscall application costs ~ 180 gas. Taking padding into account, we need a syscall invocation per ~ 14 u32 inputs, resulting in ~ 3.2 gas per byte.

Default implementations

We can now have default implementations of trait functions, similarly to Rust. Default implementations are illustrated in the example below:

trait TraitWithDefaultImpl {
    fn foo(self: u256);
    fn boo(self: u128) -> u128 {
        self+1
    }
}

impl SomeImpl of TraitWithDefaultImpl {
    fn foo(self: u256) {}
}

impl SomeOverridingImpl of TraitWithDefaultImpl {
    fn foo(self: u256) {}
    fn boo(self: u128) -> u128 { self }
}

SomeImpl::boo(3) will use the default implementation, while SomeOverridingImpl::boo(3) will use its own implementation.

Fixed size arrays

Fixed size arrays, denoted by [T; N], are a type that represents an N-tuple of T:

let arr1: [u64; 5] = [1,2,3,4,5];
let arr2: Span<u64> = [1,2,3,4,5].span();

To access a member of a fixed size array we can either deconstruct it with something like let [a, b, c, _, _] = arr1, or use the .span() function. Note that if we plan to repeatedly access the array, then it makes sense to call .span() once and keep it available throughout the accesses.

With const fixed size arrays, we can hardcode a potentially long sequence of data in our program, similarly to how dw (define word) was used in CairoZero:

const arr: [u64; 5] = [1,2,3,4,5];

To pass the const value cheaply between functions (without copying), you can call .span(), which has a trivial implementation (no cairo-steps overhead)

fn get_arr(arr: Span<u64>) { ... }
get_arr(arr.span());

Arithmetic circuits

We introduce an efficient way to define and run arithmetic circuits with a 384-bit modulus. Arithmetic circuits in Cairo are aimed at applications that need to be as bare metal as possible, and want to avoid the overhead of Cairo by defining raw circuits. Two new builtins, add_mod and mul_mod, will perform arithmetic operations under the hood. That is, Cairo is only used to describe the circuit.

Four types of gates are supported, AddModGate, SubModGate, MulModGate, and InverseGate. To construct a circuit, we use the empty CircuitElement<T> struct. The circuit description is encoded within the type T. That is, whenever we add a gate to the circuit, we wrap our existing circuit types with the appropriate gate types. For example, adding CricuitElement<Lhs> and CircuitElement<Rhs> results with CircuitElement<AddModGate<Lhs, Rhs>>.

The following code constructs the circuit: \frac{1}{x+y}\left((x+y)^{-1}-y\right):

use core::circuit::{
    RangeCheck96, AddMod, MulMod, u96, CircuitElement, CircuitInput, circuit_add, circuit_sub,
    circuit_mul, circuit_inverse, EvalCircuitTrait, u384, CircuitOutputsTrait, CircuitModulus,
    AddInputResultTrait, CircuitInputs,
};

let in1 = CircuitElement::<CircuitInput<0>> {};
let in2 = CircuitElement::<CircuitInput<1>> {};
let add = circuit_add(in1, in2);
let inv = circuit_inverse(add);
let sub = circuit_sub(inv, in2);
let mul = circuit_mul(inv, sub);

Now, we need to define the output tuple:

let output_gates = (mul);

Note that any of the circuit’s gates can be added to the output tuple. We can access the value of any gate after evaluating, but outputs must include all gates whose out degree is 0. The evaluation would have remained the same if we were do define output_gates as (mul, add, sub, inv).

Next, we proceed to initialize the circuits with inputs, each input is represented by fixed size array of four u96:

output_gates.new_inputs()
  .next([3, 0, 0, 0])
  .next([6, 0, 0, 0])
  .done()

Note that the next function returns a variant of the AddInputResult enum:

pub enum AddInputResult<C> {
    /// All inputs have been filled.
    Done: CircuitData<C>,
    /// More inputs are needed to fill the circuit instance's data.
    More: CircuitInputAccumulator<C>,
}

We must fill all the inputs of the circuit, and calling next on the Done variant, whose type CircuitData<C> describes a circuit initialized with inputs, will result in a panic.

Now that we have CircuitData<C> (C is now the long type encoding the entire circuit), we can define the modules and call eval:

let modulus = TryInto::<_, CircuitModulus>::try_into([7, 0, 0, 0]).unwrap();

let res = output_gates.eval(modulos).unwrap();

Note that eval returns Result, as the evaluation may err due to division by zero. If the evaluation had been successful, we can ask the value of any intermediate gate, assuming we an instance of the corresponding type:

assert_eq!(res.get_output(add), u384 { limb0: 2, limb1: 0, limb2: 0, limb3: 0 });
assert_eq!(res.get_output(inv), u384 { limb0: 4, limb1: 0, limb2: 0, limb3: 0 });
assert_eq!(res.get_output(sub), u384 { limb0: 5, limb1: 0, limb2: 0, limb3: 0 });
assert_eq!(res.get_output(mul), u384 { limb0: 6, limb1: 0, limb2: 0, limb3: 0 });

GM,

Exciting stuff coming!

Few question:

  • Regarding Contract Storage:
    • Can we still read entire structs?
      If I read correctly in addition to a Store impl I believe it is still possible but could you confirm?
    • It is then cheaper to read 1 member of the struct instead of the whole struct, right?
  • Regarding Maps
    • It is still using Pedersen hash, but Poseidon should be cheaper.
      Is it planned to have a Map using Poseidon to lower the costs?
  • Regarding Storage nodes
    • Using your MyNode example, does that mean I can do stuff like
    // This allows to read in loops an other behaviour
    let member1 = self.member.read();
    let member2 = member1.c.read();
    let member3 = member2.c.read();
    // Or is it mandatory to do like this?:
    let member3 = self.member.c.c.read();
    
  • Regarding += for Storage variables
    • Would you have a simple code example?
    • Does that mean there is also -=, *=, … or just += atm?
  • Regarding Phantom types
    • They cannot be instantiated but can they be passed as fn argument from external/view fn?

Thanks in advance! :slight_smile:

Hey, regarding your questions:

Can we still read entire structs?

Yes, you can still read entire structs (given a Store implementation)

It is then cheaper to read 1 member of the struct instead of the whole struct, right?

Yes, reading only one member is cheaper (less read syscalls are applied)

Is it planned to have a Map using Poseidon to lower the costs?

We chose Pedersen because, in addition to backward compatibility, it looks like with Stwo, Pedersen will be much easier to prove (due to its friendlier approach to preprocessing and lookups). It looks like we’re changing the direction from Poseidon to Pedersen for new stuff. We’re still considering other options for hashes, but we thought being backward compatible with LegacyMap breaks the symmetry.

Using your MyNode example, does that mean I can do stuff like

You can’t read a struct marked with #[storage_node]. It’s a phantom type, and can’t be instantiated, so nothing can be returned from read. Its purpose is to allow the compiler to generate code that allows you to access members of a storage node, via derfing the original struct (which is phantom) to a corresponding storage node type.

+= for Storage variables

+= May have been a bad example, you can ignore the AddAsign stuff, the idea is that with StoragePath you can now implement generic storage manipulations, see modify_from_outside. Note that this code is not part of the corelib, because it’s not a good idea to have += that is implicitly very expensive due to storage writes.

They cannot be instantiated but can they be passed as fn argument from external/view fn?

You can’t have them as a function argument since the function will never be callable (you can’t instantiate the input). They are useful as generic dependencies, for example, we can implement things for StoragePath<Map>, as done in the corelib.

Blockquote
We chose Pedersen because, in addition to backward compatibility, it looks like with Stwo, Pedersen will be much easier to prove (due to its friendlier approach to preprocessing and lookups). It looks like we’re changing the direction from Poseidon to Pedersen for new stuff. We’re still considering other options for hashes, but we thought being backward compatible with LegacyMap breaks the symmetry.

Wdym?
Does that mean poseidon should be used for new stuff?
Do you recommend to stick with Pedersen as it’ll be cheaper with Stwo?
Or will there be a new Hashing fn optimised for Stwo?

Pedersen will be cheaper in stwo. We might choose a different hash altogether that wins on both x86 & proving metrics. If you must choose now, then I suggest to be compatible with SNIP12 (which at the current revision uses Poseidon), and if you don’t need snip12 then I’d probably choose Pedersen.

This looks fascinating!

A question about Vec<T> and Map<K, V> - is there a plan to include these in regular plain Cairo?
If Cairo is supposed to be a general purpose language, and not just used for Starknet, then it would make sense to have these included in the core language.

Vec and Map are types intended for storage access, both are empty structs that can’t be instantiated, they only serve the purpose of externalizing the ability to access storage. The non-contract analog of Map is Felt252Dict<T>, and Array<T> serves the purpose of vec in memory (we originally thought about naming the new vec as StorageArray).

I think it would have been a much better name. StorageArray is explicit that it’s scope is to put an array in Storage. Vec is extremely confusing, especially for people with a Rust background that will try to use it as an memory-mutable data structure (as the one that has been implemented in Alexandria for almost a year now)

I understand your point, but it’s at least mitigated by the fact that both lie under starknet::storage, i.e. to import you need use starknet::storage::{Vec, Map}. With the storage prefix, you’d have a somewhat redundant “storage” in the full path.

Just curious about the reasoning, how come only Felt252Dict<T> is implemented in the base language, but something like Map<K, V> is not? Is there a plan to support this?

These are very different types, anything related to Starknet storage makes no sense in standalone Cairo. This leaves the following interpretation of your question: “why no dicts with non-felt keys”? I think there should be, we can have Dict<K, V> where K has Hash, and under the hood Felt252Dict is used, but this is not implemented yet.

The same argument could be used for StoragePointer, StoragePath and StorageBaseAddress :smile:

My first concern is for people not experienced, it’s confusing and hurts accessibility a bit (if you see in code X someone using Vec (as in memory Vec) and end up using Vec (as in storage Vec), it’s confusing. And this issue happens constantly with other items.

One more argument in favor of my position:

When I import from starknet::storage, I import
use core::starknet::storage::{StorageMapReadAccess, StorageMapWriteAccess, Map}

Why StorageMapRead and StorageMapWrite, but only Map ?

To keep the API consistent, either rename it MapRead, MapWrite and Map, or StorageMapRead, StorageMapWrite and StorageMap.

Since the usage of the traits is not in the structs where i would prefer in general a short name, but in the above usages.
Having long name for traits is much less of a hassle than for types (since used by . and not with full name in normal context).

After seeing a lot of people confused by the Vec and Map types (not understanding that they’re storage-only types, and trying to use it like an Array) I still stand by my point that explicit naming would have avoided all that.

I actually looked at other languages, and Sway defines StorageMap and StorageVec types that are explicitly used for storage

I agree with @eni about Vec/Map vs StorageArray/StorageMap especially because Vec/Map have some limitations/constraints directly due to the fact that they are used for storage only (for example, at the moment, we cannot remove items from a Vec or change its length).

That means we will never use a Vec/Map as we use in-memory vectors/maps in general.

+1 for Eni’s remark!

We can introduce the StorageMap/StorageVec aliases so that existing code doesn’t break, but WDYT about changing the public examples to refer to Map explicitly, i.e. instead of

use starknet::storage::Map

Write

use starknet::storage

and then refer to Map or Vec via storage::Vec or storage::Map? Won’t this resolve most of the confusion?

Why is Rescue hash function not used in Starknet ?

We only have builtins for Pedersen, Poseidon, and Keccak. Perhaps the new prover will pave the road for more efficient hashes, and down the line we can replace the hashes used when computing storage addresses.