Cairo v2.1.0 is out!

Cairo v2.1.0

TL;DR

Cairo v2.1.0 was just released. It is associated with Sierra v1.3.0 which is compatible with Starknet ≥0.12.1 (expected on Testnet on Aug 07 and Mainnet ~ 2 weeks later).

You can find the full changelog in the release notes. In this post we’ll go over a few notable mentions in more detail.

StorageAccess trait refactored and renamed to Store

Implementing the StorageAccess<T> trait was required for any type T we want to store (put in the Storage struct or as a value in LegacyMap). The old trait appears below:

trait StorageAccess<T> {
    fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<T>;
    fn write(address_domain: u32, base: StorageBaseAddress, value: T) -> SyscallResult<()>;
    fn read_at_offset_internal(
        address_domain: u32, base: StorageBaseAddress, offset: u8
    ) -> SyscallResult<T>;
    fn write_at_offset_internal(
        address_domain: u32, base: StorageBaseAddress, offset: u8, value: T
    ) -> SyscallResult<()>;
    fn size_internal(value: T) -> u8;
}

The trait is now called Store, and some of its methods were renamed. Additionally, the size function now takes no argument.

trait Store<T> {
    fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<T>;
    fn write(address_domain: u32, base: StorageBaseAddress, value: T) -> SyscallResult<()>;
    fn read_at_offset(
        address_domain: u32, base: StorageBaseAddress, offset: u8
    ) -> SyscallResult<T>;
    fn write_at_offset(
        address_domain: u32, base: StorageBaseAddress, offset: u8, value: T
    ) -> SyscallResult<()>;
    fn size() -> u8;
}

Luckily, for most applications (like storing structs or enums), you don’t have to implement this trait yourself but simply annotate your type with #[Derive(Store)]. You can see how exactly the compiler implements the new trait for structs and enums in storage_access.rs. Since the old implementations never really relied on the value to determine the size, we decided to fix the size per type in the trait.

This is the only breaking change in 2.1.0. Note that Derive(StorageAccess) needs to be replaced with Derive(Store).

Packing

The StorePacking<T, PackedT> trait was added to the corelib:

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

The motivation behind this trait is to allow storing a type T not through its derived Store implementation but first pack it into another type PackedT that has a Store implementation, and storing PackedT instead. To this end, the StoreUsingPacking<T, packedT> implementation of Store<T> was added to the corelib:

impl StoreUsingPacking<
    T, PackedT, impl TPacking: StorePacking<T, PackedT>, impl PackedTStore: Store<PackedT>
> of Store<T> {
    #[inline(always)]
    fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult<T> {
        Result::Ok(TPacking::unpack(PackedTStore::read(address_domain, base)?))
    }
    #[inline(always)]
    fn write(address_domain: u32, base: StorageBaseAddress, value: T) -> SyscallResult<()> {
        PackedTStore::write(address_domain, base, TPacking::pack(value))
    }
    #[inline(always)]
    fn read_at_offset(
        address_domain: u32, base: StorageBaseAddress, offset: u8
    ) -> SyscallResult<T> {
        Result::Ok(TPacking::unpack(PackedTStore::read_at_offset(address_domain, base, offset)?))
    }
    #[inline(always)]
    fn write_at_offset(
        address_domain: u32, base: StorageBaseAddress, offset: u8, value: T
    ) -> SyscallResult<()> {
        PackedTStore::write_at_offset(address_domain, base, offset, TPacking::pack(value))
    }
    #[inline(always)]
    fn size() -> u8 {
        PackedTStore::size()
    }
}

Suppose you have a struct containing two u8 members. Using the derived Store, this struct will use two storage slots. To save costs on the storage of this variable, you can pack it as follows:

#[starknet::interface]
trait IPack<TContractState> {
    fn pack(ref self: TContractState, value: PackingContract::PackableStruct);
    fn unpack(self: @TContractState) -> PackingContract::PackableStruct;
}

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

    #[storage]
    struct Storage {
        packable: PackableStruct
    }

    #[derive(Drop)]
    struct PackableStruct {
        first: u8,
        second: u8
    }

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

    #[external(v0)]
    impl PackContract of super::IPack<ContractState> {
        fn pack(ref self: ContractState, value: PackableStruct) {
            self.packable.write(value);
        }
        fn unpack(self: @ContractState) -> PackableStruct {
            return self.packable.read();
        }
    }
}

While the derived implementation for PackableStruct would have used two storage slots, the corelib implementation which relies on our PackPackable impl of StorePacking<PackableStruct, felt252> only uses one slot. The resulting packed implementation saves ~1.2k gas per unique write to packable.

Small tuples and enums in Storage

You can now derive Store on enums and tuples of size ≤ 4:

use array::ArrayTrait;

#[storage]
struct Storage {
  packable: PackableStruct,
  my_enum: MyEnum,
  tup: (u8, bool, u256, felt252)
}

#[derive(Drop, starknet::Store)]
enum MyEnum {
  First: u256,
  Second: u256
}

Currently, the generated code for storing an enum depends on the array trait, so you must add use array::ArrayTrait. This will be handled for you in the next version.

The `array!` macro

You can now initialize arrays as follows:

let arr = array![10, 11, 12];

Test Runner improvements

Pop log refactor

The old testing function:

extern fn pop_log(
    address: ContractAddress
) -> Option<(Span<felt252>, Span<felt252>)> implicits() nopanic;

is renamed to pop_log_raw, and the following more convenient function was introduced:

// Pop the earliest unpopped logged event for the contract as the requested type.
fn pop_log<T, impl TEvent: starknet::Event<T>>(address: ContractAddress) -> Option<T> {
    let (mut keys, mut data) = pop_log_raw(address)?;
    starknet::Event::deserialize(ref keys, ref data)
}

pop_log in its new form allows you to handle variants of the Event type rather than raw keys and data arrays. To see its power, see the events test in the compiler repository.

Replace class support

The replace_class_syscall is now supported in the test runner (previously did not affect the state).

Enums - empty variant support

No more size 0 tuples in enum variants! From now on, variants without types will be considered empty, and you can use them in a match expression more naturally.

enum MyEnum {
	First,
	Second: u256
}

let my_enum: MyEnum = ...

match my_enum {
	A => { ... },
  B(_) => { ... }
}

Bitwise operations for unsigned integers

Bitwise and, or and not are now supported for all unsigned integers:

use test::test_utils::assert_eq;

let a: u8 = 5;
let b: u8 = 6;
let res = a & b;
assert_eq(@res, @4, 'Wrong result`);

let a: u16 = 65535;
let res = ~a;
assert_eq(@res, @0, 'Wrong result`);

Keccak Support

Added support for the following three functions in keccak.cairo:

fn keccak_u256s_le_inputs(mut input: Span<u256>) -> u256;
fn keccak_u256s_be_inputs(mut input: Span<u256>) -> u256;
fn cairo_keccak(
	ref input: Array<u64>,
	last_input_word: u64, 
	last_input_num_bytes: usize
) -> u256;

In Cairo, all units are in little-endian representation, so if you simply want to Keccak an array of u256, then fn keccak_u256s_be_inputs(mut input: Span<u256>) -> u256 does the trick. In case you want to Keccak an arbitrary number of bytes (not necessarily a multiple of 8), you can use the last function, cairo_keccak, and specify the exact number of bytes to be considered from the last 64 byte word.

If you’re trying to be solidity-compatible, you’re probably interested in keccak_u256s_be_inputs. Note that you must also apply u128_byte_reverse on the output to match Soldiity’s endianness. To ensure you’re compatible with Solidity’s Keccak, see the specs of abi-encode. Most solidity types (addresses and all units) are encoded as full 256 be integers, so they’re easily mapped into Cairo. Some types, like bytes 32, require a more specific encoding (in the case of bytes n, the value needs to be preceded by the encoding of n as a u256), so make sure to follow solidity’s specs.

Secp-256k1 Support

You can now verify Ethereum signatures using Cairo. In secp256_trait.cairo you’ll find the Signature struct and verify_eth_signature function:

#[derive(Copy, Drop, PartialEq, Serde, storage_access::StorageAccess)]
struct Signature {
    r: u256,
    s: u256,
    // The parity of the y coordinate of the ec point whose x coordinate is `r`.
    // `y_parity` == true means that the y coordinate is odd.
    // Some places use non boolean v instead of y_parity.
    // In that case, `signature_from_vrs` should be used.
    y_parity: bool,
}

fn verify_eth_signature<
    Secp256Point,
    impl Secp256PointDrop: Drop<Secp256Point>,
    impl Secp256Impl: Secp256Trait<Secp256Point>,
    impl Secp256PointImpl: Secp256PointTrait<Secp256Point>
>(
    msg_hash: u256, signature: Signature, eth_address: EthAddress
)

Note that in addition to r,s, an additional parity bit needs to be supplied. Some libraries encapsulate this additional argument from the exposed function and try both options to verify the signature. We decided to keep this extra argument here since, when y is supplied, we can save two multiplications during verification. Note that costs can become relevant here since every multiplication on this curve costs the equivalent of ~130k Cairo steps.

PartialEq for small Tuples

This one is self-explanatory; the compiler now implements PartialEq (which allows using the == operator) for tuples of size ≤ 4.

let first_tuple = (1,1,2,3)
let second_tuple = (2,1,3,4)
let first_tuple == second_tuple;

Hi,

Regarding the support of secp256k1 curve, I would like to understand what is the impact with regard to hash functions ? With Stark curve, Pedersen or Poseidon hash over Stark field are used but I assume that they can’t be signed on secp256k1 (defined on another field). So what is the hash function to be used ? Thanks !

The short answer is Pedersen, the long answer is that it is up to the account implementation.

Today, all the accounts I’m familiar with check that the signature is valid over the transaction hash. The transaction hash itself is not computed in the account contract, but rather in the get_tx_info system call, i.e. it is controlled by the protocol. Currently, transaction hashes are computed with Pedersen. That said, an account can take all the transaction data, compute a different hash, and check the signature w.r.t this hash. In practice, this is not done since these are extra steps that the user will pay for instead of using the existing transaction hash.