Storage optimisation

A smart contract has a limited amount of storage slots. Each slot can store a single felt252 value. Writing to a storage slot has a cost, so we want to use as few storage slots as possible.

In Cairo, every type is derived from the felt252 type, which uses 252 bits to store a value. This design is quite simple, but it does have a drawback: it is not storage efficient. For example, if we want to store a u8 value, we need to use an entire slot, even though we only need 8 bits.

Packing

Cuando almacenamos múltiples valores, podemos utilizar una técnica llamada packing. El empaquetado es una técnica que nos permite almacenar múltiples valores en un único valor de felt. Esto se hace utilizando los bits del valor de felt para almacenar múltiples valores.

Por ejemplo, si queremos almacenar dos valores u8, podemos utilizar los primeros 8 bits del valor de felt para almacenar el primer valor u8, y los últimos 8 bits para almacenar el segundo valor u8. De esta forma, podemos almacenar dos valores u8 en un único valor de felt.

Cairo proporciona un almacén incorporado que usa empaquetado que puedes usar con el trait StorePacking.

trait StorePacking<T, PackedT> {
    fn pack(value: T) -> PackedT;
    fn unpack(value: PackedT) -> T;
}

Esto permite almacenar el tipo T empaquetándolo primero en el tipo PackedT con la función pack, y luego almacenando el valor PackedT con su implementación Store. Al leer el valor, primero recuperamos el valor PackedT, y luego lo desempaquetamos en el tipo T utilizando la función unpack.

He aquí un ejemplo de almacenamiento de una estructura Time con dos valores u8 utilizando el trait StorePacking:

#[starknet::interface]
trait ITime<TContractState> {
    fn set(ref self: TContractState, value: TimeContract::Time);
    fn get(self: @TContractState) -> TimeContract::Time;
}

#[starknet::contract]
mod TimeContract {
    use starknet::storage_access::StorePacking;
    use integer::{
        U8IntoFelt252, Felt252TryIntoU16, U16DivRem, u16_as_non_zero, U16IntoFelt252,
        Felt252TryIntoU8
    };
    use traits::{Into, TryInto, DivRem};
    use option::OptionTrait;
    use serde::Serde;

    #[storage]
    struct Storage {
        time: Time
    }

    #[derive(Copy, Serde, Drop)]
    struct Time {
        hour: u8,
        minute: u8
    }

    impl TimePackable of StorePacking<Time, felt252> {
        fn pack(value: Time) -> felt252 {
            let msb: felt252 = 256 * value.hour.into();
            let lsb: felt252 = value.minute.into();
            return msb + lsb;
        }
        fn unpack(value: felt252) -> Time {
            let value: u16 = value.try_into().unwrap();
            let (q, r) = U16DivRem::div_rem(value, u16_as_non_zero(256));
            let hour: u8 = Into::<u16, felt252>::into(q).try_into().unwrap();
            let minute: u8 = Into::<u16, felt252>::into(r).try_into().unwrap();
            return Time { hour, minute };
        }
    }

    #[abi(embed_v0)]
    impl TimeContract of super::ITime<ContractState> {
        fn set(ref self: ContractState, value: Time) {
            // This will call the pack method of the TimePackable trait
            // and store the resulting felt252
            self.time.write(value);
        }
        fn get(self: @ContractState) -> Time {
            // This will read the felt252 value from storage
            // and return the result of the unpack method of the TimePackable trait
            return self.time.read();
        }
    }
}

Juega con este contrato en Remix.

Last change: 2023-10-12, commit: 90aa7c0