diff --git a/README.md b/README.md index 5fc04037f..f59b06a40 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ This repository is a rewrite of always matches the one used by Kakarot, you can install Scarb [via asdf](https://docs.swmansion.com/scarb/download#install-via-asdf). -- Install [Bun](https://bun.sh/docs/installation) to run the JavaScipt scripts +- Install [Bun](https://bun.sh/docs/installation) to run the JavaScript scripts to compute the Starknet address. - [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the diff --git a/crates/contracts/src/kakarot_core/kakarot.cairo b/crates/contracts/src/kakarot_core/kakarot.cairo index cc9ae5960..5b7fc76b0 100644 --- a/crates/contracts/src/kakarot_core/kakarot.cairo +++ b/crates/contracts/src/kakarot_core/kakarot.cairo @@ -365,7 +365,7 @@ mod KakarotCore { } /// Maps an EVM address to a Starknet address - /// Triggerred when deployment of an EOA or CA is successful + /// Triggered when deployment of an EOA or CA is successful fn set_address_registry( ref self: ContractState, evm_address: EthAddress, account: StoredAccountType ) { diff --git a/crates/contracts/src/tests/test_kakarot_core.cairo b/crates/contracts/src/tests/test_kakarot_core.cairo index 4167715f2..25e00702a 100644 --- a/crates/contracts/src/tests/test_kakarot_core.cairo +++ b/crates/contracts/src/tests/test_kakarot_core.cairo @@ -94,7 +94,7 @@ fn test_kakarot_core_deploy_eoa() { let (native_token, kakarot_core) = contract_utils::setup_contracts_for_testing(); let eoa_starknet_address = kakarot_core.deploy_eoa(test_utils::evm_address()); // We drop the first event of Kakarot Core, as it is the initializer from Ownable, - // triggerred in the constructor + // triggered in the constructor contract_utils::drop_event(kakarot_core.contract_address); let event = contract_utils::pop_log::(kakarot_core.contract_address) @@ -405,7 +405,7 @@ fn test_eth_send_transaction_deploy_tx() { fn test_contract_account_class_hash() { let kakarot_core = contract_utils::deploy_kakarot_core(test_utils::native_token()); // We drop the first event of Kakarot Core, as it is the initializer from Ownable, - // triggerred in the constructor + // triggered in the constructor contract_utils::drop_event(kakarot_core.contract_address); let class_hash = kakarot_core.ca_class_hash(); @@ -429,7 +429,7 @@ fn test_contract_account_class_hash() { fn test_account_class_hash() { let kakarot_core = contract_utils::deploy_kakarot_core(test_utils::native_token()); // We drop the first event of Kakarot Core, as it is the initializer from Ownable, - // triggerred in the constructor + // triggered in the constructor contract_utils::drop_event(kakarot_core.contract_address); let class_hash = kakarot_core.account_class_hash(); @@ -455,7 +455,7 @@ fn test_account_class_hash() { fn test_eoa_class_hash() { let kakarot_core = contract_utils::deploy_kakarot_core(test_utils::native_token()); // We drop the first event of Kakarot Core, as it is the initializer from Ownable, - // triggerred in the constructor + // triggered in the constructor contract_utils::drop_event(kakarot_core.contract_address); let class_hash = kakarot_core.eoa_class_hash(); diff --git a/crates/evm/src/context.cairo b/crates/evm/src/context.cairo index b3fa79a92..7c7d8c37f 100644 --- a/crates/evm/src/context.cairo +++ b/crates/evm/src/context.cairo @@ -136,9 +136,6 @@ struct ExecutionContext { program_counter: u32, status: Status, call_ctx: Box, - destroyed_contracts: Array, - events: Array, - create_addresses: Array, // Return data of a child context. return_data: Span, parent_ctx: Nullable, @@ -185,9 +182,6 @@ impl ExecutionContextImpl of ExecutionContextTrait { program_counter: Default::default(), status: Default::default(), call_ctx: BoxTrait::new(call_ctx), - destroyed_contracts: Default::default(), - events: Default::default(), - create_addresses: Default::default(), return_data, parent_ctx, } @@ -224,21 +218,6 @@ impl ExecutionContextImpl of ExecutionContextTrait { (*self.call_ctx).unbox() } - #[inline(always)] - fn destroyed_contracts(self: @ExecutionContext) -> Span { - self.destroyed_contracts.span() - } - - #[inline(always)] - fn events(self: @ExecutionContext) -> Span { - self.events.span() - } - - #[inline(always)] - fn create_addresses(self: @ExecutionContext) -> Span { - self.create_addresses.span() - } - #[inline(always)] fn return_data(self: @ExecutionContext) -> Span { *self.return_data @@ -359,12 +338,6 @@ impl ExecutionContextImpl of ExecutionContextTrait { *self.program_counter } - - #[inline(always)] - fn append_event(ref self: ExecutionContext, event: Event) { - self.events.append(event); - } - fn origin(ref self: ExecutionContext) -> Address { if (self.is_root()) { return self.call_ctx().caller(); diff --git a/crates/evm/src/execution.cairo b/crates/evm/src/execution.cairo index 4e9b95265..59bb077ca 100644 --- a/crates/evm/src/execution.cairo +++ b/crates/evm/src/execution.cairo @@ -94,18 +94,7 @@ fn execute( let address = machine.address(); let status = machine.status(); let return_data = machine.return_data(); - let destroyed_contracts = machine.destroyed_contracts(); - let create_addresses = machine.create_addresses(); - let events = machine.events(); - ExecutionResult { - address, - status, - return_data, - destroyed_contracts, - create_addresses, - events, - state: machine.state, - } + ExecutionResult { address, status, return_data, state: machine.state } } fn reverted_with_err(mut machine: Machine, error: EVMError) -> ExecutionResult { @@ -113,10 +102,7 @@ fn reverted_with_err(mut machine: Machine, error: EVMError) -> ExecutionResult { ExecutionResult { address: machine.address(), status: Status::Reverted, - return_data, - destroyed_contracts: Default::default().span(), - create_addresses: Default::default().span(), - events: Default::default().span(), + return_data: Default::default().span(), state: machine.state, } } diff --git a/crates/evm/src/instructions/logging_operations.cairo b/crates/evm/src/instructions/logging_operations.cairo index 9f64a58da..e7946280b 100644 --- a/crates/evm/src/instructions/logging_operations.cairo +++ b/crates/evm/src/instructions/logging_operations.cairo @@ -48,6 +48,7 @@ mod internal { use evm::memory::MemoryTrait; use evm::model::Event; use evm::stack::StackTrait; + use evm::state::StateTrait; /// Store a new event in the dynamic context using topics /// popped from the stack and data from the memory. @@ -70,7 +71,7 @@ mod internal { self.memory.load_n(size, ref data, offset); let event: Event = Event { keys: topics, data }; - self.append_event(event); + self.state.add_event(event); Result::Ok(()) } diff --git a/crates/evm/src/instructions/sha3.cairo b/crates/evm/src/instructions/sha3.cairo index 665a38cfc..437f8c2e8 100644 --- a/crates/evm/src/instructions/sha3.cairo +++ b/crates/evm/src/instructions/sha3.cairo @@ -14,7 +14,7 @@ impl Sha3Impl of Sha3Trait { /// and push the hash result to the stack. /// /// # Inputs - /// * `offset` - The offset in memory where to read the datas + /// * `offset` - The offset in memory where to read the data /// * `size` - The amount of bytes to read /// /// # Specification: https://www.evm.codes/#20?fork=shanghai diff --git a/crates/evm/src/instructions/stop_and_arithmetic_operations.cairo b/crates/evm/src/instructions/stop_and_arithmetic_operations.cairo index 70328fcb5..03db2102e 100644 --- a/crates/evm/src/instructions/stop_and_arithmetic_operations.cairo +++ b/crates/evm/src/instructions/stop_and_arithmetic_operations.cairo @@ -54,7 +54,7 @@ impl StopAndArithmeticOperations of StopAndArithmeticOperationsTrait { fn exec_sub(ref self: Machine) -> Result<(), EVMError> { let popped = self.stack.pop_n(2)?; - // Compute the substraction + // Compute the subtraction let (result, _) = u256_overflow_sub(*popped[0], *popped[1]); self.stack.push(result) diff --git a/crates/evm/src/machine.cairo b/crates/evm/src/machine.cairo index 9822ce7fc..cd811fa38 100644 --- a/crates/evm/src/machine.cairo +++ b/crates/evm/src/machine.cairo @@ -194,30 +194,6 @@ impl MachineImpl of MachineTrait { Result::Ok(()) } - #[inline(always)] - fn destroyed_contracts(ref self: Machine) -> Span { - let current_execution_ctx = self.current_ctx.unbox(); - let destroyed_contracts = current_execution_ctx.destroyed_contracts.span(); - self.current_ctx = BoxTrait::new(current_execution_ctx); - destroyed_contracts - } - - #[inline(always)] - fn events(ref self: Machine) -> Span { - let current_execution_ctx = self.current_ctx.unbox(); - let events = current_execution_ctx.events.span(); - self.current_ctx = BoxTrait::new(current_execution_ctx); - events - } - - #[inline(always)] - fn create_addresses(ref self: Machine) -> Span { - let current_execution_ctx = self.current_ctx.unbox(); - let create_addresses = current_execution_ctx.create_addresses.span(); - self.current_ctx = BoxTrait::new(current_execution_ctx); - create_addresses - } - #[inline(always)] fn return_data(ref self: Machine) -> Span { let current_execution_ctx = self.current_ctx.unbox(); @@ -263,13 +239,6 @@ impl MachineImpl of MachineTrait { current_call_ctx.read_only() } - #[inline(always)] - fn append_event(ref self: Machine, event: Event) { - let mut current_execution_ctx = self.current_ctx.unbox(); - current_execution_ctx.append_event(event); - self.current_ctx = BoxTrait::new(current_execution_ctx); - } - #[inline(always)] fn gas_limit(ref self: Machine) -> u128 { let current_call_ctx = self.call_ctx(); @@ -345,7 +314,7 @@ impl MachineImpl of MachineTrait { } /// Sets the `return_data` field of the appropriate execution context, - /// taking into acount EVM specs: If the current context is the root + /// taking into account EVM specs: If the current context is the root /// context, sets the return_data field of the root context. If the current /// context is a subcontext, sets the return_data field of the parent. /// Should be called when returning from a context. diff --git a/crates/evm/src/model.cairo b/crates/evm/src/model.cairo index a263420f1..127ac9271 100644 --- a/crates/evm/src/model.cairo +++ b/crates/evm/src/model.cairo @@ -60,9 +60,6 @@ struct ExecutionResult { address: Address, status: Status, return_data: Span, - create_addresses: Span, - destroyed_contracts: Span, - events: Span, state: State, } diff --git a/crates/evm/src/model/account.cairo b/crates/evm/src/model/account.cairo index df12b1931..43e478b4d 100644 --- a/crates/evm/src/model/account.cairo +++ b/crates/evm/src/model/account.cairo @@ -231,7 +231,7 @@ impl AccountImpl of AccountTrait { initial_code, deploy_starknet_contract: !is_deployed )?; - //Storage is handled outside of the account and must be commited after all accounts are commited. + //Storage is handled outside of the account and must be committed after all accounts are committed. //TODO(bug) uncommenting this bugs, needs to be removed when fixed in the compiler // return Result::Ok(()); }; @@ -241,7 +241,7 @@ impl AccountImpl of AccountTrait { }; // If the account was not scheduled for deployment - then update it if it's deployed. - // Only CAs have components commited on starknet. + // Only CAs have components committed on starknet. if is_deployed && is_ca { if *self.selfdestruct { return ContractAccountTrait::selfdestruct(self); diff --git a/crates/evm/src/model/contract_account.cairo b/crates/evm/src/model/contract_account.cairo index 73560a7c6..3dab4b019 100644 --- a/crates/evm/src/model/contract_account.cairo +++ b/crates/evm/src/model/contract_account.cairo @@ -41,7 +41,7 @@ impl ContractAccountImpl of ContractAccountTrait { /// storing the contract bytecode and emitting a ContractAccountDeployed /// event. /// - /// `deploy` is only called when commiting a transaction. We already + /// `deploy` is only called when committing a transaction. We already /// checked that no account exists at this address prealably. /// # Arguments /// * `origin` - The EVM address of the transaction sender diff --git a/crates/evm/src/stack.cairo b/crates/evm/src/stack.cairo index 0acfecdac..5af0a0b9c 100644 --- a/crates/evm/src/stack.cairo +++ b/crates/evm/src/stack.cairo @@ -219,7 +219,7 @@ impl StackImpl of StackTrait { /// /// # Errors /// - /// If the index is greather than the stack length, returns with a StackUnderflow error. + /// If the index is greater than the stack length, returns with a StackUnderflow error. #[inline(always)] fn peek_at(ref self: Stack, index: usize) -> Result { if index >= self.len() { diff --git a/crates/evm/src/state.cairo b/crates/evm/src/state.cairo index 10bec9c3c..88f16deca 100644 --- a/crates/evm/src/state.cairo +++ b/crates/evm/src/state.cairo @@ -232,7 +232,7 @@ impl StateImpl of StateTrait { match maybe_entry { Option::Some((_, key, value)) => { return Result::Ok(value); }, Option::None => { - let account = AccountTrait::fetch_or_create(evm_address); + let account = self.get_account(evm_address); return account.read_storage(key); } } diff --git a/crates/evm/src/tests/test_execution_context.cairo b/crates/evm/src/tests/test_execution_context.cairo index 88689feaf..b90b830b6 100644 --- a/crates/evm/src/tests/test_execution_context.cairo +++ b/crates/evm/src/tests/test_execution_context.cairo @@ -63,9 +63,7 @@ fn test_execution_context_new() { let return_data: Array = ArrayTrait::new(); let address: Address = Default::default(); - let destroyed_contracts: Array = Default::default(); let events: Array = Default::default(); - let create_addresses: Array = Default::default(); let revert_contract_state: Felt252Dict = Default::default(); let reverted: bool = false; let read_only: bool = false; @@ -83,14 +81,6 @@ fn test_execution_context_new() { assert(execution_context.stopped() == stopped, 'wrong stopped'); assert(execution_context.return_data() == Default::default().span(), 'wrong return_data'); assert(execution_context.address() == address, 'wrong evm_address'); - assert( - execution_context.destroyed_contracts() == destroyed_contracts.span(), - 'wrong destroyed_contracts' - ); - assert(execution_context.events().len() == events.len(), 'wrong events'); - assert( - execution_context.create_addresses() == create_addresses.span(), 'wrong create_addresses' - ); assert(execution_context.reverted() == reverted, 'wrong reverted'); assert(execution_context.is_create() == false, 'wrong is_create'); } diff --git a/crates/evm/src/tests/test_instructions/test_logging_operations.cairo b/crates/evm/src/tests/test_instructions/test_logging_operations.cairo index 4eef47188..917356ba6 100644 --- a/crates/evm/src/tests/test_instructions/test_logging_operations.cairo +++ b/crates/evm/src/tests/test_instructions/test_logging_operations.cairo @@ -3,6 +3,7 @@ use evm::instructions::LoggingOperationsTrait; use evm::machine::{Machine, MachineTrait}; use evm::memory::MemoryTrait; use evm::stack::StackTrait; +use evm::state::StateTrait; use evm::tests::test_utils::{MachineBuilderTestTrait}; use integer::BoundedInt; use utils::helpers::u256_to_bytes_array; @@ -24,7 +25,7 @@ fn test_exec_log0() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack should be empty'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 1, 'context should have one event'); let event = events.pop_front().unwrap(); @@ -53,7 +54,7 @@ fn test_exec_log1() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack should be empty'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 1, 'context should have one event'); let event = events.pop_front().unwrap(); @@ -84,7 +85,7 @@ fn test_exec_log2() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack should be empty'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 1, 'context should have one event'); let event = events.pop_front().unwrap(); @@ -118,7 +119,7 @@ fn test_exec_log3() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack should be empty'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 1, 'context should have one event'); let event = events.pop_front().unwrap(); @@ -156,7 +157,7 @@ fn test_exec_log4() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack should be empty'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 1, 'context should have one event'); let event = events.pop_front().unwrap(); @@ -211,7 +212,7 @@ fn test_exec_log1_size_0_offset_0() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack should be empty'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 1, 'context should have one event'); let event = events.pop_front().unwrap(); @@ -293,7 +294,7 @@ fn test_exec_log_multiple_events() { assert(result.is_ok(), 'should have succeeded'); assert(machine.stack.len() == 0, 'stack size should be 0'); - let mut events = machine.events(); + let mut events = machine.state.events.contextual_logs; assert(events.len() == 2, 'context should have 2 events'); let event1 = events.pop_front().unwrap(); diff --git a/crates/evm/src/tests/test_instructions/test_memory_operations.cairo b/crates/evm/src/tests/test_instructions/test_memory_operations.cairo index 5b36dbe7b..7d95538d9 100644 --- a/crates/evm/src/tests/test_instructions/test_memory_operations.cairo +++ b/crates/evm/src/tests/test_instructions/test_memory_operations.cairo @@ -463,7 +463,7 @@ fn test_exec_jumpi_invalid_zero() { // Then let pc = machine.pc(); - // ideally we should assert that it incremented, but incrementing is done by `decode_and_execut` + // ideally we should assert that it incremented, but incrementing is done by `decode_and_execute` // so we can assume that will be done assert(pc == old_pc, 'PC should be same'); } @@ -610,7 +610,7 @@ fn test_exec_sstore_finalized() { machine.state.commit_storage(); // Then - assert(account.fetch_storage(key).unwrap() == value, 'wrong value in journal') + assert(account.fetch_storage(key).unwrap() == value, 'wrong committed value') } #[test] diff --git a/crates/evm/src/tests/test_instructions/test_system_operations.cairo b/crates/evm/src/tests/test_instructions/test_system_operations.cairo index 09950fc80..9c6758020 100644 --- a/crates/evm/src/tests/test_instructions/test_system_operations.cairo +++ b/crates/evm/src/tests/test_instructions/test_system_operations.cairo @@ -599,7 +599,7 @@ fn test_selfdestruct_undeployed_ca() { fund_account_with_native_token(ca_address.starknet, native_token, ca_balance); let mut machine = MachineBuilderTestTrait::new_with_presets().with_target(ca_address).build(); // - call `get_account` on an undeployed account, set its type to CA, its nonce to 1, its code to something - // to mock a cached CA that has not been commited yet. + // to mock a cached CA that has not been committed yet. let mut ca_account = machine.state.get_account(ca_address.evm); ca_account.set_code(array![0x1, 0x2, 0x3].span()); ca_account.set_type(AccountType::ContractAccount); diff --git a/crates/evm/src/tests/test_machine.cairo b/crates/evm/src/tests/test_machine.cairo index 6bf32cd5e..1b228c975 100644 --- a/crates/evm/src/tests/test_machine.cairo +++ b/crates/evm/src/tests/test_machine.cairo @@ -113,30 +113,6 @@ fn test_addresses() { assert(evm_address == expected_address, 'wrong evm address'); } -#[test] -fn test_destroyed_contracts() { - let mut machine: Machine = Default::default(); - let destroyed_contracts = machine.destroyed_contracts(); - assert(destroyed_contracts.len() == 0, 'wrong length'); -} - -#[test] -fn test_events() { - let mut machine: Machine = Default::default(); - - let events = machine.events(); - assert(events.len() == 0, 'wrong length'); -} - -#[test] -fn test_create_addresses() { - let mut machine: Machine = Default::default(); - - let create_addresses = machine.create_addresses(); - assert(create_addresses.len() == 0, 'wrong length'); -} - - #[test] fn test_set_return_data_root() { let mut machine: Machine = Default::default(); diff --git a/crates/evm/src/tests/test_model/test_contract_account.cairo b/crates/evm/src/tests/test_model/test_contract_account.cairo index 2efb262ef..2ccdd9d59 100644 --- a/crates/evm/src/tests/test_model/test_contract_account.cairo +++ b/crates/evm/src/tests/test_model/test_contract_account.cairo @@ -15,7 +15,7 @@ use starknet::testing::set_contract_address; fn test_contract_account_deploy() { let (native_token, kakarot_core) = contract_utils::setup_contracts_for_testing(); // We drop the first event of Kakarot Core, as it is the initializer from Ownable, - // triggerred in the constructor + // triggered in the constructor contract_utils::drop_event(kakarot_core.contract_address); let mut kakarot_state = KakarotCore::unsafe_new_contract_state(); diff --git a/crates/evm/src/tests/test_stack.cairo b/crates/evm/src/tests/test_stack.cairo index 6e1dc36fe..96bf1d24c 100644 --- a/crates/evm/src/tests/test_stack.cairo +++ b/crates/evm/src/tests/test_stack.cairo @@ -427,7 +427,7 @@ mod swap { let index1 = stack.peek_at(1).unwrap(); assert(index1 == 3, 'post-swap: wrong index1'); let index0 = stack.peek_at(0).unwrap(); - assert(index0 == 2, 'post-swap: wront index0'); + assert(index0 == 2, 'post-swap: wrong index0'); } #[test] @@ -462,7 +462,7 @@ mod swap { let index1 = stack.peek_at(1).unwrap(); assert(index1 == 3, 'post-swap: wrong index1'); let index0 = stack.peek_at(0).unwrap(); - assert(index0 == 2, 'post-swap: wront index0'); + assert(index0 == 2, 'post-swap: wrong index0'); } #[test] diff --git a/crates/utils/src/eth_transaction.cairo b/crates/utils/src/eth_transaction.cairo index 10269ffbe..2e33d9efb 100644 --- a/crates/utils/src/eth_transaction.cairo +++ b/crates/utils/src/eth_transaction.cairo @@ -151,8 +151,8 @@ impl EncodedTransactionImpl of EncodedTransactionTrait { let tx_type: u32 = (*encoded_tx_data.at(0)).into(); let rlp_encoded_data = encoded_tx_data.slice(1, encoded_tx_data.len() - 1); - // tx_format (EIP-2930, unsiged): 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList]) - // tx_format (EIP-1559, unsiged): 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list]) + // tx_format (EIP-2930, unsigned): 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, value, data, accessList]) + // tx_format (EIP-1559, unsigned): 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list]) let chain_idx = 0; let nonce_idx = 1; let gas_price_idx = tx_type + nonce_idx; @@ -263,7 +263,7 @@ impl EthTransactionImpl of EthTransactionTrait { return Result::Err(EthTransactionError::IncorrectChainId); } //TODO: add check for max_fee = gas_price * gas_limit - // max_fee should be later provided by the RPC, and hence this check is neccessary + // max_fee should be later provided by the RPC, and hence this check is necessary let msg_hash = encoded_tx_data.compute_keccak256_hash(); diff --git a/crates/utils/src/helpers.cairo b/crates/utils/src/helpers.cairo index 8b5053bf7..c83145baa 100644 --- a/crates/utils/src/helpers.cairo +++ b/crates/utils/src/helpers.cairo @@ -373,7 +373,7 @@ fn split_word_le(mut value: u256, mut len: usize) -> Array { dst } -/// Splits a u256 into 16 bytes, big-endien, and appends the result to `dst`. +/// Splits a u256 into 16 bytes, big-endian, and appends the result to `dst`. fn split_word_128(value: u256, ref dst: Array) { split_word(value, 16, ref dst) } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 4a3e676f1..cc57d6628 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -11,7 +11,9 @@ with the project. > **[?]** Proceed to describe how to setup local development environment. e.g: To set up a development environment, please follow the steps from -[the README.md](../README.md#installation). +[the README.md](../README.md#installation). Make sure that your Scarb version +matches the one expected, specified in the [.tool-versions](../.tool-versions) +file. ## Issues and feature requests @@ -137,7 +139,7 @@ Kakarot has many data structures, e.g. an Ethereum Transaction (struct), a Stack pay attention: - Should it be a struct? -- Should it be an enum: this is a new type made availabe in Cairo. +- Should it be an enum: this is a new type made available in Cairo. - Which types to use? Remember! **use unsigned integers as much as possible**. - Remember to add traits for specific types instead of utils to write Cairo (& Rust) idiomatic code. diff --git a/docs/general/contract_bytecode.md b/docs/general/contract_bytecode.md index 149b0264c..f2155df15 100644 --- a/docs/general/contract_bytecode.md +++ b/docs/general/contract_bytecode.md @@ -5,22 +5,27 @@ EVM will execute when a contract is called. As Kakarot's state is embedded into the Starknet chain it is deployed on, contracts are not actually "deployed" on Kakarot: instead, the EVM bytecode of the deployed contract is first executed, and the returned data is then stored on-chain at a particular storage address -inside the starknet contract corresponding to the contract's EVM address, whose +inside the Starknet contract corresponding to the contract's EVM address, whose address is deterministically computed. The Kakarot EVM will be able to load this bytecode by querying the storage of this Starknet contract when a user interacts with its associated EVM address. ```mermaid flowchart TD - A[RPC call] --> |"eth_sendTransaction (contract deployment)"| B(KakarotCore) - B --> C[Execute initialization code] - C -->|Set account code to return data| D[Store account code in KakarotCore storage] - - X[RPC call] --> |"eth_sendTransaction (contract interaction)"| Y(KakarotCore) - Y --> Z[Load account code from KakarotCore storage] - Z --> ZZ[Execute bytecode] + A[RPC call] --> B["eth_sendTransaction"] + B --> |Check transaction type| C{Is deploy transaction?} + C -- Yes --> D1[Execute initialization code] + D1 -->|Set account code to return data| E1[Commit code to Starknet storage] + E1 --> F1[Return deployed contract address] + + C -- No --> D2[Load account code from KakarotCore storage] + D2 --> E2[Execute bytecode] + E2 --> F2[Return execution result] ``` + Transaction flow for deploy and execute transactions in +Kakarot + There are several different ways to store the bytecode of a contract, and this document will provide a quick overview of the different options, to choose the most optimized one for this use case. The three main ways of handling contract @@ -67,11 +72,12 @@ significant price, as the publication of state diffs on Ethereum accounted for [over 93% of the transaction fees paid on Starknet](https://community.starknet.io/t/volition-hybrid-data-availability-solution/97387). The first choice when storing contract bytecode is to store it as a regular -storage variable, with its state diff posted on Ethereum acting as the DA Layer. +variable in the contract account's storage, with its state diff posted on +Ethereum acting as the DA Layer. In this case, the following data would reach L1: -- The KakarotCore contract address +- The Starknet address of the contract account - The number of updated keys in that contract - The keys to update - The new values for these keys @@ -83,15 +89,15 @@ $$ gas\ price \cdot c_w \cdot (2n + 2m) $$ where $c_w$ is the calldata cost (in gas) per 32-byte word. -In this case, one single contract (the Starknet contract corresponding to the -ContractAccount) would be updated, with $m$ keys, where $m = (B / 31) + 2$ and -$B$ is the size of the bytecode to store (see -[implementation details](./contract_bytecode.md#implementation-details)). +When storing the EVM bytecode during deployment, one single contract (the +Starknet contract corresponding to the ContractAccount) would be updated, with +$m$ keys, where $m = (B / 31) + 2$ and $B$ is the size of the bytecode to store +(see [implementation details](./contract_bytecode.md#implementation-details)). Considering a gas price of 34 gwei (average gas price in 2023, according to -[Etherscan](https://etherscan.io/chart/gasprice)),a calldata cost of 16 per byte -and the size of a typical ERC20 contract size of 2174 bytes, we would have -$m = 72$. The associated storage update fee would be: +[Etherscan](https://etherscan.io/chart/gasprice)), a calldata cost of 16 per +non-zero byte of calldata and the size of a typical ERC20 contract size of 2174 +bytes, we would have $m = 72$. The associated storage update fee would be: $$ fee = 34 \cdot (16 \cdot 32) \cdot (2 + 144) = 2,541,468 \text{ gwei}$$ @@ -108,7 +114,7 @@ for both L2 and L1 data availability modes. The difference is in the data availability guarantees. When a state transition is verified on L1, its correctness is ensured - however, the actual state of the L2 is not known on L1. By posting state diffs on L1, the current state of Starknet can be reconstructed -from the beginning, but this has a significant cost. +from the beginning, but this has a significant cost as mentioned previously. ![Volition](volition.png) @@ -172,12 +178,12 @@ committed to Ethereum. This solution is the most secure one, as it relies on Ethereum as a DA Layer, and thus inherits from Ethereum's security guarantees, ensuring that the bytecode of the deployed contract is always available. -A `deploy` transaction is identified by a null `to` address (`Option::None`). -The data sent to the KakarotCore contract when deploying a new contract will be -passed as an `Array` to the entrypoint `eth_send_transaction` of the -KakarotCore contract. This bytecode will then be packed 31 bytes at a time, -reducing by 31 the size of the bytecode stored in storage, which is the most -expensive part of the transaction. +In Ethereum, a `deploy` transaction is identified by a null `to` address +(`Option::None`). The calldata sent to the KakarotCore contract when deploying a +new contract will be passed as an `Array` to the `eth_send_transaction` +entrypoint of the KakarotCore contract. This bytecode will then be packed 31 +bytes at a time, reducing by 31 the size of the bytecode stored in storage, +which is the most expensive part of the transaction. The contract storage related to a deployed contract is organized as: @@ -192,34 +198,24 @@ struct Storage { We use the `List` type from the [Alexandria](https://github.com/keep-starknet-strange/alexandria/blob/main/src/storage/src/list.cairo) library to store the bytecode, allowing us to store up to 255 31-bytes values -per `StorageBaseAddress`. For bytecode containing more than 255 31-bytes values, -the `List` type abstracts the calculations of the next storage address used, -which is calculated by using poseidon hashes applied on `previous_address+1`. +per `StorageBaseAddress`. Indeed, the current limitation on the maximal size of +a complex storage value is 256 field elements, where a field element is the +native data type of the Cairo VM. If we want to store more than 256 field +elements, which is the case for bytecode larger than 255 31-bytes values, which +represents 7.9kB, we need to split the data between multiple storage addresses. +The `List` type abstracts this process by automatically calculating the next +storage address to use, by applying poseidon hashes on the base storage address +of the list with the index of the segment to store the element in. The logic behind this storage design is to make it very easy to load the bytecode in the EVM when we want to execute a program. We will rely on the ByteArray type, which is a type from the core library that we can use to access -individual byte indexes in an array of packed bytes31 values. This type is -defined as: - -```rust -struct ByteArray { - // Full "words" of 31 bytes each. The first byte of each word in the byte array - // is the most significant byte in the word. - data: Array, - // This felt252 actually represents a bytes31, with < 31 bytes. - // It is represented as a felt252 to improve performance of building the byte array. - // The number of bytes in here is specified in `pending_word_len`. - // The first byte is the most significant byte among the `pending_word_len` bytes in the word. - pending_word: felt252, - // Should be in range [0, 30]. - pending_word_len: usize, -} -``` +individual byte indexes in an array of packed bytes31 values. -The rationale behind this structure is detailed in the code snippet above - but -you can notice that our stored variables reflect the fields the ByteArray type. -Once our bytecode is written in storage, we can simply load it by doing so: +The rationale behind this structure is thoroughly documented in the core library +code. The variable stored in our contract's storage reflect the fields of the +ByteArray type. Once our bytecode is written in storage, we can simply load it +with ```rust let bytecode = ByteArray { @@ -230,4 +226,5 @@ Once our bytecode is written in storage, we can simply load it by doing so: ``` After which the value of the bytecode at offset `i` can be accessed by simply -doing `bytecode[i]` when executing the bytecode instructions in the EVM. +doing `bytecode[i]` when executing the bytecode instructions in the EVM - making +it convenient to iterate over. diff --git a/docs/general/contract_storage.md b/docs/general/contract_storage.md index b61cce493..042907767 100644 --- a/docs/general/contract_storage.md +++ b/docs/general/contract_storage.md @@ -15,7 +15,7 @@ _Account state associated to an Ethereum address. Source: [EVM Illustrated](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)_ In traditional EVM clients, like Geth, the _world state_ is stored as a _trie_, -and informations about account are stored in the world state trie and can be +and information about account are stored in the world state trie and can be retrieved through queries. Each account in the world state trie is associated with an account storage trie, which stores all of the information related to the account. When Geth updates the storage of a contract by executing the SSTORE @@ -66,17 +66,16 @@ flowchart TD; style M fill:#DB2929,stroke:#333,stroke-width:2px; ``` -_Simplified process representation of SSTORE and SLOAD Opcodes in the Geth EVM -Client_ +_Simplified representation of SSTORE and SLOAD Opcodes +behavior in the Geth EVM Client_ ## Storage in Kakarot As Kakarot is a contract that is deployed on Starknet and is not a client that can directly manipulate a storage database, our approach differs from one of a -traditional client. We do not have a world state trie, and we do not have a -storage trie. Instead, we have access to Kakarot's contract storage on the -Starknet blockchain, that we can query using syscalls to read and update the -value of a of a storage slot. +traditional client. We do not directly manipulate tries. Instead, we have access +to contracts storage on the Starknet blockchain, that we can query using +syscalls to read and update the value of a of a storage slot. There are two different ways of handling Storage in Kakarot. @@ -87,34 +86,44 @@ that for every contract that is deployed on Kakarot, we will deploy an underlying Starknet contract, which has its own state which can only be queried by itself. -The current contract storage design in Kakarot Zero is organized as such: +This design closely resembles the design of the EVM. It has the following +properties: - The two different kinds of EVM accounts - Externally Owned Accounts (EOA) and Contract Accounts (CA) - are both represented by Starknet smart contracts. - Each account is mapped to a unique Starknet contract. Each contract has its - own storage. + Each EVM account is mapped to a unique Starknet contract. Each contract + account has its own storage, and has external functions that can be called by + Kakarot to read and write to its storage. - Each contract is deployed by Kakarot, and contains its own bytecode in storage - in the case of a smart contract (no bytecode for an EOA). + in the case of a smart contract (as EOAs don't have code) - Each contract account has external functions that can be called by Kakarot to read the bytecode it stores and to read / write to its storage. This makes Kakarot an effective "admin" to all contracts with rights to modify their storage. -- SLOAD/SSTORE opcodes are used to read/write to storage and perform a - `contract_call_syscall` to modify the storage of the remote contract. -However, this design has some limitations: +This design has some limitations: -- We perform a `call_contract_syscall` for each SLOAD/SSTORE, which is - expensive. Given that only KakarotCore can modify the storage of a Kakarot - contract, we could directly store the whole world state in the main Kakarot - contract storage. +- We perform a `call_contract_syscall` for each SLOAD/SSTORE operation that is + committed to Starknet, which has an extra overhead compared to directly + modifying the current contract's storage . Given that only KakarotCore can + modify the storage of a Kakarot contract, we could directly store the whole + world state in the main Kakarot contract storage. - It adds external entrypoints with admin rights to read and write from storage - in each Kakarot contract. This is not ideal from a security perspective. + in each Kakarot contract, which adds security risks. - It moves away from the traditional EVM design, in which execution clients store account states in a common database backend. -Therefore, we will not use this design in SSJ. We will instead use the second -design presented thereafter. +However, it has some interesting properties. It allows us to have a one-to-one +mapping between Kakarot contracts and Starknet contracts, which makes it easier +to perform value transfers with the chain's native token. Moreover, it allows +one to send funds from a Starknet account to a Kakarot account, which can be +useful to implement a bridging mechanism to Kakarot with low overhead, or any +other mechanism that requires interacting with funds of a Kakarot account. + +Considering the compatibility properties, we will use this design in Kakarot. +The second design, presented after, still has some interesting properties that +we will discuss. But the benefits it brings do not outweigh the loss of +compatibility. ### A shared storage space for all Kakarot Contracts @@ -129,19 +138,61 @@ deploying contracts. A contract’s storage on Starknet is a persistent storage space where you can read, write, modify, and persist data. The storage is a map with $2^{251}$ -slots, where each slot is a felt which is initialized to 0. +slots, where each slot is a `felt252` which is initialized to 0. This new model doesn't expose read and write methods on Kakarot contracts. Instead of having $n$ contracts with `write_storage` and `read_storage` entrypoints, the only way to update the storage of a Kakarot contract is now through executing SLOAD / SSTORE internally to KakarotCore. +Regarding the security of such a design, we can reason about the probability of +a collision occurring when interacting with this shared storage. +[65M contracts](https://dune.com/queries/2284893/3744521) have been deployed on +Ethereum so far. If we assume that Kakarot could reach the same number of +contracts, that would leave us with a total of +$2^{251} / 65\cdot10^6 \approx 2^{225}$ slots per contract. Even with a +hypothetical number of 100 billion contracts, we would still have around +$2^{214}$ storage slots available per contract. + +Considering the birthday paradox, the probability of a collision occurring, +given $2^{214}$ randomly chosen slots, is roughly $1/2^{107}$. This is a very +low probability, which is considered secure by today's standards. We can +therefore consider that the collision risk is negligible and that this storage +layout doesn't introduce any security risk to Kakarot. For reference, Ethereum +has 80 bits of security on its account addresses, which are 160 bits long. + +But, as we're looking for maximum compatibility between "pure" Starknet +contracts and "Kakarot" Starknet contracts, this design is not ideal from a +compatibility perspective. It requires us to keep an internal accounting of the +balances of each account, and to expose external entrypoints in order to query +the balances of each account. This is not ideal, as it completely breaks the +compatibility with Starknet. + +### Tracking and reverting storage changes + +The storage mechanism presented in the [Local State](./local_state.md) section +enable us to revert storage changes by using a concept similar to Geth's +journal. Each storage change will be stored in a `StateChangeLog` implemented +using a `Felt252Dict` data structure, that will associate each modified storage +address to its new value. This allows us to perform three things: + +- When executing a transaction, instead of using one `storage_write_syscall` per + SSTORE opcode, we can simply store the storage changes in this cache state. At + the end of the transaction, we can finalize all the storage writes together + and perform only one `storage_write_syscall` per modified storage address. +- When reading from storage, we can first read from the state to see if the + storage slot has been modified. If it's the case, we can read the new value + from the state instead of performing a `storage_read_syscall`. +- If the transaction reverts, we won't need to revert the storage changes + manually. Instead, we can simply not finalize the storage changes present in + the state, which can save a lot of gas. + ```mermaid sequenceDiagram participant C as Caller participant K as KakarotCore participant M as Machine - participant J as Journal + participant J as State participant S as ContractState C->>K: Executes Kakarot contract @@ -151,93 +202,72 @@ sequenceDiagram Note over K,M: If it's an SLOAD operation, it reads from Storage. alt SSTORE - M-->>M: key = hash(evm_address, storage_slot) - M->>J: journal.insert(key,value) + M-->>M: hash = hash(evm_address, storage_key) + M->>J: state.accounts_storage.insert(hash, (key, value)) else SLOAD - M-->>M: key = hash(evm_address, storage_slot) - M->>J: journal.get(key) - J -->> M: Nullable - alt Journal returns value + M-->>M: hash = hash(evm_address, storage_key) + M->>J: state.accounts_storage.get(hash) + J -->> M: Nullable~value~ + alt State returns value - else Journal returns nothing - M->>S: storage_read(key) + else State returns nothing + M->>S: storage_read(evm_address, key) S-->>M: value end end - Note over K,M: Committing journal entries to storage + Note over K,M: Committing State entries to storage K->>M: Commit - M->>J: Get all journal entries + M->>J: Get all state storage entries J -->>M: entries - loop for each journal entry - M->>S: storage_write(key,value) + loop for each storage entry + M->>S: storage_write(evm_address, key, value) end Note over S: Storage is now updated with the final state of all changes made during the transaction. ``` -### Eventual security risks - -According to -[an engineer from ElectricCapital](https://twitter.com/n4motto/status/1554853912074522624?s=20), -44M contracts have been deployed on Ethereum so far. If we assume that Kakarot -could reach the same number of contracts, that would leave us with a total of -$2^{251} / 44\cdot10^6 \approx 2^{225}$ slots per contract. Even with a -hypothetical number of 100 billion contracts, we would still have around -$2^{214}$ storage slots available per contract. - -Considering the birthday paradox, the probability of a collision occurring, -given $2^{214}$ randomly chosen slots, is roughly $1/2^{107}$. This is a very -low probability, which is considered secure by today's standards. We can -therefore consider that the collision risk is negligible and that this storage -layout doesn't introduce any security risk to Kakarot. For reference, Ethereum -has 80 bits of security on its account addresses, which are 160 bits long. - -### Tracking and reverting storage changes - -This design allows reverting storage changes by using a concept similar to -Geth's journal. Each storage change will be stored in a `Journal` implemented -using a `Felt252Dict` data structure, that will associate each modified storage -address to its new value. This allows us to perform three things: - -- When executing a transaction, instead of using one `storage_write_syscall` per - SSTORE opcode, we can simply store the storage changes in this journal. At the - end of the transaction, we can finalize all the storage writes together and - perform only one `storage_write_syscall` per modified storage address. -- When reading from storage, we can first read from the journal to see if the - storage slot has been modified. If it's the case, we can read the new value - from the journal instead of performing a `storage_read_syscall`. -- If the transaction reverts, we won't need to revert the storage changes - manually. Instead, we can simply not finalize the storage changes present in - the journal, which can save a lot of gas. +Sequence of interactions between Kakarot, the local State +and Starknet for the SSTORE and SLOAD Opcodes ### Implementation The SSTORE and SLOAD opcodes are implemented to first read and write to the -`Journal` instead of directly writing to the KakarotCore contract's storage. - -Using the `storage_read_syscall` and `storage_write_syscall` syscalls, we can -arbitrarily read and write to a contract's storage. Therefore, we will be able -to simply implement the SSTORE and SLOAD opcodes as follows: +`State` instead of directly writing to the KakarotCore contract's storage. The +internal location of the storage slot is computed by applying the poseidon hash +on the EVM address of the contract and the storage key. We store the key +nonetheless, as it will be needed to finalize the storage updates at the end of +the transaction when retrieving the address of the contract to apply storage +changes to. + +Using the `storage_at` and `set_storage_at` entrypoints in the contract +accounts, we can arbitrarily read and write to another contract's storage. +Therefore, we will be able to simply implement the SSTORE and SLOAD opcodes in +two steps, as follows: ```rust // SSTORE - let storage_address = poseidon_hash(evm_address, storage_slot); - self.journal.insert(storage_address, NullableTrait::new(value)); + let hash = poseidon_hash(evm_address, key); + self.state.accounts_storage.write(hash, (evm_address, key, value)); ``` ```rust // SLOAD let storage_address = poseidon_hash(evm_address, storage_slot); - let value = match_nullable(self.journal.get(storage_address)) { - FromNullableResult::Null => storage_read_syscall(storage_address), - FromNullableResult::NotNull(value) => value.unbox(), - } + let maybe_entry = self.accounts_storage.read(internal_key); + match maybe_entry { + Option::Some((_, key, value)) => { return Result::Ok(value); }, + Option::None => { + let account = self.get_account(evm_address); + return account.read_storage(key); + } +} ``` ```rust // Finalizing storage updates - for keys in journal_keys{ - storage_write_syscall(key, journal.get(key)); + for storage_hash in accounts_storage{ + let (contract_address, key, value) = account_state_keys[storage_hash] + ContractAccountDispatcher{contract_address}.set_storage_at(key, value); } ``` diff --git a/docs/general/execution_context.md b/docs/general/execution_context.md deleted file mode 100644 index d82ad3920..000000000 --- a/docs/general/execution_context.md +++ /dev/null @@ -1,106 +0,0 @@ -# Kakarot's Execution Context - -The execution context is the environment in which the EVM bytecode code is -executed. It contains information such as the bytecode being currently executed, -the value of the program counter, the gas limit, etc. - -It is modeled through the ExecutionContext struct, which contains the following -fields - -> Note: the -> [actual implementation of the execution context](https://github.com/kkrt-labs/kakarot-ssj/blob/main/crates/evm/src/context.cairo#L163) -> doesn't match the description below, as some fields are packed together in -> sub-structs for optimisation purposes. However, the general idea remains the -> same. - -```mermaid -classDiagram - class ExecutionContext{ - id: usize, - evm_address: EthAddress, - starknet_address: ContractAddress, - program_counter: u32, - status: Status, - call_ctx: CallContext, - destroyed_contracts: Array~EthAddress~, - events: Array~Event~, - create_addresses: Array~EthAddress~, - return_data: Span~u8~, - parent_ctx: Nullable~ExecutionContext~, - } - - class CallContext{ - caller: EthAddress, - bytecode: Span~u8~, - calldata: Span~u8~, - value: u256, - gas_price: u128, - gas_limit: u128, - read_only: bool, - } - - class Event{ - keys: Array~u256~, - data: Array~u8~ - } - - class Status{ - <> - Active, - Stopped, - Reverted - } - - ExecutionContext *-- CallContext - ExecutionContext *-- Event - ExecutionContext *-- Status -``` - -When submitting a transaction to the EVM, the `call_ctx` field of the -`ExecutionContext` is initialized with the bytecode of the contract to execute, -the call data sent in the transaction, and the value of the transaction. The -`ExecutionContext` could also hold the `Stack` and `Memory` data structures -relative to the current code execution. However, due to Cairo's limitations, -these data structures have been moved to the `Machine` struct - which is -explained in detail in the [Machine](./machine.md) docs. - -Executing opcodes mutates both the execution context and the state of the -Machine in general. For example, executing the ADD opcode removes the top two -elements from the stack, pushes back their sum, updates the value of `pc`. - -## Run execution flow - -The following diagram describe the flow of the execution context when executing -the `run` function given an instance of the `Machine` struct. - -The run function is responsible for executing EVM bytecode. The flow of -execution involves decoding and executing the current opcode, handling the -execution, and continue executing the next opcode if the execution of the -previous one succeeded. If the execution of an opcode fails, the execution -context reverts and changes made to the blockchain state are not finalized. - -```mermaid -flowchart TD -AA["START"] --> A -A["run()"] --> B[Decode and Execute Opcode] -B --> C{Result OK?} -C -->|Yes| D{Execution stopped?} -D -->|No => pc+=1| A -D -->|Yes| F{Reverted?} -C -->|No| RA -F --> |No| J["emit pending events"] -J --> K["finalize local storage updates"] -K --> END["return"] -F -->|Yes| RA[Erase contracts created] - -subgraph revert context changes -RA --> RB["Clear un-emitted events"] -RB --> RC["Revert state updates"] -end -RC --> END -``` - - - -> Note: The revert context changes subgraph is not implemented yet. Note: The -> event emission is not implemented yet. diff --git a/docs/general/local_state.md b/docs/general/local_state.md index 43da71266..038ca8cfb 100644 --- a/docs/general/local_state.md +++ b/docs/general/local_state.md @@ -24,7 +24,7 @@ contextual changes, which refers to changes made inside the current execution context. The local state is updated as the code is executed, and when an execution -context is finished, we merge the contextual state updates into the +context is finalized, we merge the contextual state updates into the transactional state. When a transaction finishes, we apply the transactional state diffs by writing them inside the blockchain storage. @@ -44,7 +44,7 @@ sequenceDiagram participant Starknet User->>+KakarotCore:Sends transaction - KakarotCore->>+Machine: Instanciates machine with bytecode to run + KakarotCore->>+Machine: Instantiates machine with bytecode to run Machine->>LocalState: Initialize local state Machine->>+LocalState: Start execution @@ -61,7 +61,7 @@ sequenceDiagram Machine-->>-Machine: Execution ended - alt Execution terminated sucessfully + alt Execution terminated successfully KakarotCore->>Starknet: Apply transactional state diffs to Starknet else Execution failed @@ -72,6 +72,9 @@ sequenceDiagram ``` +Interactions between Kakarot, its local state and Starknet +storage + ## Implementation We need to be able to store in the local state the current information: @@ -91,7 +94,7 @@ perform actual token transfers. ## Implementation design The state is implemented as a struct composed by `StateChangeLog` - for objects -that need to be overriden in the State - and `SimpleLog` data structures, for +that need to be overridden in the State - and `SimpleLog` data structures, for objects that only need to be appended to a list. They are implemented as follows: @@ -109,13 +112,15 @@ struct State { /// Pending emitted events events: SimpleLog, /// Account balances updates. This is only internal accounting and stored - /// balances are updated when commiting transfers. + /// balances are updated when committing transfers. balances: StateChangeLog, /// Pending transfers transfers: SimpleLog, } ``` +The State struct + A `StateChangeLog` is a data structure that tracks the changes made to a specific object both at the context-level and at the transaction-level using the mechanism specified above. The `SimpleLog` is a simplified version of this @@ -145,6 +150,8 @@ struct StateChangeLog { } ``` +The StateChangeLog generic struct + ```rust /// `SimpleLog` is a straightforward logging mechanism. @@ -160,17 +167,11 @@ struct SimpleLog { contextual_logs: Array, transactional_logs: Array, } - ``` -The reason for this implementations are that: - -- Including `balances` inside the `Account` struct would require loading the - entire bytecode of a ContractAccount, even for simple transfers - which is not - optimized as it can be an expensive operation. Therefore, by storing these - changes in a standalone field in the state, we can apply balance changes - without loading the accounts first. -- Storage changes are modeled using a dictionary where the key is the storage - address to update, and the value is the updated value. However, we can't - create nested dictionaries in Cairo - so we have to separate the accounts - storage updates from the account updates themselves. +The SimpleLog generic struct + +Storage changes are modeled using a dictionary where the key is the storage +address to update, and the value is the updated value. However, we can't create +nested dictionaries in Cairo - so we have to separate the accounts storage +updates from the account updates themselves. diff --git a/docs/general/machine.md b/docs/general/machine.md index 90a967f61..efc8f6d07 100644 --- a/docs/general/machine.md +++ b/docs/general/machine.md @@ -13,10 +13,10 @@ context, which contained the stack, the memory, and the execution state. Each local execution context optionally contained parent and child execution contexts, which were used to model the execution of sub-calls. However, this design was not possible to implement in Cairo, as Cairo does not support the use -of Nullable types containing dictionaries. Since the ExecutionContext struct -mentioned in [execution_context](./execution_context.md) contains such Nullable -types, we had to change the design of the EVM to use a machine with a single -Stack and Memory, which are our dict-based data structures. +of `Nullable` types containing dictionaries. Since the `ExecutionContext` struct +contains such `Nullable` types, we had to change the design of the EVM to use a +machine with a single stack and memory, which are our dict-based data +structures. ## The Kakarot Machine design @@ -41,16 +41,16 @@ To overcome the problem stated above, we have come up with the following design: - The execution context tree is initialized with a single root execution context, which has no parent and no child. It has an `id` field equal to 0. -The following diagram describes the model of the Kakarot Machine. +The following diagram describes the model of the Kakarot machine. ```mermaid classDiagram class Machine{ - current_ctx: Box, + current_ctx: Box~ExecutionContext~, ctx_count: usize, stack: Stack, memory: Memory, - storage_journal: Journal, + state: State, } class Memory{ @@ -66,20 +66,22 @@ classDiagram } class ExecutionContext{ - id: usize, - evm_address: EthAddress, - starknet_address: ContractAddress, - program_counter: u32, - status: Status, - call_ctx: CallContext, - destroyed_contracts: Array~EthAddress~, - events: Array~Event~, - create_addresses: Array~EthAddress~, - return_data: Array~u8~, - parent_ctx: Nullable~ExecutionContext~, - child_return_data: Option~Span~u8~~ + ctx_type: ExecutionContextType, + address: Address, + program_counter: u32, + status: Status, + call_ctx: Box~CallContext~, + return_data: Span~u8~, + parent_ctx: Nullable~ExecutionContext~, + gas_used: u128, } + class ExecutionContextType { + <> + Root: IsCreate, + Call: usize, + Create: usize + } class CallContext{ caller: EthAddress, @@ -89,13 +91,27 @@ classDiagram gas_price: u128, gas_limit: u128, read_only: bool, + ret_offset: usize, + ret_size: usize, + } + + class State{ + accounts: StateChangeLog~Account~, + accounts_storage: StateChangeLog~EthAddress_u256_u256~, + events: SimpleLog~Event~, + transfers: SimpleLog~Transfer~, + } + + class StateChangeLog~T~ { + contextual_changes: Felt252Dict~Nullable~T~~, + contextual_keyset: Array~felt252~, + transactional_changes: Felt252Dict~Nullable~T~~, + transactional_keyset: Array~felt252~ } - class Journal{ - local_changes: Felt252Dict~felt252~, - local_keys: Array~felt252~, - global_changes: Felt252Dict~felt252~, - global_keys: Array~felt252~, + class SimpleLog~T~ { + contextual_logs: Array~T~, + transactional_logs: Array~T~, } class Status{ @@ -109,12 +125,17 @@ classDiagram Machine *-- Memory Machine *-- Stack Machine *-- ExecutionContext - Machine *-- Journal + Machine *-- State ExecutionContext *-- ExecutionContext + ExecutionContext *-- ExecutionContextType ExecutionContext *-- CallContext ExecutionContext *-- Status + State *-- StateChangeLog + State *-- SimpleLog ``` +Kakarot internal architecture model + ### The Stack Instead of having one Stack per execution context, we have a single Stack shared @@ -176,54 +197,41 @@ If we want to store an item at offset 10 of the memory relative to the execution context of id 1, the internal index will be $index = 10 + 1 \cdot 131072 = 131082$. -### Tracking storage changes - -The EVM has a persistent storage, which is a key-value store. The storage -changes are tracked in the `journal` field of the Machine. This field is a -dictionary mapping storage slots addresses modified by the current execution -context to their most recent value. For more information on how storage is -managed in Kakarot, read [contract_storage](./contract_storage.md). - -We encounter the same constraints as for the Stack and the Memory, as the -storage changes are tracked using a dictionary; meaning that it can't be a part -of the ExecutionContext struct. Therefore, we will track the storage changes in -the Machine struct. What we want to achieve is the following: - -- Track the storage changes performed in the transaction as a whole. -- Track the storage changes performed in the current execution context. -- Rollback the storage changes performed in the current execution context when - the execution context is reverted. -- Finalize the storage changes performed in the transaction when the transaction - is finalized. - -Considering Cairo's limitations raised previously, we will use a single data -structure to track local and global storage changes. We will use a `Journal` -data structure, that will track two things: the changes performed in the current -execution context, and the changes performed in the transaction as a whole. The -Journal will have the following fields: - -```rust - struct Journal { - local_changes: Felt252Dict, - local_keys: Array, - global_changes: Felt252Dict, - global_keys: Array, - } +## Execution flow + +The following diagram describe the flow of the execution context when executing +the `run` function given an instance of the `Machine` struct instantiated with +the bytecode to execute and the appropriate execution context. + +The run function is responsible for executing EVM bytecode. The flow of +execution involves decoding and executing the current opcode, handling the +execution, and continue executing the next opcode if the execution of the +previous one succeeded. If the execution of an opcode fails, the execution +context reverts, the changes made in this context are dropped, and the state of +the blockchain is not updated. + +```mermaid +flowchart TD +AA["START"] --> A +A["run()"] --> B[Decode and Execute Opcode] +B --> |pc+=1| C{Result OK?} +C -->|Yes| D{Execution stopped?} +D -->|No| A +D -->|Yes| F{Reverted?} +C -->|No| RA +F --> |No| FA +F -->|Yes| RA[Discard account updates] + +subgraph Discard context changes +RA --> RB["Discard storage updates"] +RB --> RC["Discard event log"] +RC --> RD["Discard transfers log"] +end + +RD --> FA[finalize context] ``` -The `local_changes` field is a dictionary mapping storage slots addresses to the -most recent changes, performed in the local execution context. The `local_keys` -field is used to track the indexes of the storage slots addresses in the -dictionary in order to be able to iterate over the dictionary. Similarly, the -`global_changes` field is a dictionary mapping storage slots addresses to the -changes performed in the transaction as a whole. The `global_keys` tracks the -indexes of these storage slots addresses in the dictionary. - -When an execution contexts stops, we will commit the local changes to the global -changes by inserting the local changes in the `global_changes` dictionary, and -updating the `global_keys` array. When the transaction is finalized, we will -iterate over the `global_changes` dictionary, and perform the required storage -updates, as mentioned in [Contract Storage](./contract_storage.md). +Execution flow of EVM bytecode ## Conclusion