Cairo v2.1.0
- TL;Dr
- StorageAccess trait refactored and renamed to Store
- Packing
- Small tuples and enums in Storage
- The
array!
macro - Test runner improvements
- Enums - empty variant support
- Bitwise operations for unsigned integers
- Keccak support
- Secp-256k1 support
- PartialEq for small Tuples
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;