cairo-vm-go icon indicating copy to clipboard operation
cairo-vm-go copied to clipboard

Starknet contracts execution and blockifier-like API discussion

Open quasilyte opened this issue 11 months ago • 0 comments

This is a work-in-progress document. It may be extended further, or perhaps it will become redundant after some point (and will be replaced by a proper design document). Right now it's more of a TODO list.

To create an alternative for a Blockifier, a current state of a Blockifier needs to be analyzed in the context of its usage inside Juno.

Dependencies

The API surface is small, but since the respective documentation is lacking on the both sides, it's hard to get going without describing a high-level picture first.

Juno uses blockifier as a transaction execution component. Since Juno is written in Go and blockifier in Rust, some FFI interfaces are used to make it work. Juno repository contains a small amount of Rust code that implements the 2-function API for the Go side:

  • cairoVMCall
  • cairoVMExecute

These two functions are then called via CGo. These functions use blockifier inside. Blockifier symbols used from Juno (Rust side):

  • blockifier::block::BlockInfo
  • blockifier::block::BlockNumberHashPair
  • blockifier::block::GasPrices
  • blockifier::context::BlockContext
  • blockifier::context::ChainInfo
  • blockifier::context::FeeTokenAddresses
  • blockifier::execution::contract_class::ClassInfo
  • blockifier::execution::entry_point::CallEntryPoint
  • blockifier::execution::entry_point::CallType
  • blockifier::execution::entry_point::EntryPointExecutionContext
  • blockifier::state::cached_state::CachedState
  • blockifier::state::cached_state::GlobalContractCache
  • blockifier::state::state_api::State
  • blockifier::transaction::errors::TransactionExecutionError::ContractConstructorExecutionFailed
  • blockifier::transaction::errors::TransactionExecutionError::ExecutionError
  • blockifier::transaction::errors::TransactionExecutionError::ValidateTransactionError
  • blockifier::transaction::objects::DeprecatedTransactionInfo
  • blockifier::transaction::objects::HasRelatedFeeType
  • blockifier::transaction::objects::TransactionInfo
  • blockifier::transaction::transaction_execution::Transaction
  • blockifier::transaction::transactions::ExecutableTransaction
  • blockifier::versioned_constants::VersionedConstants
  • blockifier::block::pre_process_block
  • blockifier::fee::fee_utils::calculate_tx_fee

It also uses these symbols from the cairo_vm crate:

  • cairo_vm::vm::runners::cairo_runner::ExecutionResources

If blockifier's alternative is written in Go, we would still have to write some glue code, but it would be a normal Go code. The existing Go side would not change much (unless we want it too) apart from getting rid of CGo things. It could use the same-ish cairoVMCall and cairoVMExecute functions.

cairoVMExecute creates transaction objects (e.g. InvokeTransaction, etc.) and executes them. The state changes are commited on success.

cairoVMCall is a more simple function of the two. It basically prepares a blockifier's entry point and executes it. cairoVMCall still creates a transaction object, but it's never commited in the end.

The VM invocation is here: transaction/entry point -> CallEntryPoint.execute -> execute_entry_point_call -> run_entry_point -> VM.run_from_entrypoint

Contract Execution

Entry Point

The contract method's invocation would require an entry point specifier.

pub struct EntryPointV1 {
    pub selector: EntryPointSelector,
    pub offset: EntryPointOffset,
    pub builtins: Vec<String>,
}

EntryPointSelector and EntryPointOffset are types from the starknet_api crate (EntryPointSelector is a StarkHash which in turn is felt).

This VM should provide an entry point runner (e.g. something like https://github.com/FuzzingLabs/cairo-rs/blob/48af153240392992f18a09e969bae6518eec9639/vm/src/vm/runners/cairo_runner.rs#L952).

Right now we always assume the main to be our entry point.

This is how Blockifier constructs EntryPointV1 object:

  1. get_compiled_contract_class is executed via the state accessor.
  2. The contract class (i.e. ContractClassV1Inner) contains lists of entry points mapped by their type.
  3. The list of the entry points is iterated until the entry point's selector matches the call's selector
  4. The matching EntryPointV1 object's offset field is a starting PC for the VM
pub struct ContractClassV1Inner {
    pub program: Program,
    pub entry_points_by_type: HashMap<EntryPointType, Vec<EntryPointV1>>,
    pub hints: HashMap<String, Hint>,
    bytecode_segment_lengths: NestedIntList,
}

In other words, the VM needs to implement a PC-based entry point, not the symbol-based one (in other words, the input is an offset, not a function name).

The Blockifier's init/run code is quite a mess: you have different run methods, some of them run most of the init functions, some of them dont; sometimes you need to call several init-related functions before doing run stuff. An example: it might create a VM, call initialize_builtins and initialize_segments, then do a run_from_entrypoint call which in turn does initialize_vm. The run/init API looks to be all over the place.

Whether possible, we should make the configuration separated explicitly. There should be a simpler configurate & run stages. That would require a clean API from the VM package (this repository). This is hard to get right from the first try, but we should keep that in mind.

Syscalls

There are also several "syscalls" that need to be implemented via hints:

  • CallContract
  • DelegateCall
  • DelegateL1Handler
  • Deploy
  • EmitEvent
  • GetBlockHash
  • GetBlockNumber
  • GetBlockTimestamp
  • GetCallerAddress
  • GetContractAddress
  • GetExecutionInfo
  • GetSequencerAddress
  • GetTxInfo
  • GetTxSignature
  • Keccak
  • LibraryCall
  • LibraryCallL1Handler
  • ReplaceClass
  • Secp256k1Add
  • Secp256k1GetPointFromX
  • Secp256k1GetXy
  • Secp256k1Mul
  • Secp256k1New
  • Secp256r1Add
  • Secp256r1GetPointFromX
  • Secp256r1GetXy
  • Secp256r1Mul
  • Secp256r1New
  • SendMessageToL1
  • StorageRead
  • StorageWrite

They're called "deprecated syscalls" (why?); the Blockifier's code aliases it like SyscallSelector = DeprecatedSyscallSelector

The SyscallHintProcessor is used as a hint processor for the VM. It shall be implemented in a separate repository, but VM should export all necessary stuff to make an external implementation like that possible (we already export tons of things, perhaps no changes are needed).

State

In the context of the execution, the state contains the class hash (get_class_hash_at) and its code (get_compiled_contract_class) at the CallEntryPoint.storage_address.

The state is owned by Juno (see StateReader inside juno_state_reader.rs). All relevant methods like get_compiled_contract_class are there. The Rust code calls Go functions via FFI (see funcs like JunoStateGetCompiledClass in Go code).

The feature/sequencer Juno branch supports persistent state changes.

quasilyte avatar Mar 08 '24 07:03 quasilyte