Cairo v2.4.0 is out!

Cairo v2.4.0

TL;DR

Cairo 2.4.0 was just released. This version involves a Sierra upgrade to v1.4.0, which means contracts written with v2.4.0 are only deployable on Starknet ≥ 0.13.0. The testnet upgrade to v0.13.0 is planned for next week, December 12’th, and the mainnet upgrade is planned to follow approximately 1 month later. Note that if you plan to deploy on mainnet before the upgrade, then you should keep using 2.3.1 (Scarb supports a convenient transition between compiler versions via asdf, see Scarb docs).

This version focuses on the introduction of ByteArray (aka strings), filling a hole that existed in the language since the days of Cairo 0. This version adds a few standard macros that allow conveniently working with strings, and finally drops the 31-character length constraint on error messages. Additional features are still in development, most notably keeping a string in the contract’s storage.

Continue with the rest of the post to see what’s new in this version.

ByteArrays

Cairo’s representation of strings is done with the following ByteArray struct:

#[derive(Drop, Clone, PartialEq, Serde, Default)]
struct ByteArray {
    data: Array<bytes31>,
    pending_word: felt252,
    pending_word_len: usize,
}

The new Sierra type bytes31 holds (unsurprisingly) 31 bytes, which fits into a single felt. The ByteArray struct keeps an array of 31byte chunks, and another word + length for the remainder (needed in case the length isn’t divisible by 31).

ByteAarrays can be initialized with string literals:

let ba: ByteArray = "this is a string which has more than 31 characters";

ByteArrays support the following operations that are defined in the corelib:

#[generate_trait]
impl ByteArrayImpl of ByteArrayTrait {
    fn append_word(ref self: ByteArray, word: felt252, len: usize) {...}
    fn append(ref self: ByteArray, mut other: @ByteArray) {...}
    fn concat(left: @ByteArray, right: @ByteArray) -> ByteArray {...}
    fn append_byte(ref self: ByteArray, byte: u8) {...}
    fn len(self: @ByteArray) -> usize {...}
    fn at(self: @ByteArray, index: usize) -> Option<u8> {...}
}

Usually, you will not work directly with the above operations but instead use one of the new macros. Before we present the new macros, note that the corelib now contains the Formatter struct alongside the Display and Debug traits:

#[derive(Default, Drop)]
struct Formatter {
    /// The pending result of formatting.
    buffer: ByteArray,
}

/// A trait for standard formatting, using the empty format ("{}").
trait Display<T> {
    fn fmt(self: @T, ref f: Formatter) -> Result<(), Error>;
}

/// A trait for debug formatting, using the empty format ("{:?}").
trait Debug<T> {
    fn fmt(self: @T, ref f: Formatter) -> Result<(), Error>;
}

The write! macro

This macro takes a formatter and a byte array or formatted string, and appends it to the formatter.

let var = 5;
let mut formatter: Formatter = Default::default();
write!(formatter, "test");
write!(formatter, "{var:?}");
println!("{}", formatter.buffer); //prints test5

write! is mostly used in the corelib for the implementation of the Debug and Display traits. Usually you would prefer using the format! macro described below.

The format! macro

let var1 = 5;
let var2: ByteArray = "hello";
let var3 = 5_u32;
let ba = format!("{},{},{}", var1, var2, var3);
let ba = format!("{var1}{var2}{var3}");
let ba = format!("{var1:?}{var2:?}{var3:?}");

Similarly to rust, you can use formatting directives inside the curly brackets. You can specify the desired formatting traits with ?, i.e. "{var:}" will use the Display trait while {var:?} will use the Debug trait. You can find more examples here.

The print! and println! macros

let var1 = 5;
let var2: ByteArray = "hello";
let var3 = 5_u32;
// prints:
// 5,hello,5
// 5hello55hello5
println!("{},{},{}", var1, var2, var3);
print!("{var1}{var2}{var3}");
print!("{var1:?}{var2:?}{var3:?}");

As expected, these macros print to the console. Note that prin!t and println! can only be used in tests (printing is meaningless in a smart contract context).

The panic! macro

Panic causes the program to terminate (after destructing all the outstanding types that are not droppable). The input to the panic macro is the panic error:

panic!("this should not be reached, but at least I'm not limited to 31 characters anymore")

The assert! and assert_eq! macros

The assert! macro allows you to check whether a condition holds and panic otherwise with a given panic string (which can be formatted).

let var1 = 5;
let var2 = 6;
assert!(var1 != var2, "should not be equal");
assert!(var1 != var2, "{},{} should not be equal", var1, var2);

The assert_eq! macro is only useable in the test plugin, and as the name suggests it asserts equality between two values.

#[derive(Drop, Debug, PartialEq)]
struct MyStruct {
    a: u8,
    b: u8
}
...
let first = MyStruct { a: 1, b: 2 };
let second = MyStruct { a: 1, b: 2 };
assert_eq!(first, second, "{:?},{:?} should be equal", first, second);

Note that in order to format MyStruct we can either derive Debug and use :? or implement Display ourselves.

Next steps for byte arrays

In v2.4.0, storing bytearrays inside a contract is still unsupported (ByteArray does not derive Store as it includes a dynamic array). This is needed for common instances of ERC721 and will be addressed in the next Cairo version.

Editions and the introduction of Preludes

So far, all the corelib base module was in the context of all cairo code. Now, depending on your package configuration, you’ll get a much smaller subset.

To set your package configuration, add the edition entry to Scarb.toml (also see the Scarb docs):

[package]
name = "my_package"
version = "0.1.0"
edition = '2023_10'

Currently, there are two possible editions, 2023_01 and 2023_10, corresponding to the old (currently default) and the new preludes. You can see exactly what parts they include from the corelib in v2023_01.cairo and v2023_10.cairo. Developers are encouraged to update their packages to the newest edition.

Sierra 1.4.0

This version includes a minor Sierra upgrade and thus isn’t usable on Starknet before the v0.13.0 protocol upgrade (the sequencer compiles classes given in DECLARE transactions with the Sierra→CASM compilation; if a newer compiler generated the class in question, then the compilation will fail, resulting in the transaction being rejected).

The new Sierra version brings two important updates:

  • ByteArray libfuncs (necessary for all the ByteArray-related code we saw above)
  • Rewriting the Sierra→CASM compiler pricing algorithm

To realize the importance of the second point, recall that the Sierra→CASM compiler attempts to assign every function its gas cost. Sometimes, e.g. in the case of recursive functions, this can get tricky. The idea, in a nutshell, is to break the code into its call graph, and make sure that in each cycle we have a command that withdraws enough gas (technically, “enough” here means the amount of gas that covers the worst case cost until we hit the next withdraw gas instruction). This way, the compiler only determines costs of “static” parts of the program, and during runtime the transaction will request more gas depending on the actual execution path. For more details on this, see "not stopping at the halting problem".

So far, the mechanism of determining those costs was based on solving a linear program, which is slow and prone to being unstable (e.g. the solution may depend on the LP library that is used). In v2.4.0, we’re replacing this with a different algorithm that determines the costs in linear time. The new approach is more natural in a sense, and only involves scanning the program’s graph in topological order, determining costs along the way. Moving to a simpler solution means that we’re much less likely to encounter esoteric compilation errors related to the pricing stage.

No more #[available_gas(123456)] in testing!

Cairo programs dynamically count gas. The remaining gas amount changes whenever we hit a gas related instruction that was placed by the compiler (see the previous section for more details). Until now, every test had to mention the maximum available gas for the test execution. In practice, many devs who were not concerned about gas usage in particular tests added some arbitrarily large numbers. As of v2.4.0, specifying available gas in tests is no longer mandatory. When not specified, then 4294967295 (u32::MAX) is assumed, which corresponds to ~40M cairo steps (100 gas units = 1 Cairo step).

Note that this gas counting mechanism is still not used in production and may undergo changes. For accurate cost estimates on the actual network, use the starknet_estimateFee endpoint in Starknet’s json-rpc.

Ahoy,
Thank you for the detailed notes!

Is there a plan to add slices to ByteArray?
I would like to be able to do:

let some_ba = "Ahoy Caironaut!";
let sub_ba = some_ba[5,10];
assert!(sub_ba == "Cairo");