cairo-vm-go
cairo-vm-go copied to clipboard
Starknet contracts execution and blockifier-like API discussion
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 aStarkHash
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:
-
get_compiled_contract_class
is executed via the state accessor. - The contract class (i.e.
ContractClassV1Inner
) contains lists of entry points mapped by their type. - The list of the entry points is iterated until the entry point's selector matches the call's selector
- The matching
EntryPointV1
object'soffset
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.