Cairo v2.6.0 is out!

Cairo v2.6.0

TL;DR

Cairo 2.6.0 was just released. This version involves a Sierra upgrade to v1.5.0, which means contracts written with v2.6.0 are only deployable on Starknet ≥ 0.13.1. The testnets upgrades are planned for March 5’th and 6’th (Sepolia and Goerli correspondingly), while the mainnet upgrade is planned for approximately one week later. As usual, you can continue using older compiler versions and deploy them on Starknet.

This version is not very heavy feature-wise, but it does include significant performance improvements, potentially significantly reducing the number of steps (and, as a result, the transaction fees) in your contract’s functions without requiring code changes. To be clear, to enjoy this potential speedup, you need to recompile your contracts with v2.6.0, and upgrade your contracts to use the new code. Continue with the rest of the post to see what’s new in this version in more detail.

Performance improvements

Up until now, ERC20 transfers with both the account and ERC20 contracts written in Cairo were ~2-2.5x more expensive, in terms of # of steps, than similar ERC20 transfers where both the account and ERC20 contracts were written in Cairo zero. With Cairo 2.6.0, transfer transactions beat the Cairo zero performance by ~10-20% (this was tested on OpenZeppelin 0.9.0 ERC20 and account contracts, when compiled with the new version). Generally, the more abstracted the language is (or the further away it is from assembly), the less performant one would expect it to be. How, then, do Cairo 2.6.0 transfer transactions beat their Cairo zero counterpart? This was made possible due to two recent changes:

Using Span<felt252> rather than Array<felt252> in external function signatures

Whenever devs write external functions, the compiler wraps those functions with the actual contract entry points, which gets the raw calldata as a sequence of felts, and deserializes it according to the function signature. This deserialization phase has some step costs, which can get large for large calldata. This happens at least twice in every transaction, during the call to the __validate__ and __execute__ functions. In standard implementations, both execute and validate have the following signature:

fn __execute__(ref self: ContractState, calls: Array<Call>) -> Array<Span<felt252>>

Already in v2.5.0 the Call struct was changed from:

#[derive(Drop, Serde, Debug)]
pub struct Call {
    pub to: ContractAddress,
    pub selector: felt252,
    pub calldata: Array<felt252>
}

to:

#[derive(Drop, Serde, Debug)]
pub struct Call {
    pub to: ContractAddress,
    pub selector: felt252,
    pub calldata: Span<felt252>
}

We’re mentioning this change now, although it technically happened on v2.5.0, since popular accounts only recently adjusted to the new struct. While the deserialization into an array requires looping over the elements, deserialization into Span<felt252> is trivial, hence thanks to the above change, we’re doing less steps during the calldata deserialization phase for both __validate__ and __execute__.

(Aggresive) Inlining

Thanks to the fact that the new language has a much more structured representation compared to Cairo zero (which was closer to a syntactic sugar for Cairo assembly), we can now decide on an inlining strategy and try to find the sweet spot on the # of steps vs code length tradeoff.

By inlining small enough functions (thus saving the step-cost involved in calling the functions and obtaining its arguments), we were able to significantly reduce the # of steps involved in a transfer transaction. In fact, for accounts and ERC20 contracts, our inlining heuristic even reduced the code length, which is expected if there are many small functions with a large number of arguments, making their arguments handling longer (in terms of # of instructions) than the actual function body.

Note that in addition to user-defined functions, as of v2.6.0 many of the corelib functions are now inlined. To see the specific details of our inlining strategy, we refer the interested reader to the relevant compiler code.

We emphasize that to enjoy the benefits of the new optimizations, devs should recompile their contracts with the v2.6.0 compiler and upgrade their contracts on-chain

if let & while let

Similarly to Rust, Cairo now supports if let and while let expressions:

#[derive(Drop)]
enum MyEnum {
    Foo,
    Bar
}

let number = Option::Some(5);
let foo_or_bar = MyEnum::Foo;

if let Option::Some(i) = number {
    //do something
}

if let MyEnum::Bar = foo_or_bar {
    //do something
}

fn array_sum(mut arr: Array<felt252>) -> felt252 {
    let mut sum = 0;
    while let Option::Some(x) = arr.pop_front() {
        sum += x;
    };
    sum
}

Consts

Before Cairo v2.6.0, only literal constants were supported (that is, constants of types that can be instantiated from a literal). To “define” constants of more complicated types, devs usually had to write dedicated functions, for example:

struct MyStruct {
    a: u256,
    b: u256
}

// instead of const zero_struct: MyStruct { 0, 0 }, we need the following:
fn zero_struct() -> MyStruct {
    MyStruct { a: 0, b: 0 }
}

Sierra v1.5.0 has the notion of a consts segment within the program (Sierra) bytecode, which finally enables us to have non-trivial consts in our high-level Cairo program:

enum ThreeOptions {
    A: felt252,
    B: (u256, u256),
    C,
}

struct ThreeOptionsPair {
    a: ThreeOptions,
    b: ThreeOptions,
}

const V: ThreeOptionsPair = ThreeOptionsPair {
    a: ThreeOptions::A(1337),
    b: ThreeOptions::C,
};

We can refer to our defined consts directly or via Box:

fn bar() -> ThreeOptionsPair { V }
fn foo() -> Box<ThreeOptionsPair> { BoxTrait::new(V) }

Using boxes we can pass the const value around without paying for copying the contents.

Currently, structs and enums are supported, and we’ll soon extend the support to additional types. Most notably, const Span<felt252> will be supported soon, analogous to dw (define word) from Cairo zero. Since additional type support only requires changes to the high-level compiler (Cairo → Sierra), once it is added it will immediately be accessible on Starknet via Scarb nightlies.

Miscellaneous

In this post we covered the major features of v2.6.0. As usual, there are several minor improvements that you can find, for example, adding support for _ in a match arm when matching over tuples or the addition of min and max functions to the corelib for types T for which there is a PartialOrd<T> implementation. For an exhaustive list of features, you can see the release notes on GitHub.