From d18b0a94a2a4cf050ba74c24208aefbff8375535 Mon Sep 17 00:00:00 2001 From: enitrat Date: Fri, 13 Sep 2024 15:21:32 +0200 Subject: [PATCH] refactor: add EthRPC trait --- crates/contracts/src/account_contract.cairo | 54 +-- crates/contracts/src/kakarot_core.cairo | 1 + .../contracts/src/kakarot_core/eth_rpc.cairo | 212 +++++++++++ .../src/kakarot_core/interface.cairo | 11 - .../contracts/src/kakarot_core/kakarot.cairo | 230 ++---------- .../contracts/tests/test_kakarot_core.cairo | 73 +--- .../src/instructions/system_operations.cairo | 2 +- crates/evm/src/interpreter.cairo | 337 +++++++++++++++++- 8 files changed, 593 insertions(+), 327 deletions(-) create mode 100644 crates/contracts/src/kakarot_core/eth_rpc.cairo diff --git a/crates/contracts/src/account_contract.cairo b/crates/contracts/src/account_contract.cairo index 18e792192..7149adb67 100644 --- a/crates/contracts/src/account_contract.cairo +++ b/crates/contracts/src/account_contract.cairo @@ -50,6 +50,7 @@ pub mod AccountContract { use contracts::components::ownable::ownable_component::InternalTrait; use contracts::components::ownable::ownable_component; use contracts::errors::KAKAROT_REENTRANCY; + use contracts::kakarot_core::eth_rpc::{IEthRPCDispatcher, IEthRPCDispatcherTrait}; use contracts::kakarot_core::interface::{IKakarotCoreDispatcher, IKakarotCoreDispatcherTrait}; use contracts::storage::StorageBytecode; use core::cmp::min; @@ -69,7 +70,7 @@ pub mod AccountContract { use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; use super::OutsideExecution; use utils::constants::{POW_2_32}; - use utils::eth_transaction::transaction::{TransactionUnsignedTrait, Transaction}; + use utils::eth_transaction::transaction::TransactionUnsignedTrait; use utils::serialization::{deserialize_signature, deserialize_bytes, serialize_bytes}; use utils::traits::DefaultSignature; @@ -256,7 +257,9 @@ pub mod AccountContract { let address = self.Account_evm_address.read(); verify_eth_signature(unsigned_transaction.hash, signature, address); - let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; + let kakarot = IEthRPCDispatcher { contract_address: self.ownable.owner() }; + //TODO: refactor this to call eth_send_raw_unsigned_tx. Only the transactions bytes are + //passed. let (success, return_data, gas_used) = kakarot .eth_send_transaction(unsigned_transaction.transaction); let return_data = serialize_bytes(return_data).span(); @@ -278,51 +281,4 @@ pub mod AccountContract { array![return_data] } } - - #[generate_trait] - impl Eip1559TransactionImpl of Eip1559TransactionTrait { - //TODO: refactor into a generic tx validation function. - fn validate_eip1559_tx(ref self: ContractState, tx: Transaction,) -> bool { - // let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; - // let block_gas_limit = kakarot.get_block_gas_limit(); - - // if tx.gas_limit() >= block_gas_limit { - // return false; - // } - - // let base_fee = kakarot.get_base_fee(); - // let native_token = kakarot.get_native_token(); - // let balance = IERC20CamelDispatcher { contract_address: native_token } - // .balanceOf(get_contract_address()); - - // let max_fee_per_gas = tx_fee_infos.max_fee_per_gas; - // let max_priority_fee_per_gas = tx_fee_infos.max_priority_fee_per_gas; - - // // ensure that the user was willing to at least pay the base fee - // if base_fee >= max_fee_per_gas { - // return false; - // } - - // // ensure that the max priority fee per gas is not greater than the max fee per gas - // if max_priority_fee_per_gas >= max_fee_per_gas { - // return false; - // } - - // let max_gas_fee = tx.gas_limit() * max_fee_per_gas; - // let tx_cost = max_gas_fee.into() + tx_fee_infos.amount; - - // if tx_cost >= balance { - // return false; - // } - - // // priority fee is capped because the base fee is filled first - // let possible_priority_fee = max_fee_per_gas - base_fee; - - // if max_priority_fee_per_gas >= possible_priority_fee { - // return false; - // } - - return true; - } - } } diff --git a/crates/contracts/src/kakarot_core.cairo b/crates/contracts/src/kakarot_core.cairo index 83b34337c..0989f1244 100644 --- a/crates/contracts/src/kakarot_core.cairo +++ b/crates/contracts/src/kakarot_core.cairo @@ -1,3 +1,4 @@ +pub mod eth_rpc; pub mod interface; mod kakarot; pub use interface::{ diff --git a/crates/contracts/src/kakarot_core/eth_rpc.cairo b/crates/contracts/src/kakarot_core/eth_rpc.cairo new file mode 100644 index 000000000..23c6333f1 --- /dev/null +++ b/crates/contracts/src/kakarot_core/eth_rpc.cairo @@ -0,0 +1,212 @@ +use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait}; +use contracts::kakarot_core::interface::IKakarotCore; +use contracts::kakarot_core::kakarot::{KakarotCore, KakarotCore::{KakarotCoreState}}; +use core::num::traits::Zero; +use core::starknet::get_tx_info; +use core::starknet::{EthAddress, get_caller_address}; +use evm::backend::starknet_backend; +use evm::backend::validation::validate_eth_tx; +use evm::model::{TransactionResult, Address}; +use evm::{EVMTrait}; +use utils::eth_transaction::transaction::{TransactionTrait, Transaction}; + +#[starknet::interface] +pub trait IEthRPC { + /// Returns the balance of the specified address. + /// + /// This is a view-only function that doesn't modify the state. + /// + /// # Arguments + /// + /// * `address` - The Ethereum address to get the balance from + /// + /// # Returns + /// + /// The balance of the address as a u256 + fn eth_get_balance(self: @T, address: EthAddress) -> u256; + + /// Returns the number of transactions sent from the specified address. + /// + /// This is a view-only function that doesn't modify the state. + /// + /// # Arguments + /// + /// * `address` - The Ethereum address to get the transaction count from + /// + /// # Returns + /// + /// The transaction count of the address as a u64 + fn eth_get_transaction_count(self: @T, address: EthAddress) -> u64; + + /// Returns the current chain ID. + /// + /// This is a view-only function that doesn't modify the state. + /// + /// # Returns + /// + /// The chain ID as a u64 + fn eth_chain_id(self: @T) -> u64; + + /// Executes a new message call immediately without creating a transaction on the block chain. + /// + /// This is a view-only function that doesn't modify the state. + /// + /// # Arguments + /// + /// * `origin` - The address the transaction is sent from + /// * `tx` - The transaction object + /// + /// # Returns + /// + /// A tuple containing: + /// * A boolean indicating success + /// * The return data as a Span + /// * The amount of gas used as a u64 + fn eth_call(self: @T, origin: EthAddress, tx: Transaction) -> (bool, Span, u64); + + /// Generates and returns an estimate of how much gas is necessary to allow the transaction to + /// complete. + /// + /// This is a view-only function that doesn't modify the state. + /// + /// # Arguments + /// + /// * `origin` - The address the transaction is sent from + /// * `tx` - The transaction object + /// + /// # Returns + /// + /// A tuple containing: + /// * A boolean indicating success + /// * The return data as a Span + /// * The estimated gas as a u64 + fn eth_estimate_gas(self: @T, origin: EthAddress, tx: Transaction) -> (bool, Span, u64); + + //TODO: make this an internal function. The account contract should call + //eth_send_raw_transaction. + /// Executes a transaction and possibly modifies the state. + /// + /// # Arguments + /// + /// * `tx` - The transaction object + /// + /// # Returns + /// + /// A tuple containing: + /// * A boolean indicating success + /// * The return data as a Span + /// * The amount of gas used as a u64 + fn eth_send_transaction(ref self: T, tx: Transaction) -> (bool, Span, u64); + + /// Executes an unsigned transaction. + /// + /// This is a modified version of the eth_sendRawTransaction function. + /// Signature validation should be done before calling this function. + /// + /// # Arguments + /// + /// * `tx_data` - The unsigned transaction data as a Span + /// + /// # Returns + /// + /// A tuple containing: + /// * A boolean indicating success + /// * The return data as a Span + /// * The amount of gas used as a u64 + fn eth_send_raw_unsigned_tx(ref self: T, tx_data: Span) -> (bool, Span, u64); +} + + +#[starknet::embeddable] +pub impl EthRPC< + TContractState, impl KakarotState: KakarotCoreState, +Drop +> of IEthRPC { + fn eth_get_balance(self: @TContractState, address: EthAddress) -> u256 { + panic!("unimplemented") + } + + fn eth_get_transaction_count(self: @TContractState, address: EthAddress) -> u64 { + panic!("unimplemented") + } + + fn eth_chain_id(self: @TContractState) -> u64 { + panic!("unimplemented") + } + + fn eth_call( + self: @TContractState, origin: EthAddress, tx: Transaction + ) -> (bool, Span, u64) { + let mut kakarot_state = KakarotState::get_state(); + if !is_view(@kakarot_state) { + core::panic_with_felt252('fn must be called, not invoked'); + }; + + let origin = Address { + evm: origin, starknet: kakarot_state.compute_starknet_address(origin) + }; + + let TransactionResult { success, return_data, gas_used, state: _state } = + EVMTrait::process_transaction( + ref kakarot_state, origin, tx, tx.effective_gas_price(Option::None), 0 + ); + + (success, return_data, gas_used) + } + + fn eth_estimate_gas( + self: @TContractState, origin: EthAddress, tx: Transaction + ) -> (bool, Span, u64) { + panic!("unimplemented") + } + + //TODO: make this one internal, and the eth_send_raw_unsigned_tx one public + fn eth_send_transaction( + ref self: TContractState, mut tx: Transaction + ) -> (bool, Span, u64) { + let mut kakarot_state = KakarotState::get_state(); + let (gas_price, intrinsic_gas) = validate_eth_tx(@kakarot_state, tx); + + let starknet_caller_address = get_caller_address(); + let account = IAccountDispatcher { contract_address: starknet_caller_address }; + let origin = Address { evm: account.get_evm_address(), starknet: starknet_caller_address }; + + let TransactionResult { success, return_data, gas_used, mut state } = + EVMTrait::process_transaction( + ref kakarot_state, origin, tx, gas_price, intrinsic_gas + ); + starknet_backend::commit(ref state).expect('Committing state failed'); + (success, return_data, gas_used) + } + + fn eth_send_raw_unsigned_tx( + ref self: TContractState, tx_data: Span + ) -> (bool, Span, u64) { + panic!("unimplemented") + } +} + +trait IEthRPCInternal { + fn eth_send_transaction( + ref self: T, origin: EthAddress, tx: Transaction + ) -> (bool, Span, u64); +} + +impl EthRPCInternalImpl> of IEthRPCInternal { + fn eth_send_transaction( + ref self: TContractState, origin: EthAddress, tx: Transaction + ) -> (bool, Span, u64) { + panic!("unimplemented") + } +} + +fn is_view(self: @KakarotCore::ContractState) -> bool { + let tx_info = get_tx_info().unbox(); + + // If the account that originated the transaction is not zero, this means we + // are in an invoke transaction instead of a call; therefore, `eth_call` is being + // wrongly called For invoke transactions, `eth_send_transaction` must be used + if !tx_info.account_contract_address.is_zero() { + return false; + } + true +} diff --git a/crates/contracts/src/kakarot_core/interface.cairo b/crates/contracts/src/kakarot_core/interface.cairo index 8c014f3bf..f5d8318bd 100644 --- a/crates/contracts/src/kakarot_core/interface.cairo +++ b/crates/contracts/src/kakarot_core/interface.cairo @@ -24,17 +24,6 @@ pub trait IKakarotCore { ref self: TContractState, evm_address: EthAddress ) -> ContractAddress; - /// View entrypoint into the EVM - /// Performs view calls into the blockchain - /// It cannot modify the state of the chain - fn eth_call( - self: @TContractState, origin: EthAddress, tx: Transaction - ) -> (bool, Span, u64); - - /// Transaction entrypoint into the EVM - /// Executes an EVM transaction and possibly modifies the state - fn eth_send_transaction(ref self: TContractState, tx: Transaction) -> (bool, Span, u64); - /// Upgrade the KakarotCore smart contract /// Using replace_class_syscall fn upgrade(ref self: TContractState, new_class_hash: ClassHash); diff --git a/crates/contracts/src/kakarot_core/kakarot.cairo b/crates/contracts/src/kakarot_core/kakarot.cairo index a35d41802..9518afd14 100644 --- a/crates/contracts/src/kakarot_core/kakarot.cairo +++ b/crates/contracts/src/kakarot_core/kakarot.cairo @@ -3,9 +3,9 @@ const INVOKE_ETH_CALL_FORBIDDEN: felt252 = 'KKT: Cannot invoke eth_call'; #[starknet::contract] pub mod KakarotCore { - use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait}; use contracts::components::ownable::{ownable_component}; use contracts::components::upgradeable::{IUpgradeable, upgradeable_component}; + use contracts::kakarot_core::eth_rpc; use contracts::kakarot_core::interface::IKakarotCore; use core::num::traits::Zero; use core::starknet::event::EventEmitter; @@ -13,34 +13,14 @@ pub mod KakarotCore { Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, StoragePointerWriteAccess }; - use core::starknet::{ - EthAddress, ContractAddress, ClassHash, get_tx_info, get_contract_address, - get_caller_address - }; + use core::starknet::{EthAddress, ContractAddress, ClassHash, get_contract_address}; use evm::backend::starknet_backend; - use evm::backend::validation::validate_eth_tx; - use evm::model::account::AccountTrait; - use evm::model::{Message, TransactionResult, ExecutionSummaryTrait, Address}; - use evm::precompiles::eth_precompile_addresses; - use evm::state::StateTrait; - use evm::{EVMTrait}; - use utils::address::compute_contract_address; - use utils::eth_transaction::common::TxKind; - use utils::eth_transaction::eip2930::{AccessListItem, AccessListItemTrait}; - use utils::eth_transaction::transaction::{Transaction, TransactionTrait}; use utils::helpers::compute_starknet_address; - use utils::set::{Set, SetTrait}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: upgradeable_component, storage: upgradeable, event: UpgradeableEvent); - #[abi(embed_v0)] - impl OwnableImpl = ownable_component::Ownable; - - impl OwnableInternalImpl = ownable_component::InternalImpl; - - impl UpgradeableImpl = upgradeable_component::Upgradeable; - + /// STORAGE /// #[storage] pub struct Storage { @@ -59,6 +39,8 @@ pub mod KakarotCore { upgradeable: upgradeable_component::Storage, } + /// EVENTS /// + #[event] #[derive(Drop, starknet::Event)] pub enum Event { @@ -91,8 +73,19 @@ pub mod KakarotCore { } - // TODO: add ability to pass Span, which should be deployed along with Kakarot - // this can be done once https://github.com/starkware-libs/cairo/issues/4488 is resolved + /// Trait bounds allowing embedded implementations to be specific to this contract + pub trait KakarotCoreState { + fn get_state() -> ContractState; + } + + impl _KakarotCoreState of KakarotCoreState { + fn get_state() -> ContractState { + unsafe_new_contract_state() + } + } + + /// CONSTRUCTOR /// + #[constructor] fn constructor( ref self: ContractState, @@ -115,6 +108,20 @@ pub mod KakarotCore { }; } + /// PUBLIC-FACING FUNCTIONS /// + + // Public-facing "ownable" functions + #[abi(embed_v0)] + impl OwnableImpl = ownable_component::Ownable; + + /// Public-facing "ethereum" functions + /// Used to make EVM-related actions through Kakarot. + #[abi(embed_v0)] + pub impl EthRPCImpl = eth_rpc::EthRPC; + + + /// Public-facing "kakarot" functions + /// Used to interact with the Kakarot contract outside of EVM-related actions. #[abi(embed_v0)] pub impl KakarotCoreImpl of IKakarotCore { fn set_native_token(ref self: ContractState, native_token: ContractAddress) { @@ -145,38 +152,6 @@ pub mod KakarotCore { starknet_backend::deploy(evm_address).expect('EOA Deployment failed').starknet } - fn eth_call( - self: @ContractState, origin: EthAddress, tx: Transaction - ) -> (bool, Span, u64) { - if !self.is_view() { - core::panic_with_felt252('fn must be called, not invoked'); - }; - - let origin = Address { evm: origin, starknet: self.compute_starknet_address(origin) }; - - let TransactionResult { success, return_data, gas_used, state: _state } = self - .process_transaction(origin, tx, tx.effective_gas_price(Option::None), 0); - - (success, return_data, gas_used) - } - - fn eth_send_transaction( - ref self: ContractState, mut tx: Transaction - ) -> (bool, Span, u64) { - let (gas_price, intrinsic_gas) = validate_eth_tx(@self, tx); - - let starknet_caller_address = get_caller_address(); - let account = IAccountDispatcher { contract_address: starknet_caller_address }; - let origin = Address { - evm: account.get_evm_address(), starknet: starknet_caller_address - }; - - let TransactionResult { success, return_data, gas_used, mut state } = self - .process_transaction(origin, tx, gas_price, intrinsic_gas); - starknet_backend::commit(ref state).expect('Committing state failed'); - (success, return_data, gas_used) - } - fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { self.ownable.assert_only_owner(); self.upgradeable.upgrade_contract(new_class_hash); @@ -245,142 +220,11 @@ pub mod KakarotCore { } } - #[generate_trait] - pub impl KakarotCoreInternalImpl of KakarotCoreInternal { - fn is_view(self: @ContractState) -> bool { - let tx_info = get_tx_info().unbox(); - - // If the account that originated the transaction is not zero, this means we - // are in an invoke transaction instead of a call; therefore, `eth_call` is being - // wrongly called For invoke transactions, `eth_send_transaction` must be used - if !tx_info.account_contract_address.is_zero() { - return false; - } - true - } - - /// Maps an EVM address to a Starknet address - /// Triggered when deployment of an EOA or CA is successful - fn set_address_registry( - ref self: ContractState, evm_address: EthAddress, starknet_address: ContractAddress - ) { - self.Kakarot_evm_to_starknet_address.write(evm_address, starknet_address); - } + /// INTERNAL-FACING FUNCTIONS /// + // Internal-facing "ownable" functions + impl OwnableInternalImpl = ownable_component::InternalImpl; - fn process_transaction( - self: @ContractState, - origin: Address, - tx: Transaction, - gas_price: u128, - intrinsic_gas: u64 - ) -> TransactionResult { - // Charge the cost of intrinsic gas - which has been verified to be <= gas_limit. - let gas_left = tx.gas_limit() - intrinsic_gas; - let mut env = starknet_backend::get_env(origin.evm, gas_price); - - let mut sender_account = env.state.get_account(origin.evm); - sender_account.set_nonce(sender_account.nonce() + 1); - env.state.set_account(sender_account); - - // Handle deploy/non-deploy transaction cases - let (to, is_deploy_tx, code, code_address, calldata) = match tx.kind() { - TxKind::Create => { - // Deploy tx case. - let mut origin_nonce: u64 = get_tx_info().unbox().nonce.try_into().unwrap(); - let to_evm_address = compute_contract_address(origin.evm, origin_nonce); - let to_starknet_address = self.compute_starknet_address(to_evm_address); - let to = Address { evm: to_evm_address, starknet: to_starknet_address }; - let code = tx.input(); - let calldata = [].span(); - (to, true, code, Zero::zero(), calldata) - }, - TxKind::Call(to) => { - let target_starknet_address = self.compute_starknet_address(to); - let to = Address { evm: to, starknet: target_starknet_address }; - let code = env.state.get_account(to.evm).code; - (to, false, code, to, tx.input()) - } - }; - - let mut accessed_addresses: Set = Default::default(); - accessed_addresses.add(env.coinbase); - accessed_addresses.add(to.evm); - accessed_addresses.add(origin.evm); - accessed_addresses.extend(eth_precompile_addresses().spanset()); - - let mut accessed_storage_keys: Set<(EthAddress, u256)> = Default::default(); - - if let Option::Some(mut access_list) = tx.access_list() { - for access_list_item in access_list { - let AccessListItem { ethereum_address, storage_keys: _ } = *access_list_item; - let storage_keys = access_list_item.to_storage_keys(); - accessed_addresses.add(ethereum_address); - accessed_storage_keys.extend_from_span(storage_keys); - } - }; - - let message = Message { - caller: origin, - target: to, - gas_limit: gas_left, - data: calldata, - code, - code_address: code_address, - value: tx.value(), - should_transfer_value: true, - depth: 0, - read_only: false, - accessed_addresses: accessed_addresses.spanset(), - accessed_storage_keys: accessed_storage_keys.spanset(), - }; - let mut summary = EVMTrait::process_message_call(message, env, is_deploy_tx); - - // Gas refunds - let gas_used = tx.gas_limit() - summary.gas_left; - let gas_refund = core::cmp::min(gas_used / 5, summary.gas_refund); - - // Charging gas fees to the sender - // At the end of the tx, the sender must have paid - // (gas_used - gas_refund) * gas_price to the miner - // Because tx.gas_price == env.gas_price, and we checked the sender has enough balance - // to cover the gas fees + the value transfer, this transfer should never fail. - // We can thus directly charge the sender for the effective gas fees, - // without pre-emtively charging for the tx gas fee and then refund. - // This is not true for EIP-1559 transactions - not supported yet. - let total_gas_used = gas_used - gas_refund; - let _transaction_fee = total_gas_used.into() * gas_price; - - //TODO(gas): EF-tests doesn't yet support in-EVM gas charging, they assume that the gas - //charged is always correct for now. - // As correct gas accounting is not an immediate priority, we can just ignore the gas - // charging for now. - // match summary - // .state - // .add_transfer( - // Transfer { - // sender: origin, - // recipient: Address { - // evm: coinbase, starknet: block_info.sequencer_address, - // }, - // amount: transaction_fee.into() - // } - // ) { - // Result::Ok(_) => {}, - // Result::Err(err) => { - // - // return TransactionResultTrait::exceptional_failure( - // err.to_bytes(), tx.gas_limit() - // ); - // } - // }; - - TransactionResult { - success: summary.is_success(), - return_data: summary.return_data, - gas_used: total_gas_used, - state: summary.state, - } - } - } + // Internal-facing "upgradeable" functions + impl UpgradeableImpl = upgradeable_component::Upgradeable; } diff --git a/crates/contracts/tests/test_kakarot_core.cairo b/crates/contracts/tests/test_kakarot_core.cairo index 2e1b82460..f343964f9 100644 --- a/crates/contracts/tests/test_kakarot_core.cairo +++ b/crates/contracts/tests/test_kakarot_core.cairo @@ -3,7 +3,7 @@ use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait}; use contracts::kakarot_core::interface::{ IExtendedKakarotCoreDispatcher, IExtendedKakarotCoreDispatcherTrait }; -use contracts::kakarot_core::{KakarotCore, KakarotCore::KakarotCoreInternal}; +use contracts::kakarot_core::{KakarotCore}; use contracts::test_contracts::test_upgradeable::{ MockContractUpgradeableV1, IMockContractUpgradeableDispatcher, IMockContractUpgradeableDispatcherTrait @@ -268,77 +268,6 @@ fn test_eth_call() { assert_eq!(return_data, u256_to_bytes_array(1).span()); } -#[test] -fn test_process_transaction() { - // Pre - test_utils::setup_test_environment(); - let chain_id = chain_id(); - - // Given - let eoa_evm_address = test_utils::evm_address(); - let eoa_starknet_address = utils::helpers::compute_starknet_address( - test_address(), eoa_evm_address, test_utils::uninitialized_account() - ); - test_utils::register_account(eoa_evm_address, eoa_starknet_address); - start_mock_call::(eoa_starknet_address, selector!("get_nonce"), 0); - start_mock_call::>(eoa_starknet_address, selector!("bytecode"), [].span()); - start_mock_call::(eoa_starknet_address, selector!("get_code_hash"), EMPTY_KECCAK); - - let contract_evm_address = test_utils::other_evm_address(); - let contract_starknet_address = utils::helpers::compute_starknet_address( - test_address(), contract_evm_address, test_utils::uninitialized_account() - ); - test_utils::register_account(contract_evm_address, contract_starknet_address); - start_mock_call::(contract_starknet_address, selector!("get_nonce"), 1); - let counter_evm_bytecode = counter_evm_bytecode(); - start_mock_call::< - Span - >(contract_starknet_address, selector!("bytecode"), counter_evm_bytecode); - - start_mock_call::< - u256 - >( - contract_starknet_address, - selector!("get_code_hash"), - counter_evm_bytecode.compute_keccak256_hash() - ); - start_mock_call::(contract_starknet_address, selector!("storage"), 0); - - let nonce = 0; - let gas_limit = test_utils::tx_gas_limit(); - let gas_price = test_utils::gas_price(); - let value = 0; - // selector: function get() - let input = [0x6d, 0x4c, 0xe6, 0x3c].span(); - - let tx = TxLegacy { - chain_id: Option::Some(chain_id), - nonce, - to: contract_evm_address.into(), - value, - gas_price, - gas_limit, - input - }; - - // When - let mut kakarot_core = KakarotCore::unsafe_new_contract_state(); - start_mock_call::< - u256 - >(test_utils::native_token(), selector!("balanceOf"), 0xfffffffffffffffffffffffffff); - let result = kakarot_core - .process_transaction( - origin: Address { evm: eoa_evm_address, starknet: eoa_starknet_address }, - tx: Transaction::Legacy(tx), - :gas_price, - intrinsic_gas: 0 - ); - let return_data = result.return_data; - - // Then - assert!(result.success); - assert_eq!(return_data, u256_to_bytes_array(0).span()); -} #[test] fn test_eth_send_transaction_deploy_tx() { diff --git a/crates/evm/src/instructions/system_operations.cairo b/crates/evm/src/instructions/system_operations.cairo index 7bfe4c8f5..525f71f7e 100644 --- a/crates/evm/src/instructions/system_operations.cairo +++ b/crates/evm/src/instructions/system_operations.cairo @@ -761,7 +761,7 @@ mod tests { 0xf2, 0x00 ].span(); - let code_hash = bytecode.compute_keccak256_hash(); + let _code_hash = bytecode.compute_keccak256_hash(); let mut vm = VMBuilderTrait::new_with_presets().with_bytecode(bytecode).build(); let eoa_account = Account { address: vm.message().target, diff --git a/crates/evm/src/interpreter.cairo b/crates/evm/src/interpreter.cairo index e3225aa22..3bfc2bd19 100644 --- a/crates/evm/src/interpreter.cairo +++ b/crates/evm/src/interpreter.cairo @@ -1,3 +1,9 @@ +use contracts::kakarot_core::KakarotCore; +use contracts::kakarot_core::interface::IKakarotCore; +use core::num::traits::Zero; +use core::starknet::EthAddress; +use core::starknet::get_tx_info; +use evm::backend::starknet_backend; use evm::create_helpers::CreateHelpers; use evm::errors::{EVMError, EVMErrorTrait, CONTRACT_ACCOUNT_EXISTS}; @@ -12,15 +18,137 @@ use evm::model::account::{AccountTrait}; use evm::model::vm::{VM, VMTrait}; use evm::model::{ Message, Environment, Transfer, ExecutionSummary, ExecutionSummaryTrait, ExecutionResult, - ExecutionResultTrait, ExecutionResultStatus, AddressTrait + ExecutionResultTrait, ExecutionResultStatus, AddressTrait, TransactionResult, Address }; use evm::precompiles::Precompiles; +use evm::precompiles::eth_precompile_addresses; use evm::state::StateTrait; +use utils::address::compute_contract_address; use utils::constants; +use utils::eth_transaction::common::TxKind; +use utils::eth_transaction::eip2930::{AccessListItem, AccessListItemTrait}; +use utils::eth_transaction::transaction::{Transaction, TransactionTrait}; use utils::helpers::EthAddressExTrait; +use utils::set::{Set, SetTrait}; #[generate_trait] pub impl EVMImpl of EVMTrait { + fn process_transaction( + ref self: KakarotCore::ContractState, + origin: Address, + tx: Transaction, + gas_price: u128, + intrinsic_gas: u64 + ) -> TransactionResult { + // Charge the cost of intrinsic gas - which has been verified to be <= gas_limit. + let gas_left = tx.gas_limit() - intrinsic_gas; + let mut env = starknet_backend::get_env(origin.evm, gas_price); + + let mut sender_account = env.state.get_account(origin.evm); + sender_account.set_nonce(sender_account.nonce() + 1); + env.state.set_account(sender_account); + + // Handle deploy/non-deploy transaction cases + let (to, is_deploy_tx, code, code_address, calldata) = match tx.kind() { + TxKind::Create => { + // Deploy tx case. + let mut origin_nonce: u64 = get_tx_info().unbox().nonce.try_into().unwrap(); + let to_evm_address = compute_contract_address(origin.evm, origin_nonce); + let to_starknet_address = self.compute_starknet_address(to_evm_address); + let to = Address { evm: to_evm_address, starknet: to_starknet_address }; + let code = tx.input(); + let calldata = [].span(); + (to, true, code, Zero::zero(), calldata) + }, + TxKind::Call(to) => { + let target_starknet_address = self.compute_starknet_address(to); + let to = Address { evm: to, starknet: target_starknet_address }; + let code = env.state.get_account(to.evm).code; + (to, false, code, to, tx.input()) + } + }; + + let mut accessed_addresses: Set = Default::default(); + accessed_addresses.add(env.coinbase); + accessed_addresses.add(to.evm); + accessed_addresses.add(origin.evm); + accessed_addresses.extend(eth_precompile_addresses().spanset()); + + let mut accessed_storage_keys: Set<(EthAddress, u256)> = Default::default(); + + if let Option::Some(mut access_list) = tx.access_list() { + for access_list_item in access_list { + let AccessListItem { ethereum_address, storage_keys: _ } = *access_list_item; + let storage_keys = access_list_item.to_storage_keys(); + accessed_addresses.add(ethereum_address); + accessed_storage_keys.extend_from_span(storage_keys); + } + }; + + let message = Message { + caller: origin, + target: to, + gas_limit: gas_left, + data: calldata, + code, + code_address: code_address, + value: tx.value(), + should_transfer_value: true, + depth: 0, + read_only: false, + accessed_addresses: accessed_addresses.spanset(), + accessed_storage_keys: accessed_storage_keys.spanset(), + }; + let mut summary = EVMTrait::process_message_call(message, env, is_deploy_tx); + + // Gas refunds + let gas_used = tx.gas_limit() - summary.gas_left; + let gas_refund = core::cmp::min(gas_used / 5, summary.gas_refund); + + // Charging gas fees to the sender + // At the end of the tx, the sender must have paid + // (gas_used - gas_refund) * gas_price to the miner + // Because tx.gas_price == env.gas_price, and we checked the sender has enough balance + // to cover the gas fees + the value transfer, this transfer should never fail. + // We can thus directly charge the sender for the effective gas fees, + // without pre-emtively charging for the tx gas fee and then refund. + // This is not true for EIP-1559 transactions - not supported yet. + let total_gas_used = gas_used - gas_refund; + let _transaction_fee = total_gas_used.into() * gas_price; + + //TODO(gas): EF-tests doesn't yet support in-EVM gas charging, they assume that the gas + //charged is always correct for now. + // As correct gas accounting is not an immediate priority, we can just ignore the gas + // charging for now. + // match summary + // .state + // .add_transfer( + // Transfer { + // sender: origin, + // recipient: Address { + // evm: coinbase, starknet: block_info.sequencer_address, + // }, + // amount: transaction_fee.into() + // } + // ) { + // Result::Ok(_) => {}, + // Result::Err(err) => { + // + // return TransactionResultTrait::exceptional_failure( + // err.to_bytes(), tx.gas_limit() + // ); + // } + // }; + + TransactionResult { + success: summary.is_success(), + return_data: summary.return_data, + gas_used: total_gas_used, + state: summary.state, + } + } + + fn process_message_call( message: Message, mut env: Environment, is_deploy_tx: bool, ) -> ExecutionSummary { @@ -838,3 +966,210 @@ pub impl EVMImpl of EVMTrait { return Result::Err(EVMError::InvalidOpcode(opcode)); } } + +pub(crate) fn process_transaction( + ref self: KakarotCore::ContractState, + origin: Address, + tx: Transaction, + gas_price: u128, + intrinsic_gas: u64 +) -> TransactionResult { + // Charge the cost of intrinsic gas - which has been verified to be <= gas_limit. + let gas_left = tx.gas_limit() - intrinsic_gas; + let mut env = starknet_backend::get_env(origin.evm, gas_price); + + let mut sender_account = env.state.get_account(origin.evm); + sender_account.set_nonce(sender_account.nonce() + 1); + env.state.set_account(sender_account); + + // Handle deploy/non-deploy transaction cases + let (to, is_deploy_tx, code, code_address, calldata) = match tx.kind() { + TxKind::Create => { + // Deploy tx case. + let mut origin_nonce: u64 = get_tx_info().unbox().nonce.try_into().unwrap(); + let to_evm_address = compute_contract_address(origin.evm, origin_nonce); + let to_starknet_address = self.compute_starknet_address(to_evm_address); + let to = Address { evm: to_evm_address, starknet: to_starknet_address }; + let code = tx.input(); + let calldata = [].span(); + (to, true, code, Zero::zero(), calldata) + }, + TxKind::Call(to) => { + let target_starknet_address = self.compute_starknet_address(to); + let to = Address { evm: to, starknet: target_starknet_address }; + let code = env.state.get_account(to.evm).code; + (to, false, code, to, tx.input()) + } + }; + + let mut accessed_addresses: Set = Default::default(); + accessed_addresses.add(env.coinbase); + accessed_addresses.add(to.evm); + accessed_addresses.add(origin.evm); + accessed_addresses.extend(eth_precompile_addresses().spanset()); + + let mut accessed_storage_keys: Set<(EthAddress, u256)> = Default::default(); + + if let Option::Some(mut access_list) = tx.access_list() { + for access_list_item in access_list { + let AccessListItem { ethereum_address, storage_keys: _ } = *access_list_item; + let storage_keys = access_list_item.to_storage_keys(); + accessed_addresses.add(ethereum_address); + accessed_storage_keys.extend_from_span(storage_keys); + } + }; + + let message = Message { + caller: origin, + target: to, + gas_limit: gas_left, + data: calldata, + code, + code_address: code_address, + value: tx.value(), + should_transfer_value: true, + depth: 0, + read_only: false, + accessed_addresses: accessed_addresses.spanset(), + accessed_storage_keys: accessed_storage_keys.spanset(), + }; + let mut summary = EVMTrait::process_message_call(message, env, is_deploy_tx); + + // Gas refunds + let gas_used = tx.gas_limit() - summary.gas_left; + let gas_refund = core::cmp::min(gas_used / 5, summary.gas_refund); + + // Charging gas fees to the sender + // At the end of the tx, the sender must have paid + // (gas_used - gas_refund) * gas_price to the miner + // Because tx.gas_price == env.gas_price, and we checked the sender has enough balance + // to cover the gas fees + the value transfer, this transfer should never fail. + // We can thus directly charge the sender for the effective gas fees, + // without pre-emtively charging for the tx gas fee and then refund. + // This is not true for EIP-1559 transactions - not supported yet. + let total_gas_used = gas_used - gas_refund; + let _transaction_fee = total_gas_used.into() * gas_price; + + //TODO(gas): EF-tests doesn't yet support in-EVM gas charging, they assume that the gas + //charged is always correct for now. + // As correct gas accounting is not an immediate priority, we can just ignore the gas + // charging for now. + // match summary + // .state + // .add_transfer( + // Transfer { + // sender: origin, + // recipient: Address { + // evm: coinbase, starknet: block_info.sequencer_address, + // }, + // amount: transaction_fee.into() + // } + // ) { + // Result::Ok(_) => {}, + // Result::Err(err) => { + // + // return TransactionResultTrait::exceptional_failure( + // err.to_bytes(), tx.gas_limit() + // ); + // } + // }; + + TransactionResult { + success: summary.is_success(), + return_data: summary.return_data, + gas_used: total_gas_used, + state: summary.state, + } +} + + +#[cfg(test)] +mod tests { + use contracts::kakarot_core::KakarotCore; + use contracts::test_data::counter_evm_bytecode; + use evm::model::Address; + use evm::test_utils::{ + setup_test_environment, native_token, chain_id, tx_gas_limit, gas_price, register_account, + other_evm_address, uninitialized_account, evm_address + }; + use snforge_std::{start_mock_call, test_address}; + + use super::process_transaction; + use utils::constants::EMPTY_KECCAK; + use utils::eth_transaction::legacy::{TxLegacy}; + use utils::eth_transaction::transaction::{Transaction}; + use utils::helpers::{U8SpanExTrait, u256_to_bytes_array}; + + + #[test] + fn test_process_transaction() { + // Pre + setup_test_environment(); + let chain_id = chain_id(); + + // Given + let eoa_evm_address = evm_address(); + let eoa_starknet_address = utils::helpers::compute_starknet_address( + test_address(), eoa_evm_address, uninitialized_account() + ); + register_account(eoa_evm_address, eoa_starknet_address); + start_mock_call::(eoa_starknet_address, selector!("get_nonce"), 0); + start_mock_call::>(eoa_starknet_address, selector!("bytecode"), [].span()); + start_mock_call::(eoa_starknet_address, selector!("get_code_hash"), EMPTY_KECCAK); + + let contract_evm_address = other_evm_address(); + let contract_starknet_address = utils::helpers::compute_starknet_address( + test_address(), contract_evm_address, uninitialized_account() + ); + register_account(contract_evm_address, contract_starknet_address); + start_mock_call::(contract_starknet_address, selector!("get_nonce"), 1); + let counter_evm_bytecode = counter_evm_bytecode(); + start_mock_call::< + Span + >(contract_starknet_address, selector!("bytecode"), counter_evm_bytecode); + + start_mock_call::< + u256 + >( + contract_starknet_address, + selector!("get_code_hash"), + counter_evm_bytecode.compute_keccak256_hash() + ); + start_mock_call::(contract_starknet_address, selector!("storage"), 0); + + let nonce = 0; + let gas_limit = tx_gas_limit(); + let gas_price = gas_price(); + let value = 0; + // selector: function get() + let input = [0x6d, 0x4c, 0xe6, 0x3c].span(); + + let tx = TxLegacy { + chain_id: Option::Some(chain_id), + nonce, + to: contract_evm_address.into(), + value, + gas_price, + gas_limit, + input + }; + + // When + let mut kakarot_core = KakarotCore::unsafe_new_contract_state(); + start_mock_call::< + u256 + >(native_token(), selector!("balanceOf"), 0xfffffffffffffffffffffffffff); + let result = process_transaction( + ref kakarot_core, + origin: Address { evm: eoa_evm_address, starknet: eoa_starknet_address }, + tx: Transaction::Legacy(tx), + :gas_price, + intrinsic_gas: 0 + ); + let return_data = result.return_data; + + // Then + assert!(result.success); + assert_eq!(return_data, u256_to_bytes_array(0).span()); + } +}