Cairo v2.2.0 is out!

Cairo v2.2.0

TL;DR

Cairo v2.2.0 was just released. Since there were only high-level upgrades (i.e., no Sierra changes), it is associated with Sierra v1.3.0 (same as Cairo v2.1.0) which is compatible with Starknet ≥0.12.1 (that is, it is supported on all Starknet environments).

The full changelog is found in the release notes. In this post we’ll go over a few notable mentions in more detail.

Missing from this post are the new ByteArray struct and bytes31 type that will serve as the basis for strings in Cairo, as it is still experimental, and thus, contracts using it cannot yet be deployed on Starknet. Soon, they will get their due attention in a standalone post. For now, you can find usage examples in byte_array_test.cairo.

Language server improvements

We now have many more autocompletions:

and templates for various common blocks in the language:

the above completions generate the following templates:

// enum template
enum  {
    : ,
}

// event dictionary template
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
    : ,
}

// external impl template
#[external(v0)]
impl  of <ContractState>{
    
}

// external function template
fn (ref self: ContractState, ) {
    
}

Hash trait refactored and can be derived

The following traits are now available in the core lib:

/// A trait for values that can be hashed.
trait Hash<T, S, impl SHashState: HashStateTrait<S>> {
    /// Updates the hash state with the given value.
    fn update_state(state: S, value: T) -> S;
}
/// A trait for hash state accumulators.
trait HashStateTrait<S> {
    fn update(self: S, value: felt252) -> S;
    fn finalize(self: S) -> felt252;
}
/// Extension trait for hash state accumulators.
trait HashStateExTrait<S, T> {
    /// Updates the hash state with the given value.
    fn update_with(self: S, value: T) -> S;
}

These traits provide a convenient hashing interface that is general enough to support both Pedersen (which should only be used for backward-compatibility purposes, as Poseidon is more STARK friendly and thus cheaper) and Poseidon. The following example illustrates how we can hash a user-defined type:

use core::hash::HashStateExTrait;
use poseidon::{PoseidonTrait, HashState};
use hash::{HashStateTrait, Hash};

#[derive(Hash)]
struct MyMessage {
    message_type: u256,
    content: u256
}

fn main() {
  let hash_state = PoseidonTrait::new();
  let message = MyMessage { message_type: 1, content: 2 };
  let hash = hash_state.update_with(message).finalize();
}

The resulting hash from using a new Poseidon HashState and the derived implementation is poseidon(message_type.low, message_type.high, content.low, content.high), where the specs for the Poseidon hash implementation can be found in the Starknet documentation.

Thanks to the flexibility of the new hashing mechanism, to use a different underlying hash function, we only need to change the hash state while using the same derived implementation:

use core::hash::HashStateExTrait;
use pedersen::{PedersenTrait, HashState};
use hash::{HashStateTrait, Hash};

let hash_state = PedersenTrait::new();
let message = MyMessage { message_type: 1, content: 2 };
let hash = hash_state.update_with(message).finalize();

Note that to use the update_with function directly on hash_state, we needed to import the HashStateExTrait trait, which has the following generic implementation in the corelib:

impl HashStateEx<
    S, impl SHashState: HashStateTrait<S>, T, impl THash: Hash<T, S, SHashState>
> of HashStateExTrait<S, T> {
    #[inline(always)]
    fn update_with(self: S, value: T) -> S {
        THash::update_state(self, value)
    }
} 

Added derive default and a default impl for small tuples

You can now use derive(Default) on structs and enums that specify that default variant (via annotating one of the variants with #[default]):

#[derive(Copy, Drop, Default)]
struct MyStruct {
    a: u128,
    b: (u64, u128, u256, bool)
}

#[derive(Copy, Drop, Default, PartialEq)]
enum MyEnum {
    A,
    #[default]
    B: u128
}

let my_struct: MyStruct = Default::default();
let my_enum: MyEnum = Default::default();
assert(my_enum == MyEnum::B(0), 'unexpected default');

Note that the corelib now contains a default implementation for tuples of size up to 4.

Added a `PartialEq` implementation for arrays and spans to the corelib

You can now compare arrays and spans using the corelib’s implementation of PartialEq:

use array::ArrayTrait;

let first = array![1, 2, 3];
let second = array![1, 2, 3];
assert(first == second, 'should be equal');
assert(first.span() == second.span(), 'should be equal')

Nullable new

Implementing NullableTrait<T> allows using T as dictionary values. Up until now, to create Nullable<T> you first had to get Box<T> and then call the nullable_from_box libfunc. Using the new function in the trait NullableTrait, you can do the following:

let mut dict: Felt252Dict<Nullable<u256>> = felt252_dict_new::<Nullable<u256>>();
dict.insert(0, NullableTrait::new(5));

Added the ability to test L2→L1 messages

You can now use the test runner to test sending L2→L1 messages via the pop_l2_to_l1_message function from starknet::testing:

#[test]
#[available_gas(300000)]
fn test_pop_l2_to_l1_message() {
    let contract_address = starknet::contract_address_const::<0x42>();
    testing::set_contract_address(contract_address);

    let mut to_address = 1234;
    let mut payload = array![2345];

    starknet::send_message_to_l1_syscall(to_address, payload.span());
    starknet::send_message_to_l1_syscall(to_address, payload.span());

    let (to_address, payload) = starknet::testing::pop_l2_to_l1_message(contract_address).unwrap();
    assert_eq(@payload.len(), @1, 'unexpected payload size');
    assert_eq(@to_address, @1234, 'unexpected to_address');
    assert_eq(payload.at(0), @2345, 'unexpected payload');

    let (to_address, payload) = starknet::testing::pop_l2_to_l1_message(contract_address).unwrap();
    assert_eq(@payload.len(), @1, 'unexpected payload size');
    assert_eq(@to_address, @1234, 'unexpected to_address');
    assert_eq(payload.at(0), @2345, 'unexpected payload');
}

You can find more examples in l2_to_l1_messages.cairo.

Added the `selector!` macro

You can now use the selector! macro instead of hardcoding certain selectors in your code (particularly relevant for testing):

let selector: felt252 = selector!("my_function");

The selector of an external function in Starknet is defined to be the sn_keccak of the big-endian ASCII encoding of the function’s name.

Many use statements are no longer necessary

Everything under lib.cairo doesn’t need to bee imported explicitly. This includes all common traits: Default, PartialEq, Into, TryInto and many others.

Additionally, as long as the relevant trait has at least one impl in the current context, the trait itself does not need to be imported. For example, we no longer need to import a trait when we have a generic impl parameter:

// no longer need "use another_module::SomeTrait"
fn some_func<impl X: another_module::SomeTrait>() {
    SomeTrait::some_other_func();
}