greged93 / cairo_native

A compiler to convert Cairo's intermediate representation "Sierra" code to MLIR.

Home Page:https://lambdaclass.github.io/cairo_native/cairo_native

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

⚡ Cairo Native ⚡

A compiler to convert Cairo's intermediate representation "Sierra" code
to machine code via MLIR and LLVM.

Report Bug · Request Feature

Telegram Chat rust codecov license pr-welcome

To get started on how to setup and run cairo-native check the getting started section.

To read more in-depth documentation, visit this page.

Implemented Library Functions

Cairo Native works by leveraging the intermediate representation of Cairo called Sierra. Sierra uses a list of builtin functions that implement the language functionality, those are called library functions, short: libfuncs. Basically every statement in a sierra program is a call to a libfunc, thus they are the core of Cairo Native progress towards feature parity.

This is a list of the current progress implementing each libfunc.

Implemented libfuncs (click to open)
  1. alloc_local
  2. array_append
  3. array_get
  4. array_len
  5. array_new
  6. array_pop_front_consume
  7. array_pop_front
  8. array_slice
  9. array_snapshot_pop_back
  10. array_snapshot_pop_front
  11. bitwise
  12. bool_and_impl
  13. bool_not_impl
  14. bool_or_impl
  15. bool_to_felt252
  16. bool_xor_impl
  17. branch_align
  18. bytes31_const
  19. bytes31_to_felt252
  20. bytes31_try_from_felt252
  21. call_contract_syscall (StarkNet)
  22. class_hash_try_from_felt252 (StarkNet)
  23. contract_address_const (StarkNet)
  24. contract_address_to_felt252 (StarkNet)
  25. contract_address_try_from_felt252 (StarkNet)
  26. deploy_syscall (StarkNet)
  27. disable_ap_tracking
  28. downcast
  29. drop (3)
  30. dup (3)
  31. ec_neg
  32. ec_point_from_x_nz
  33. ec_point_is_zero
  34. ec_point_try_new_nz
  35. ec_point_unwrap
  36. ec_point_zero
  37. ec_state_add_mul
  38. ec_state_add
  39. ec_state_init
  40. ec_state_try_finalize_nz
  41. emit_event_syscall (StarkNet)
  42. enable_ap_tracking
  43. enum_init
  44. enum_match
  45. felt252_add_const (4)
  46. felt252_add
  47. felt252_const
  48. felt252_dict_entry_finalize
  49. felt252_dict_entry_get
  50. felt252_dict_new
  51. felt252_dict_squash
  52. felt252_div_const (4)
  53. felt252_div (4)
  54. felt252_is_zero
  55. felt252_mul_const (4)
  56. felt252_mul
  57. felt252_sub_const (4)
  58. felt252_sub
  59. finalize_locals
  60. function_call
  61. get_block_hash_syscall (StarkNet)
  62. get_builtin_costs (5)
  63. get_execution_info_syscall (StarkNet)
  64. hades_permutation
  65. into_box (2)
  66. jump
  67. keccak_syscall (StarkNet)
  68. library_call_syscall (StarkNet)
  69. match_nullable
  70. null
  71. nullable_from_box
  72. pedersen
  73. print
  74. rename
  75. replace_class_syscall (StarkNet)
  76. revoke_ap_tracking (1)
  77. send_message_to_l1_syscall (StarkNet)
  78. snapshot_take (6)
  79. storage_address_from_base_and_offset (StarkNet)
  80. storage_address_from_base (StarkNet)
  81. storage_address_to_felt252 (StarkNet)
  82. storage_address_try_from_felt252 (StarkNet)
  83. storage_base_address_const (StarkNet)
  84. storage_base_address_from_felt252 (StarkNet)
  85. storage_read_syscall (StarkNet)
  86. storage_write_syscall (StarkNet)
  87. store_local
  88. store_temp
  89. struct_construct
  90. struct_deconstruct
  91. u128_byte_reverse
  92. u128_const
  93. u128_eq
  94. u128_guarantee_mul
  95. u128_is_zero
  96. u128_mul_guarantee_verify
  97. u128_overflowing_add
  98. u128_overflowing_sub
  99. u128_safe_divmod
  100. u128_sqrt
  101. u128_to_felt252
  102. u128s_from_felt252
  103. u16_const
  104. u16_eq
  105. u16_is_zero
  106. u16_overflowing_add
  107. u16_overflowing_sub
  108. u16_safe_divmod
  109. u16_sqrt
  110. u16_to_felt252
  111. u16_try_from_felt252
  112. u16_wide_mul
  113. u256_is_zero
  114. u256_safe_divmod
  115. u256_sqrt
  116. u32_const
  117. u32_eq
  118. u32_is_zero
  119. u32_overflowing_add
  120. u32_overflowing_sub
  121. u32_safe_divmod
  122. u32_sqrt
  123. u32_to_felt252
  124. u32_try_from_felt252
  125. u32_wide_mul
  126. u64_const
  127. u64_eq
  128. u64_is_zero
  129. u64_overflowing_add
  130. u64_overflowing_sub
  131. u64_safe_divmod
  132. u64_sqrt
  133. u64_to_felt252
  134. u64_try_from_felt252
  135. u64_wide_mul
  136. u8_const
  137. u8_eq
  138. u8_is_zero
  139. u8_overflowing_add
  140. u8_overflowing_sub
  141. u8_safe_divmod
  142. u8_sqrt
  143. u8_to_felt252
  144. u8_try_from_felt252
  145. u8_wide_mul
  146. unbox (2)
  147. unwrap_non_zero
  148. upcast
  149. withdraw_gas_all (5)
  150. withdraw_gas (5)
Not yet implemented libfuncs (click to open)
  1. class_hash_to_felt252 (StarkNet)
  2. enum_snapshot_match
  3. get_available_gas
  4. pop_log (StarkNet, testing)
  5. redeposit_gas
  6. secp256k1_add_syscall (StarkNet)
  7. secp256k1_get_point_from_x_syscall (StarkNet)
  8. secp256k1_get_xy_syscall (StarkNet)
  9. secp256k1_mul_syscall (StarkNet)
  10. secp256k1_new_syscall (StarkNet)
  11. secp256r1_add_syscall (StarkNet)
  12. secp256r1_get_point_from_x_syscall (StarkNet)
  13. secp256r1_get_xy_syscall (StarkNet)
  14. secp256r1_mul_syscall (StarkNet)
  15. secp256r1_new_syscall (StarkNet)
  16. set_account_contract_address (StarkNet, testing)
  17. set_block_number (StarkNet, testing)
  18. set_block_timestamp (StarkNet, testing)
  19. set_caller_address (StarkNet, testing)
  20. set_chain_id (StarkNet, testing)
  21. set_contract_address (StarkNet, testing)
  22. set_max_fee (StarkNet, testing)
  23. set_nonce (StarkNet, testing)
  24. set_sequencer_address (StarkNet, testing)
  25. set_signature (StarkNet, testing)
  26. set_transaction_hash (StarkNet, testing)
  27. set_version (StarkNet, testing)
  28. struct_snapshot_deconstruct
  29. i128_diff
  30. i16_diff
  31. i32_diff
  32. i64_diff
  33. i8_diff

Footnotes on the libfuncs list:

  1. It is implemented but we're not sure if it has some stuff we don't know of.
  2. It is implemented but we're still debating whether it should be a Rust-like Box<T> or if it's fine treating it like another variable.
  3. It is implemented but side-effects are not yet handled (ex. array cloning/dropping).
  4. Not supported by the Cairo to Sierra compiler.
  5. Implemented with a dummy. It doesn't do anything yet.
  6. It is implemented but we're not handling potential issues like lifetimes yet.

Getting Started

Dependencies

  • Linux or macOS (aarch64 included) only for now
  • LLVM 17 with MLIR: On debian you can use apt.llvm.org, on macOS you can use brew
  • Nightly Rust
  • Git

Setup

This step applies to all operating systems.

Run the following make target to install the dependencies (both Linux and macOS):

make deps

Linux

Since Linux distributions change widely, you need to install LLVM 17 via your package manager, compile it or check if the current release has a Linux binary.

If you are on Debian/Ubuntu, check out the repository https://apt.llvm.org/ Then you can install with:

sudo apt-get install llvm-17 llvm-17-dev llvm-17-runtime clang-17 clang-tools-17 lld-17 libpolly-17-dev libmlir-17-dev mlir-17-tools

If you decide to build from source, here are some indications:

Install LLVM from source instructions
# Go to https://github.com/llvm/llvm-project/releases
# Download the latest LLVM 17 release:
# The blob to download is called llvm-project-17.x.x.src.tar.xz

# For example
wget https://github.com/llvm/llvm-project/releases/download/llvmorg-17.0.3/llvm-project-17.0.3.src.tar.xz
tar xf llvm-project-17.0.3.src.tar.xz

cd llvm-project-17.0.3.src.tar
mkdir build
cd build

# The following cmake command configures the build to be installed to /opt/llvm-17
cmake -G Ninja ../llvm \
   -DLLVM_ENABLE_PROJECTS="mlir;clang;clang-tools-extra;lld;polly" \
   -DLLVM_BUILD_EXAMPLES=OFF \
   -DLLVM_TARGETS_TO_BUILD="Native" \
   -DCMAKE_INSTALL_PREFIX=/opt/llvm-17 \
   -DCMAKE_BUILD_TYPE=RelWithDebInfo \
   -DLLVM_PARALLEL_LINK_JOBS=4 \
   -DLLVM_ENABLE_BINDINGS=OFF \
   -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DLLVM_ENABLE_LLD=ON \
   -DLLVM_ENABLE_ASSERTIONS=OFF

ninja install

Setup a environment variable called MLIR_SYS_170_PREFIX, LLVM_SYS_170_PREFIX and TABLEGEN_170_PREFIX pointing to the llvm directory:

# For Debian/Ubuntu using the repository, the path will be /usr/lib/llvm-17
export MLIR_SYS_170_PREFIX=/usr/lib/llvm-17
export LLVM_SYS_170_PREFIX=/usr/lib/llvm-17
export TABLEGEN_170_PREFIX=/usr/lib/llvm-17

Run the deps target to install the other dependencies such as the cairo compiler (for tests, benchmarks).

make deps

MacOS

The makefile deps target (which you should have ran before) installs LLVM 17 with brew for you, afterwards you need to execute the env-macos.sh script to setup the needed environment variables.

source env-macos.sh

Make commands:

Running make by itself will list available targets.

  • Install the necessary dependencies (on Linux, you need to get LLVM 17 manually):
make deps
  • Build a release version:
make build

Or with your native CPU Architecture for even more performance (usually):

make build-native
  • Install the cairo-native-dump and cairo-native-run commands:
make install
  • Build a optimized development version:
make build-dev
  • View and open the docs:
make doc-open
  • Run the tests:
make test
  • Generate coverage:
make coverage
  • Run clippy and format checks:
make check

Command Line Interface

cairo-native-dump:

Usage: cairo-native-dump [OPTIONS] <INPUT>

Arguments:
  <INPUT>

Options:
  -o, --output <OUTPUT>  [default: -]
  -h, --help             Print help

cairo-native-run:

This tool allows to run programs using the JIT engine, like the cairo-run tool, the parameters can only be felt values.

echo '1' | cairo-native-run 'program.cairo' 'program::program::main' --inputs - --outputs -

Usage: cairo-native-run [OPTIONS] <INPUT> <ENTRY_POINT>

Arguments:
  <INPUT>
  <ENTRY_POINT>

Options:
  -i, --inputs <INPUTS>
  -o, --outputs <OUTPUTS>
  -p, --print-outputs
  -h, --help               Print help

API usage example

This is a usage example using the API for an easy Cairo program that requires the least setup to get running. It allows you to compile and execute a program using the JIT.

Example code to run a program:

use starknet_types_core::felt::Felt;
use cairo_native::context::NativeContext;
use cairo_native::executor::NativeExecutor;
use cairo_native::values::JitValue;
use std::path::Path;

fn main() {
    let program_path = Path::new("programs/examples/hello.cairo");
    // Compile the cairo program to sierra.
    let sierra_program = cairo_native::utils::cairo_to_sierra(program_path);

    // Instantiate a Cairo Native MLIR context. This data structure is responsible for the MLIR
    // initialization and compilation of sierra programs into a MLIR module.
    let native_context = NativeContext::new();

    // Compile the sierra program into a MLIR module.
    let native_program = native_context.compile(&sierra_program).unwrap();

    // The parameters of the entry point.
    let params = &[JitValue::Felt252(Felt::from_bytes_be_slice(b"user"))];

    // Find the entry point id by its name.
    let entry_point = "hello::hello::greet";
    let entry_point_id = cairo_native::utils::find_function_id(&sierra_program, entry_point);

    // Instantiate the executor.
    let native_executor = NativeExecutor::new(native_program);

    // Execute the program.
    let result = native_executor
        .execute(entry_point_id, params, None)
        .unwrap();

    println!("Cairo program was compiled and executed successfully.");
    println!("{:?}", result);
}

Example code to run a Starknet contract:

use starknet_types_core::felt::Felt;
use cairo_lang_compiler::CompilerConfig;
use cairo_lang_starknet::contract_class::compile_path;
use cairo_native::context::NativeContext;
use cairo_native::executor::NativeExecutor;
use cairo_native::utils::find_entry_point_by_idx;
use cairo_native::values::JitValue;
use cairo_native::{
    metadata::syscall_handler::SyscallHandlerMeta,
    starknet::{BlockInfo, ExecutionInfo, StarkNetSyscallHandler, SyscallResult, TxInfo, U256},
};
use std::path::Path;

/// To run a starknet contract, we need to use a syscall handler, here we show how to implement one (at the end).
#[derive(Debug)]
struct SyscallHandler;

fn main() {
    let path = Path::new("programs/examples/hello_starknet.cairo");

    let contract = compile_path(
        path,
        None,
        CompilerConfig {
            replace_ids: true,
            ..Default::default()
        },
    )
    .unwrap();

    let entry_point = contract.entry_points_by_type.constructor.get(0).unwrap();
    let sierra_program = contract.extract_sierra_program().unwrap();

    let native_context = NativeContext::new();

    let mut native_program = native_context.compile(&sierra_program).unwrap();
    native_program
        .insert_metadata(SyscallHandlerMeta::new(&mut SyscallHandler))
        .unwrap();

    // Call the echo function from the contract using the generated wrapper.
    let entry_point_fn =
        find_entry_point_by_idx(&sierra_program, entry_point.function_idx).unwrap();

    let fn_id = &entry_point_fn.id;

    let native_executor = NativeExecutor::new(native_program);

    let result = native_executor
        .execute_contract(
            fn_id,
            // The calldata
            &[JitValue::Felt252(Felt::from(1))],
            u64::MAX.into(),
        )
        .expect("failed to execute the given contract");

    println!();
    println!("Cairo program was compiled and executed successfully.");
    println!("{result:#?}");
}

// Implement an example syscall handler.
impl StarkNetSyscallHandler for SyscallHandler {
    fn get_block_hash(
        &mut self,
        block_number: u64,
        _gas: &mut u128,
    ) -> SyscallResult<Felt> {
        println!("Called `get_block_hash({block_number})` from MLIR.");
        Ok(Felt::from_bytes_be_slice(b"get_block_hash ok"))
    }

    fn get_execution_info(
        &mut self,
        _gas: &mut u128,
    ) -> SyscallResult<cairo_native::starknet::ExecutionInfo> {
        println!("Called `get_execution_info()` from MLIR.");
        Ok(ExecutionInfo {
            block_info: BlockInfo {
                block_number: 1234,
                block_timestamp: 2345,
                sequencer_address: 3456.into(),
            },
            tx_info: TxInfo {
                version: 4567.into(),
                account_contract_address: 5678.into(),
                max_fee: 6789,
                signature: vec![1248.into(), 2486.into()],
                transaction_hash: 9876.into(),
                chain_id: 8765.into(),
                nonce: 7654.into(),
            },
            caller_address: 6543.into(),
            contract_address: 5432.into(),
            entry_point_selector: 4321.into(),
        })
    }

    fn deploy(
        &mut self,
        class_hash: Felt,
        contract_address_salt: Felt,
        calldata: &[Felt],
        deploy_from_zero: bool,
        _gas: &mut u128,
    ) -> SyscallResult<(Felt, Vec<Felt>)> {
        println!("Called `deploy({class_hash}, {contract_address_salt}, {calldata:?}, {deploy_from_zero})` from MLIR.");
        Ok((
            class_hash + contract_address_salt,
            calldata.iter().map(|x| x + &Felt::from(1)).collect(),
        ))
    }

    fn replace_class(
        &mut self,
        class_hash: Felt,
        _gas: &mut u128,
    ) -> SyscallResult<()> {
        println!("Called `replace_class({class_hash})` from MLIR.");
        Ok(())
    }

    fn library_call(
        &mut self,
        class_hash: Felt,
        function_selector: Felt,
        calldata: &[Felt],
        _gas: &mut u128,
    ) -> SyscallResult<Vec<Felt>> {
        println!(
            "Called `library_call({class_hash}, {function_selector}, {calldata:?})` from MLIR."
        );
        Ok(calldata.iter().map(|x| x * Felt::from(3)).collect())
    }

    fn call_contract(
        &mut self,
        address: Felt,
        entry_point_selector: Felt,
        calldata: &[Felt],
        _gas: &mut u128,
    ) -> SyscallResult<Vec<Felt>> {
        println!(
            "Called `call_contract({address}, {entry_point_selector}, {calldata:?})` from MLIR."
        );
        Ok(calldata.iter().map(|x| x * Felt::from(3)).collect())
    }

    fn storage_read(
        &mut self,
        address_domain: u32,
        address: Felt,
        _gas: &mut u128,
    ) -> SyscallResult<Felt> {
        println!("Called `storage_read({address_domain}, {address})` from MLIR.");
        Ok(address * Felt::from(3))
    }

    fn storage_write(
        &mut self,
        address_domain: u32,
        address: Felt,
        value: Felt,
        _gas: &mut u128,
    ) -> SyscallResult<()> {
        println!("Called `storage_write({address_domain}, {address}, {value})` from MLIR.");
        Ok(())
    }

    fn emit_event(
        &mut self,
        keys: &[Felt],
        data: &[Felt],
        _gas: &mut u128,
    ) -> SyscallResult<()> {
        println!("Called `emit_event({keys:?}, {data:?})` from MLIR.");
        Ok(())
    }

    fn send_message_to_l1(
        &mut self,
        to_address: Felt,
        payload: &[Felt],
        _gas: &mut u128,
    ) -> SyscallResult<()> {
        println!("Called `send_message_to_l1({to_address}, {payload:?})` from MLIR.");
        Ok(())
    }

    fn keccak(
        &mut self,
        input: &[u64],
        _gas: &mut u128,
    ) -> SyscallResult<cairo_native::starknet::U256> {
        println!("Called `keccak({input:?})` from MLIR.");
        Ok(U256(Felt::from(1234567890).to_le_bytes()))
    }

    /*
    ... more code here, check out the full example in examples/starknet.rsd
    */
}

For more examples, check out the examples/ directory.

Benchmarking

Requirements

You need to setup some environment variables:

$MLIR_SYS_170_PREFIX=/path/to/llvm17  # Required for non-standard LLVM install locations.
$LLVM_SYS_170_PREFIX=/path/to/llvm17  # Required for non-standard LLVM install locations.
$TABLEGEN_170_PREFIX=/path/to/llvm17  # Required for non-standard LLVM install locations.
make bench

The bench target will run the ./scripts/bench-hyperfine.sh script. This script runs hyperfine commands to compare the execution time of programs in the ./programs/benches/ folder. Each program is compiled and executed via the execution engine with the cairo-native-run command and via the cairo-vm with the cairo-run command provided by the cairo codebase. The cairo-run command should be available in the $PATH and ideally compiled with cargo build --release. If you want the benchmarks to run using a specific build, or the cairo-run commands conflicts with something (e.g. the cairo-svg package binaries in macos) then the command to run cairo-run with a full path can be specified with the $CAIRO_RUN environment variable.

From MLIR to native binary

# to mlir with llvm dialect
sierra2mlir program.sierra -o program.mlir

# translate all dialects to the llvm dialect
"$MLIR_SYS_170_PREFIX/bin/mlir-opt" \
        --canonicalize \
        --convert-scf-to-cf \
        --canonicalize \
        --cse \
        --expand-strided-metadata \
        --finalize-memref-to-llvm \
        --convert-func-to-llvm \
        --convert-index-to-llvm \
        --reconcile-unrealized-casts \
        "program.mlir" \
        -o "program-llvm.mlir"

# translate mlir to llvm-ir
"$MLIR_SYS_170_PREFIX"/bin/mlir-translate --mlir-to-llvmir program-llvm.mlir -o program.ll

# compile natively
"$MLIR_SYS_170_PREFIX"/bin/clang program.ll -Wno-override-module \
    -L "$MLIR_SYS_170_PREFIX"/lib -L"./target/release/" \
    -lsierra2mlir_utils -lmlir_c_runner_utils \
    -Wl,-rpath "$MLIR_SYS_170_PREFIX"/lib \
    -Wl,-rpath ./target/release/ \
    -o program

./program

About

A compiler to convert Cairo's intermediate representation "Sierra" code to MLIR.

https://lambdaclass.github.io/cairo_native/cairo_native

License:Apache License 2.0


Languages

Language:Rust 91.8%Language:Cairo 6.9%Language:Assembly 0.4%Language:Shell 0.3%Language:Makefile 0.3%Language:C 0.1%Language:Dockerfile 0.1%Language:C++ 0.1%