Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Edited Contract migration guide #674

Closed
wants to merge 13 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
[id="sierra"]
= Cairo 1.0 and Sierra

Up until xref:documentation:starknet_versions:upcoming_versions.adoc[Starknet Alpha v0.11.0] users would write contracts in Cairo 0 and compile them locally to Cairo assembly (or Casm for short).
Next, the user would submit the compilation output (the xref:documentation:architecture_and_concepts:Contracts/contract-classes.adoc[contract class]) to the Starknet sequencer via a `declare` transaction.

With Cairo 1.0, the contract class resulting from xref:documentation:architecture_and_concepts:Contracts/class-hash.adoc#cairo1_class[compiling Cairo 1.0] does not include Casm. Instead of Casm, it includes instructions in an intermediate representation called Sierra (Safe Intermediate Representation).
This new contract class is then compiled by the sequencer, via the Sierra → Casm compiler, to generate the Cairo assembly associated with this class.

== Why do we need Casm?

Starknet is a validity rollup, which means that the execution inside every block needs to be proven, and this is where STARKs come in handy.
However, STARK proofs can address statements that are formulated in the language of polynomial
constraints, and have no knowledge of smart contract execution.
To overcome this gap, we developed link:https://github.com/starknet-io/starknet-stack-resources/blob/main/Cairo/Cairo%20%E2%80%93%20a%20Turing-complete%20STARK-friendly%20CPU%20architecture.pdf[Cairo].

Cairo instructions (which we referred to previously by Casm) are translated to polynomial constraints that enforce the correct execution of a program (according to the Cairo semantics defined in the paper).

Thanks to Cairo, we can formulate the statement "this Starknet block is valid" in a way that we can prove.
Note that we can only prove things about Casm. That is, regardless of what the user sends to the Starknet sequencer, what's proven is the correct Casm execution.

This means that we need a way to translate Sierra into Casm, and this is achieved with the Sierra
→
Casm compiler.


== Why do we need Sierra?

To understand why we chose to add an additional layer between the code that the user writes (Cairo 1.0) and the code that is being proven (Casm),
we need to consider more components in the system, and the limitations of Cairo.

=== Reverted transactions, unsatisfiable AIRs, and DOS attacks

A crucial property of every decentralized L2 is that the sequencers are guaranteed to be compensated for work they do.
The notion of reverted transactions is a good example: even if the user's transaction failed mid execution, the sequencer should be able to include it in a block and charge execution fees up to the point of failure.

If the sequencer cannot charge for such transactions, then sending transactions that will eventually fail (after a lot of computation steps) is an obvious DOS attack on the sequencer.
The sequencer cannot look at a transaction and conclude that it would fail without actually doing the work (this is equivalent to solving the halting problem).


The obvious solution to the above predicament is to include such transactions in the block, similar to Ethereum. However, this may not be as simple to do in a validity rollup.
With Cairo 0, there is no separating layer between user code and what is being proven.

This means that users can write code which is unprovable in some cases. In fact, such code is very easy to write, e.g. `assert 0=1` is a valid
Cairo instruction that cannot be proven, as it translates to polynomial constraints that are not satisfiable. Any Casm execution that contains this instruction cannot be proven.
Sierra is the layer between user code and the provable statement, that allows us to make sure all transactions are eventually provable.

=== Safe Casm

The method by which Sierra guarantees that user code is always provable is by compiling Sierra instructions to a subset of Casm, which we call "safe Casm".
The important property that we require from safe Casm is being provable for all inputs. A canonical example for safe Casm is using `if/else` instructions instead of `assert`, that is, making sure all failures are
graceful.

To better understand the considerations that go into designing the Sierra → Casm compiler,
consider the `find_element` function from the common library of Cairo 0:

[source,cairo]
----
func find_element{range_check_ptr}(array_ptr: felt*, elm_size, n_elms, key) -> (elm_ptr: felt*) {
alloc_locals;
local index;
%{
...
%}
assert_nn_le(a=index, b=n_elms - 1);
tempvar elm_ptr = array_ptr + elm_size * index;
assert [elm_ptr] = key;
return (elm_ptr=elm_ptr);
}
----

[NOTE]
====
Below we abuse the "Casm" notation by not distinguishing Cairo 0 from Casm and referring to the
above as Casm (while we actually refer to the compilation result of the above).
====

For brevity, we have omitted the hint in the above snippet, but it's clear that this function can only execute correctly if the requested element exists in the array (otherwise it would fail for every possible hint -
there is nothing we can substitute `index` for, that makes the following lines run successfully).

Such Casm cannot be generated by the Sierra→Casm compiler.
Furthermore, simply replacing the assertion with an if/else statement doesn't do, as this results in non-deterministic execution. That is, for the same input, different hint values can yield different results.
A malicious prover can use this freedom to harm the user - in this example, they are able to make it seem as if an element isn't part of the array, even though it actually is.

The safe Casm for finding an element in an array behaves like the above snippet in the happy flow (element is there): an index is given in a hint, and we verify that the array at the hinted index contains the requested element.
However, in the unhappy flow (element isn't there), we *must* go over the entire array to verify this.

This was not the case in Cairo 0, as we were fine with certain paths not being provable (in the above snippet, the unhappy flow in which the element isn't in the array is never provable).

[NOTE]
====
Sierra's gas metering adds further complications to the above example. Even looking through the array to verify that the element isn't there may leave some flexibility to the prover.

If we take gas limitations into consideration, the user may have enough gas for the happy flow, but not for the unhappy one, making the execution stop mid-search, and allowing the prover to get away with lying about the element not being present.

The way we plan to handle this is by requiring the user to have enough gas for the unhappy flow before actually calling `find_element`.
====

=== Hints in Cairo 1.0

Smart contracts written with Cairo 1.0 cannot contain user defined hints. This is already true with Cairo 0 contracts (only whitelisted hints are accepted), but with Cairo 1.0 the hints in use are
determined by the Sierra → Casm compiler. Since this compilation is there to ensure that only
"safe" Casm is generated, there is no room for hints that are not generated by the compiler.

In the future, native Cairo 1.0 may contain hint syntax similar to Cairo 0, but it will not be available in Starknet smart contracts (link:https://medium.com/starkware/fractal-scaling-from-l2-to-l3-7fe238ecfb4f[L3s] on top of Starknet may make use of such functionality).
Note that this is currently not part of Starknet's roadmap.
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
= Contract syntax - migration guide

With the link:https://github.com/starkware-libs/cairo/releases/tag/v2.0.0-rc0[v2.0.0 release] of
the Cairo compiler, the Starknet contract syntax has evolved, affecting the organization of
functions, storage, and events.

For more information on the latest syntax changes, see the link:https://community.starknet.io/t/cairo-1-contract-syntax-is-evolving/94794[community forum post].

== New contract syntax - concrete steps for migrating

Given a contract written with the previous compiler version (v1.1.0), you can follow the steps below in order to make it compatible with the new syntax.

1 . Change the contract annotation from `#[contract]` to `#[starknet::contract]`.

For example:

[tabs]
====
old::
+
[source,rust]
----
#[contract]
mod CounterContract {
...
}
----
new::
+
[source,rust]
----
#[starknet::contract]
mod CounterContract {
...
}
----
====



2 . Annotate the `Storage` struct with the `#[storage]` attribute:

[tabs]
====
old::
+
[source,rust]
----
struct Storage {
counter: u128,
other_contract: IOtherContractDispatcher
}
----
new::
+
[source,rust]
----
#[storage]
struct Storage {
counter: u128,
other_contract: IOtherContractDispatcher
}
----
====

3 . Gather your contract’s external and view function signatures under a trait annotated with
`#[starknet::interface]` by completing the following:

* Add a generic parameter to the trait. In this example, the name `TContractState` is used as it
stands for the state of your contract.
* For view functions, add the `self: @TContractState` argument.
* For external functions, add the `ref self: TContractState` argument.
* Static functions that do not touch storage or emit events do not require an addition argument.

For example:

[tabs]
====
old::
+
[source,rust]
----
#[contract]
mod CounterContract {
#[external]
fn increase_counter(amount: u128) { ... }
#[external]
fn decrease_counter(amount: u128) { ... }
#[view]
fn get_counter() -> u128 { ... }
}
----

new::
+
[source,rust]
----
#[starknet::interface]
trait ICounterContract<TContractState> {
fn increase_counter(ref self: TContractState, amount: u128);
fn decrease_counter(ref self: TContractState, amount: u128);
fn get_counter(self: @TContractState) -> u128;
}

#[starknet::contract]
mod CounterContract {
...
}
----
====

4 . Add the external and view function bodies under an `impl` of the interface trait, and mark the
`impl` with the `[external(v0)]` attribute:

[tabs]
====
old::
+
[source,rust]
----
#[contract]
mod CounterContract {
#[external]
fn increase_counter(amount: u128) { ... }
#[external]
fn decrease_counter(amount: u128) { ... }
#[view]
fn get_counter() -> u128 { ... }
}
----

new::
+
[source,rust]
----
#[starknet::interface]
trait ICounterContract<TContractState> {
fn increase_counter(ref self: TContractState, amount: u128);
fn decrease_counter(ref self: TContractState, amount: u128);
fn get_counter(self: @TContractState) -> u128;
}

#[starknet::contract]
mod CounterContract {
#[external(v0)]
impl CounterContract of super::ICounterContract<ContractState> {
fn increase_counter(ref self: ContractState, amount: u128) { ... }
fn decrease_counter(ref self: ContractState, amount: u128) { ... }
fn get_counter(self: @ContractState) -> u128 { ... }
}
}
----
====

These attributes are responsible for generating the dispatcher type, used to call the contract.

5 . Replace the `#[abi]` attribute with `#[starknet::interface]`.

[TIP]
====
While it doesn't affect the generated code, adding to the trait a generic parameter `T` representing the contract's state,
and adding the `ref self: T` argument to external functions and `self: @T` argument for view
functions makes the implementation more complete.
====

For example:

[tabs]
====
old::
+
[source,rust]
----
#[abi]
trait IOtherContract {
fn decrease_allowed() -> bool;
}
----
new::
+
[source,rust]
----
#[starknet::interface]
trait IOtherContract<TContractState> {
fn decrease_allowed(self: @TContractState) -> bool;
}
----
====

6 . Modify storage accesses to happen through `ContractState` or `@ContractState`.

[NOTE]
====
No external functions in the contract that access storage also need to get it as an argument.
====

For example:

[tabs]
====
old::
+
[source,rust]
----
let current = counter::read();
----
new::
+
[source,rust]
----
let current = self.counter.read();
----
====

7 . Unify all the contract's events under the `Event` enum, and add a corresponding struct for every
variant.

[NOTE]
====
All the structs must derive the `Event` trait,
and each member type has to implement the `Serde` trait.
====

For example:

[tabs]
====
old::
+
[source,rust]
----
#[event]
fn counter_increased(amount: u128) {}
#[event]
fn counter_decreased(amount: u128) {}
----
new::
+
[source,rust]
----
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
CounterIncreased: CounterIncreased,
CounterDecreased: CounterDecreased
}

#[derive(Drop, starknet::Event)]
struct CounterIncreased {
amount: u128
}

#[derive(Drop, starknet::Event)]
struct CounterDecreased {
amount: u128
}
----
====

8 . Emit events via the `ContractState` type:

[tabs]
====
old::
+
[source,rust]
----
fn increase_counter(amount: u128) {
...
counter_increased(amount);
}
----
new::
+
[source,rust]
----
fn increase_counter(ref self: ContractState, amount: u128) {
...
self.emit(Event::CounterIncreased(CounterIncreased { amount }));
}
----
====
Loading
Loading