Reading Sierra: Starknet's secret sauce for Cairo 1.0

Peteris Erins
Yagi Finance
Published in
4 min readMar 7, 2023

--

The best Solidity developers can program in Yul (an intermediate language used in Solidity code compilation) and understand the EVM. They do this because it helps them understand security vulnerabilities and optimize their code to make it efficient. They also do this because it's fun. At an extreme, OpenSea's Seaport protocol was written with significant amounts of assembly code.

Starknet also has an intermediate language called Sierra, built as a stable layer to allow for rapid iteration and innovation at the Cairo 1.0 language level. While not absolutely essential to write efficient programs (Starknet contracts written in high-level Cairo will be orders of magnitude more efficient than hand-optimized Solidity contracts), reading Sierra code can help us get a deeper understanding of the underlying type system, security risks and improving efficiency where it matters (e.g., reducing the storage footprint or execution steps in highly recursive functions).

For Cairo 0.x programmers, Sierra is the bridge between our past and the future, a familiar blueprint guiding our craft. Let's take a look.

If you'd like to follow along, you can generate readable Sierra output for your contracts by using the make sierra command in the Minimal Cairo 1.0 Template. This effectively runs cairo-compile . -r on your code. While Sierra is reported as stabilized, the present examples are accurate as of March 2023.

Starting from the bottom

The natural place to start is with a function that does nothing:

A minimal Cairo function

The Sierra output is organized into 4 separate sections always presented in the same order. First come the type declarations. Next, declarations of built-in library functions that will be used. Then the sequence of statements and finally the declared Cairo functions.

Sierra output for do_nothing

A couple of observations:

  • The Unit type () is a special case of an empty struct and is used as a default return type for procedures (functions without a declared return type)
  • New temporary variables are created and indexed using square brackets ( [0] and [1] )
  • Statements have the following form <libfunc>(<inputs>) -> (<outputs>); , i.e., we don't see the more traditional [0] = struct_construct<Unit>();
  • Code blocks are separate from Cairo function declarations. A function is tied to a specific block of code by starting at a dedicated statement index location (note the do_nothing@0 which indicates that the function begins at the first statement).

Types & Code generation

Sierra output can be used to understand how built-in types are used in compilation. Consider this simple function to add two felt values:

A function that adds two felts
Sierra output for add

The output above is simple enough, showing us how function arguments become unnamed variables in statements.

Let's try a more complex type like u128 :

A function that adds two u128 values
Truncated Sierra output for add_128

In this case, the output is much larger given that u128 is not a primitive type. This is reflected well in the function declarations:

  • Our add function now has a phantom argument with a type of RangeCheck . This is a note to include the ZK circuit to help with felt comparisons, which is needed to perform addition. A welcome change from Cairo 0.x where built-ins had to be explicitly provided
  • We see that some additional functions were generated in this case. These functions were included based on traits defined in the Cairo core library ( corelib). In short, any Cairo code needed for compilation will be present in the final Sierra output.
U128Add implementation in corelib

Control Flow

To study how control flow is represented, we can use a recursive function to compute the factorial:

A recursive function computing n!
Sierra statements and function declarations for factorial function

In line 22, a function_call libfunc is used to make the recursive call. We also see the jump construct in line 13. The jump label is the index of the statement so jump() { 28() }; would take execution to row 29.

Learning more

I hope that this has been a useful teaser on how to read Sierra. Cairo smart contract developers and auditors alike shouldn't hesitate to jump down into Sierra to understand how their contracts work. There is a lot more going on behind the scenes, but Sierra strikes a good balance between readability and insight.

--

--

Peteris Erins
Yagi Finance

Founder @auditless Prev @mckinsey @twitter @google · @p_e · peteriserins.com