From c66fe800cc78b9c827f7f989a539ee8d2939e6b3 Mon Sep 17 00:00:00 2001 From: Mathieu <60658558+enitrat@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:32:26 +0200 Subject: [PATCH] refactor: eth transaction model (#922) * refactor: eth transaction model * lint * TransactionUnsigned model * lint * fmt * apply suggestions from review --- crates/contracts/src/account_contract.cairo | 159 ++-- crates/contracts/src/cairo1_helpers.cairo | 4 +- .../src/kakarot_core/interface.cairo | 25 +- .../contracts/src/kakarot_core/kakarot.cairo | 58 +- crates/contracts/src/test_utils.cairo | 20 +- crates/contracts/tests/lib.cairo | 2 - crates/contracts/tests/test_eoa.cairo | 23 +- .../tests/test_execution_from_outside.cairo | 25 +- .../contracts/tests/test_kakarot_core.cairo | 90 +- crates/contracts/tests/test_utils.cairo | 149 ---- crates/evm/src/backend/starknet_backend.cairo | 10 +- crates/evm/src/call_helpers.cairo | 2 +- crates/evm/src/gas.cairo | 179 ++-- .../src/instructions/block_information.cairo | 4 +- .../environmental_information.cairo | 8 +- .../src/instructions/memory_operations.cairo | 14 +- crates/evm/src/instructions/sha3.cairo | 2 +- .../src/instructions/system_operations.cairo | 16 +- crates/evm/src/model.cairo | 26 +- crates/evm/src/model/account.cairo | 14 +- crates/evm/src/model/vm.cairo | 10 +- crates/evm/src/precompiles.cairo | 2 +- crates/evm/src/precompiles/blake2f.cairo | 8 +- crates/evm/src/precompiles/ec_add.cairo | 4 +- crates/evm/src/precompiles/ec_mul.cairo | 4 +- crates/evm/src/precompiles/ec_recover.cairo | 10 +- crates/evm/src/precompiles/identity.cairo | 12 +- crates/evm/src/precompiles/modexp.cairo | 4 +- crates/evm/src/precompiles/p256verify.cairo | 12 +- crates/evm/src/precompiles/sha256.cairo | 12 +- crates/evm/src/stack.cairo | 40 + crates/evm/src/state.cairo | 18 +- crates/evm/src/test_utils.cairo | 16 +- crates/utils/src/constants.cairo | 5 +- crates/utils/src/errors.cairo | 19 +- crates/utils/src/eth_transaction.cairo | 839 +----------------- crates/utils/src/eth_transaction/common.cairo | 51 ++ .../utils/src/eth_transaction/eip1559.cairo | 127 +++ .../utils/src/eth_transaction/eip2930.cairo | 119 +++ crates/utils/src/eth_transaction/legacy.cairo | 39 + .../src/eth_transaction/transaction.cairo | 532 +++++++++++ .../utils/src/eth_transaction/tx_type.cairo | 23 + .../src/eth_transaction/validation.cairo | 189 ++++ crates/utils/src/rlp.cairo | 26 +- crates/utils/src/serialization.cairo | 30 +- crates/utils/src/traits.cairo | 8 + 46 files changed, 1629 insertions(+), 1360 deletions(-) delete mode 100644 crates/contracts/tests/test_utils.cairo create mode 100644 crates/utils/src/eth_transaction/common.cairo create mode 100644 crates/utils/src/eth_transaction/eip1559.cairo create mode 100644 crates/utils/src/eth_transaction/eip2930.cairo create mode 100644 crates/utils/src/eth_transaction/legacy.cairo create mode 100644 crates/utils/src/eth_transaction/transaction.cairo create mode 100644 crates/utils/src/eth_transaction/tx_type.cairo create mode 100644 crates/utils/src/eth_transaction/validation.cairo diff --git a/crates/contracts/src/account_contract.cairo b/crates/contracts/src/account_contract.cairo index 05640cc1f..45628e297 100644 --- a/crates/contracts/src/account_contract.cairo +++ b/crates/contracts/src/account_contract.cairo @@ -63,16 +63,17 @@ pub mod AccountContract { }; use core::starknet::syscalls::{call_contract_syscall, replace_class_syscall}; use core::starknet::{ - EthAddress, ClassHash, VALIDATED, get_caller_address, get_contract_address, get_tx_info, - get_block_timestamp + EthAddress, ClassHash, VALIDATED, get_caller_address, get_tx_info, get_block_timestamp }; use core::traits::TryInto; use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; use super::{IAccountLibraryDispatcher, IAccountDispatcherTrait, OutsideExecution}; use utils::constants::{POW_2_32}; - use utils::eth_transaction::EthereumTransactionTrait; - use utils::eth_transaction::{EthTransactionTrait, TransactionMetadata}; + use utils::eth_transaction::transaction::{TransactionUnsignedTrait, Transaction}; + use utils::eth_transaction::validation::validate_eth_tx; + use utils::eth_transaction::{TransactionMetadata}; use utils::serialization::{deserialize_signature, deserialize_bytes, serialize_bytes}; + use utils::traits::DefaultSignature; // Add ownable component component!(path: ownable_component, storage: ownable, event: OwnableEvent); @@ -109,7 +110,7 @@ pub mod AccountContract { pub struct TransactionExecuted { pub response: Span, pub success: bool, - pub gas_used: u128 + pub gas_used: u64 } #[constructor] @@ -177,7 +178,7 @@ pub mod AccountContract { "Validate: selector must be eth_send_transaction" ); - let chain_id: u128 = tx_info.chain_id.try_into().unwrap() % POW_2_32; + let chain_id: u64 = tx_info.chain_id.try_into().unwrap() % POW_2_32.try_into().unwrap(); let signature = deserialize_signature(tx_info.signature, chain_id) .expect('EOA: invalid signature'); @@ -188,11 +189,12 @@ pub mod AccountContract { signature }; - let encoded_tx = deserialize_bytes(*call.calldata) - .expect('conversion to Span failed'); - let validation_result = EthTransactionTrait::validate_eth_tx( - tx_metadata, encoded_tx.span() - ) + let mut encoded_tx = deserialize_bytes(*call.calldata) + .expect('conversion to Span failed') + .span(); + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx) + .expect('EOA: could not decode tx'); + let validation_result = validate_eth_tx(tx_metadata, unsigned_transaction) .expect('failed to validate eth tx'); assert(validation_result, 'transaction validation failed'); @@ -229,17 +231,19 @@ pub mod AccountContract { self.Account_nonce.write(tx_info.nonce.try_into().unwrap() + 1); let call: @Call = calls[0]; - let encoded_tx = deserialize_bytes(*call.calldata).expect('conversion failed').span(); - let tx = EthTransactionTrait::decode(encoded_tx).expect('rlp decoding of tx failed'); + let mut encoded_tx = deserialize_bytes(*call.calldata) + .expect('conversion to Span failed') + .span(); + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx) + .expect('EOA: could not decode tx'); - let is_valid = match tx.try_into_fee_market_transaction() { - Option::Some(tx_fee_infos) => { self.validate_eip1559_tx(@tx, tx_fee_infos) }, - Option::None => true - }; + //TODO: validation of EIP-1559 transactions + // Not done because this endpoint will end up deprecated after EIP-1559 + let is_valid = true; let (success, return_data, gas_used) = if is_valid { - kakarot.eth_send_transaction(tx) + kakarot.eth_send_transaction(unsigned_transaction.transaction) } else { (false, KAKAROT_VALIDATION_FAILED.span(), 0) }; @@ -319,11 +323,16 @@ pub mod AccountContract { *call.selector == selector!("eth_send_transaction"), "selector must be eth_send_transaction" ); - - let chain_id: u128 = tx_info.chain_id.try_into().unwrap() % POW_2_32; - - let signature = deserialize_signature(signature, chain_id).expect('invalid signature'); - + let chain_id: u64 = tx_info.chain_id.try_into().unwrap() % POW_2_32.try_into().unwrap(); + let signature = deserialize_signature(signature, chain_id) + .expect('EOA: invalid signature'); + let mut encoded_tx_data = deserialize_bytes((*outside_execution.calls[0]).calldata) + .expect('conversion to Span failed') + .span(); + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped( + ref encoded_tx_data + ) + .expect('EOA: could not decode tx'); // TODO(execute-from-outside): move validation to KakarotCore let tx_metadata = TransactionMetadata { address: self.Account_evm_address.read(), @@ -332,26 +341,23 @@ pub mod AccountContract { signature }; - let encoded_tx = deserialize_bytes((*outside_execution.calls[0]).calldata) - .expect('conversion to Span failed') - .span(); - - let validation_result = EthTransactionTrait::validate_eth_tx(tx_metadata, encoded_tx) + let validation_result = validate_eth_tx(tx_metadata, unsigned_transaction) .expect('failed to validate eth tx'); assert(validation_result, 'transaction validation failed'); - let tx = EthTransactionTrait::decode(encoded_tx).expect('rlp decoding of tx failed'); - - let is_valid = match tx.try_into_fee_market_transaction() { - Option::Some(tx_fee_infos) => { self.validate_eip1559_tx(@tx, tx_fee_infos) }, - Option::None => true - }; + //TODO: validate eip1559 transactions + // let is_valid = match tx.try_into_fee_market_transaction() { + // Option::Some(tx_fee_infos) => { self.validate_eip1559_tx(@tx, tx_fee_infos) }, + // Option::None => true + // }; + let is_valid = true; let kakarot = IKakarotCoreDispatcher { contract_address: self.ownable.owner() }; let return_data = if is_valid { - let (_, return_data, _) = kakarot.eth_send_transaction(tx); + let (_, return_data, _) = kakarot + .eth_send_transaction(unsigned_transaction.transaction); return_data } else { KAKAROT_VALIDATION_FAILED.span() @@ -364,49 +370,46 @@ pub mod AccountContract { #[generate_trait] impl Eip1559TransactionImpl of Eip1559TransactionTrait { - fn validate_eip1559_tx( - ref self: ContractState, - tx: @utils::eth_transaction::EthereumTransaction, - tx_fee_infos: utils::eth_transaction::FeeMarketTransaction - ) -> 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; - } + //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/cairo1_helpers.cairo b/crates/contracts/src/cairo1_helpers.cairo index 08241e7b8..3beac0a78 100644 --- a/crates/contracts/src/cairo1_helpers.cairo +++ b/crates/contracts/src/cairo1_helpers.cairo @@ -16,7 +16,7 @@ pub trait IPrecompiles { /// * A boolean indicating the success or failure of the execution. /// * The gas cost of the execution. /// * The output data from the execution. - fn exec_precompile(self: @T, address: felt252, data: Span) -> (bool, u128, Span); + fn exec_precompile(self: @T, address: felt252, data: Span) -> (bool, u64, Span); } #[starknet::interface] @@ -134,7 +134,7 @@ pub mod embeddable_impls { pub impl Precompiles of super::IPrecompiles { fn exec_precompile( self: @TContractState, address: felt252, data: Span - ) -> (bool, u128, Span) { + ) -> (bool, u64, Span) { let result = match address { 0 => Result::Err(EVMError::NotImplemented), 1 => Result::Err(EVMError::NotImplemented), diff --git a/crates/contracts/src/kakarot_core/interface.cairo b/crates/contracts/src/kakarot_core/interface.cairo index b787d2ae1..7addae72e 100644 --- a/crates/contracts/src/kakarot_core/interface.cairo +++ b/crates/contracts/src/kakarot_core/interface.cairo @@ -1,5 +1,5 @@ use core::starknet::{ContractAddress, EthAddress, ClassHash}; -use utils::eth_transaction::EthereumTransaction; +use utils::eth_transaction::transaction::Transaction; #[starknet::interface] pub trait IKakarotCore { @@ -28,14 +28,12 @@ pub trait IKakarotCore { /// Performs view calls into the blockchain /// It cannot modify the state of the chain fn eth_call( - self: @TContractState, origin: EthAddress, tx: EthereumTransaction - ) -> (bool, Span, u128); + 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: EthereumTransaction - ) -> (bool, Span, u128); + fn eth_send_transaction(ref self: TContractState, tx: Transaction) -> (bool, Span, u64); /// Upgrade the KakarotCore smart contract /// Using replace_class_syscall @@ -53,9 +51,10 @@ pub trait IKakarotCore { fn register_account(ref self: TContractState, evm_address: EthAddress); // Getter for the Block Gas Limit - fn get_block_gas_limit(self: @TContractState) -> u128; + fn get_block_gas_limit(self: @TContractState) -> u64; + // Getter for the Base Fee - fn get_base_fee(self: @TContractState) -> u128; + fn get_base_fee(self: @TContractState) -> u64; // Getter for the Starknet Address fn get_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress; @@ -88,12 +87,12 @@ pub trait IExtendedKakarotCore { /// Performs view calls into the blockchain /// It cannot modify the state of the chain fn eth_call( - self: @TContractState, origin: EthAddress, tx: EthereumTransaction - ) -> (bool, Span); + 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: EthereumTransaction) -> (bool, Span); + fn eth_send_transaction(ref self: TContractState, tx: Transaction) -> (bool, Span, u64); /// Upgrade the KakarotCore smart contract /// Using replace_class_syscall @@ -111,9 +110,9 @@ pub trait IExtendedKakarotCore { fn register_account(ref self: TContractState, evm_address: EthAddress); // Getter for the Block Gas Limit - fn get_block_gas_limit(self: @TContractState) -> u128; + fn get_block_gas_limit(self: @TContractState) -> u64; // Getter for the Base Fee - fn get_base_fee(self: @TContractState) -> u128; + fn get_base_fee(self: @TContractState) -> u64; // Getter for the Starknet Address fn get_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress; diff --git a/crates/contracts/src/kakarot_core/kakarot.cairo b/crates/contracts/src/kakarot_core/kakarot.cairo index 93f6c5493..954a40468 100644 --- a/crates/contracts/src/kakarot_core/kakarot.cairo +++ b/crates/contracts/src/kakarot_core/kakarot.cairo @@ -28,8 +28,9 @@ pub mod KakarotCore { use evm::state::StateTrait; use evm::{EVMTrait}; use utils::address::compute_contract_address; - use utils::eth_transaction::AccessListItemTrait; - use utils::eth_transaction::{EthereumTransaction, EthereumTransactionTrait, AccessListItem}; + 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}; @@ -51,9 +52,9 @@ pub mod KakarotCore { pub Kakarot_account_contract_class_hash: ClassHash, pub Kakarot_native_token_address: ContractAddress, pub Kakarot_coinbase: EthAddress, - pub Kakarot_base_fee: u128, + pub Kakarot_base_fee: u64, pub Kakarot_prev_randao: u256, - pub Kakarot_block_gas_limit: u128, + pub Kakarot_block_gas_limit: u64, // Components #[substorage(v0)] ownable: ownable_component::Storage, @@ -103,7 +104,7 @@ pub mod KakarotCore { account_contract_class_hash: ClassHash, uninitialized_account_class_hash: ClassHash, coinbase: EthAddress, - block_gas_limit: u128, + block_gas_limit: u64, mut eoas_to_deploy: Span, ) { self.ownable.initializer(owner); @@ -148,8 +149,8 @@ pub mod KakarotCore { } fn eth_call( - self: @ContractState, origin: EthAddress, tx: EthereumTransaction - ) -> (bool, Span, u128) { + self: @ContractState, origin: EthAddress, tx: Transaction + ) -> (bool, Span, u64) { if !self.is_view() { core::panic_with_felt252('fn must be called, not invoked'); }; @@ -162,9 +163,7 @@ pub mod KakarotCore { (success, return_data, gas_used) } - fn eth_send_transaction( - ref self: ContractState, tx: EthereumTransaction - ) -> (bool, Span, u128) { + fn eth_send_transaction(ref self: ContractState, tx: Transaction) -> (bool, Span, u64) { let starknet_caller_address = get_caller_address(); let account = IAccountDispatcher { contract_address: starknet_caller_address }; let origin = Address { @@ -216,12 +215,12 @@ pub mod KakarotCore { self.emit(AccountDeployed { evm_address, starknet_address }); } - fn get_block_gas_limit(self: @ContractState) -> u128 { + fn get_block_gas_limit(self: @ContractState) -> u64 { self.Kakarot_block_gas_limit.read() } - fn get_base_fee(self: @ContractState) -> u128 { + fn get_base_fee(self: @ContractState) -> u64 { self.Kakarot_base_fee.read() } @@ -265,15 +264,15 @@ pub mod KakarotCore { fn process_transaction( - self: @ContractState, origin: Address, tx: EthereumTransaction + self: @ContractState, origin: Address, tx: Transaction ) -> TransactionResult { //TODO(gas) handle FeeMarketTransaction - let gas_price = tx.gas_price(); + let gas_price = tx.max_fee_per_gas(); let gas_limit = tx.gas_limit(); let mut env = starknet_backend::get_env(origin.evm, gas_price); // TX Gas - let gas_fee = gas_limit * gas_price; + let gas_fee = gas_limit.into() * gas_price; let mut sender_account = env.state.get_account(origin.evm); let sender_balance = sender_account.balance(); match ensure( @@ -281,9 +280,7 @@ pub mod KakarotCore { ) { Result::Ok(_) => {}, Result::Err(err) => { - return TransactionResultTrait::exceptional_failure( - err.to_bytes(), tx.gas_limit() - ); + return TransactionResultTrait::exceptional_failure(err.to_bytes(), gas_limit); } }; @@ -291,29 +288,28 @@ pub mod KakarotCore { Option::Some(gas_left) => gas_left, Option::None => { return TransactionResultTrait::exceptional_failure( - EVMError::OutOfGas.to_bytes(), tx.gas_limit() + EVMError::OutOfGas.to_bytes(), gas_limit ); } }; - // Handle deploy/non-deploy transaction cases - let (to, is_deploy_tx, code, code_address, calldata) = match tx.destination() { - Option::Some(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.calldata()) - }, - Option::None => { + 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.calldata(); + 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(); @@ -324,7 +320,7 @@ pub mod KakarotCore { let mut accessed_storage_keys: Set<(EthAddress, u256)> = Default::default(); - if let Option::Some(mut access_list) = tx.try_access_list() { + 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(); @@ -363,7 +359,7 @@ pub mod KakarotCore { // 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 * gas_price; + 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. diff --git a/crates/contracts/src/test_utils.cairo b/crates/contracts/src/test_utils.cairo index 2846d0145..e391d35fa 100644 --- a/crates/contracts/src/test_utils.cairo +++ b/crates/contracts/src/test_utils.cairo @@ -7,14 +7,15 @@ use core::starknet::syscalls::deploy_syscall; use core::starknet::{EthAddress, ContractAddress}; use evm::model::{Address}; -use evm::test_utils::{other_starknet_address, sequencer_evm_address}; +use evm::test_utils::{other_starknet_address, sequencer_evm_address, chain_id}; use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; +use snforge_std::start_cheat_chain_id_global; use snforge_std::{ declare, DeclareResultTrait, start_cheat_caller_address, start_cheat_sequencer_address_global, stop_cheat_caller_address, start_cheat_caller_address_global }; use utils::constants::BLOCK_GAS_LIMIT; -use utils::eth_transaction::LegacyTransaction; +use utils::eth_transaction::legacy::TxLegacy; pub mod constants { @@ -107,10 +108,16 @@ pub fn deploy_eoa( } pub fn call_transaction( - chain_id: u128, destination: Option, calldata: Span -) -> LegacyTransaction { - LegacyTransaction { - chain_id, nonce: 0, gas_price: 0, gas_limit: 500000000, destination, amount: 0, calldata + chain_id: u64, destination: Option, input: Span +) -> TxLegacy { + TxLegacy { + chain_id: Option::Some(chain_id), + nonce: 0, + gas_price: 0, + gas_limit: 500000000, + to: destination.into(), + value: 0, + input } } @@ -133,5 +140,6 @@ pub fn setup_contracts_for_testing() -> (IERC20CamelDispatcher, IExtendedKakarot let sequencer_sn_address = kakarot_core.address_registry(sequencer); start_cheat_sequencer_address_global(sequencer_sn_address); start_cheat_caller_address_global(kakarot_core.contract_address); + start_cheat_chain_id_global(chain_id().into()); return (native_token, kakarot_core); } diff --git a/crates/contracts/tests/lib.cairo b/crates/contracts/tests/lib.cairo index a014dfc5b..ca7f92038 100644 --- a/crates/contracts/tests/lib.cairo +++ b/crates/contracts/tests/lib.cairo @@ -9,5 +9,3 @@ mod test_execution_from_outside; mod test_kakarot_core; mod test_ownable; - -mod test_utils; diff --git a/crates/contracts/tests/test_eoa.cairo b/crates/contracts/tests/test_eoa.cairo index 7beb6835d..8372fa82b 100644 --- a/crates/contracts/tests/test_eoa.cairo +++ b/crates/contracts/tests/test_eoa.cairo @@ -36,9 +36,8 @@ use snforge_std::{ cheat_caller_address }; use snforge_utils::snforge_utils::{ContractEvents, ContractEventsTrait, EventsFilterBuilderTrait}; -use utils::eth_transaction::{ - TransactionType, EthereumTransaction, EthereumTransactionTrait, LegacyTransaction -}; +use utils::eth_transaction::transaction::Transaction; +use utils::eth_transaction::tx_type::TxType; use utils::helpers::{U8SpanExTrait, u256_to_bytes_array}; use utils::serialization::{serialize_bytes, serialize_transaction_signature}; use utils::test_data::{legacy_rlp_encoded_tx, eip_2930_encoded_tx, eip_1559_encoded_tx}; @@ -77,8 +76,8 @@ fn test___execute__a() { // check counter value is 0 before doing inc let tx = call_transaction(chain_id(), Option::Some(other_evm_address()), data_get_tx); - let (_, return_data) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx),); + let (_, return_data, _) = kakarot_core + .eth_call(origin: evm_address, tx: Transaction::Legacy(tx),); assert_eq!(return_data, u256_to_bytes_array(0).span()); @@ -129,8 +128,8 @@ fn test___execute__a() { } // check counter value has increased let tx = call_transaction(chain_id(), Option::Some(other_evm_address()), data_get_tx); - let (_, return_data) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx),); + let (_, return_data, _) = kakarot_core + .eth_call(origin: evm_address, tx: Transaction::Legacy(tx),); assert_eq!(return_data, u256_to_bytes_array(1).span()); } @@ -207,7 +206,7 @@ fn test___validate__fail__to_address_not_kakarot_core() { }; start_cheat_signature( eoa_contract.contract_address, - serialize_transaction_signature(signature, TransactionType::Legacy, 1).span() + serialize_transaction_signature(signature, TxType::Legacy, 1).span() ); let call = Call { @@ -246,7 +245,7 @@ fn test___validate__fail__selector_not_eth_send_transaction() { }; start_cheat_signature( eoa_contract.contract_address, - serialize_transaction_signature(signature, TransactionType::Legacy, chain_id).span() + serialize_transaction_signature(signature, TxType::Legacy, chain_id).span() ); let call = Call { @@ -282,7 +281,7 @@ fn test___validate__legacy_transaction() { }; start_cheat_signature( eoa_contract.contract_address, - serialize_transaction_signature(signature, TransactionType::Legacy, chain_id).span() + serialize_transaction_signature(signature, TxType::Legacy, chain_id).span() ); let call = Call { @@ -322,7 +321,7 @@ fn test___validate__eip_2930_transaction() { start_cheat_signature( eoa_contract.contract_address, - serialize_transaction_signature(signature, TransactionType::EIP2930, chain_id).span() + serialize_transaction_signature(signature, TxType::Eip2930, chain_id).span() ); let call = Call { @@ -368,7 +367,7 @@ fn test___validate__eip_1559_transaction() { start_cheat_signature( eoa_contract.contract_address, - serialize_transaction_signature(signature, TransactionType::EIP1559, chain_id).span() + serialize_transaction_signature(signature, TxType::Eip1559, chain_id).span() ); cheat_caller_address( eoa_contract.contract_address, contract_address_const::<0>(), CheatSpan::TargetCalls(1) diff --git a/crates/contracts/tests/test_execution_from_outside.cairo b/crates/contracts/tests/test_execution_from_outside.cairo index df2683be1..98a126fa3 100644 --- a/crates/contracts/tests/test_execution_from_outside.cairo +++ b/crates/contracts/tests/test_execution_from_outside.cairo @@ -11,7 +11,7 @@ use snforge_std::{ stop_cheat_block_timestamp, start_cheat_chain_id_global, stop_cheat_chain_id_global, start_mock_call, stop_mock_call }; -use utils::eth_transaction::TransactionType; +use utils::eth_transaction::tx_type::TxType; use utils::serialization::{serialize_bytes, serialize_transaction_signature}; use utils::test_data::eip_2930_encoded_tx; @@ -249,21 +249,34 @@ fn test_execute_from_outside_wrong_selector() { } #[test] -#[should_panic(expected: 'invalid signature')] +#[should_panic(expected: 'EOA: invalid signature')] fn test_execute_from_outside_invalid_signature() { let (kakarot_core, contract_account) = set_up(); + let caller = contract_address_const::(); + let outside_execution = OutsideExecutionBuilderTrait::new(kakarot_core.contract_address) + .with_caller(caller) .build(); let signature: Span = [1, 2, 3, 4, (chain_id() * 2 + 40).into()].span(); + start_cheat_caller_address(contract_account.contract_address, caller); + + start_mock_call::< + (bool, Span, u128) + >( + kakarot_core.contract_address, + selector!("eth_send_transaction"), + (true, [1, 2, 3].span(), 0) + ); + let _ = contract_account.execute_from_outside(outside_execution, signature); tear_down(contract_account); } #[test] -#[should_panic(expected: 'failed to validate eth tx')] +#[should_panic(expected: 'EOA: could not decode tx')] fn test_execute_from_outside_invalid_tx() { let (kakarot_core, contract_account) = set_up(); @@ -281,7 +294,7 @@ fn test_execute_from_outside_invalid_tx() { .build(); let signature = serialize_transaction_signature( - VALID_EIP2930_SIGNATURE, TransactionType::EIP2930, chain_id() + VALID_EIP2930_SIGNATURE, TxType::Eip2930, chain_id() ) .span(); @@ -291,7 +304,7 @@ fn test_execute_from_outside_invalid_tx() { } #[test] -fn test_execute_from_outside() { +fn test_execute_from_outside_a() { let (kakarot_core, contract_account) = set_up(); let caller = contract_address_const::(); @@ -300,7 +313,7 @@ fn test_execute_from_outside() { .with_caller(caller) .build(); let signature = serialize_transaction_signature( - VALID_EIP2930_SIGNATURE, TransactionType::EIP2930, chain_id() + VALID_EIP2930_SIGNATURE, TxType::Eip2930, chain_id() ) .span(); diff --git a/crates/contracts/tests/test_kakarot_core.cairo b/crates/contracts/tests/test_kakarot_core.cairo index f02ab1f3e..9638316a3 100644 --- a/crates/contracts/tests/test_kakarot_core.cairo +++ b/crates/contracts/tests/test_kakarot_core.cairo @@ -24,16 +24,18 @@ use evm::test_utils::{sequencer_evm_address, chain_id}; use evm::test_utils; use snforge_std::{ declare, DeclareResultTrait, start_cheat_caller_address, stop_cheat_caller_address, - start_cheat_signature, stop_cheat_signature, start_cheat_chain_id, stop_cheat_chain_id, - start_cheat_transaction_hash, stop_cheat_transaction_hash, spy_events, Event, EventSpyTrait, - test_address, cheat_caller_address, CheatSpan, store, load, EventSpyAssertionsTrait, - start_mock_call, stop_mock_call + start_cheat_signature, stop_cheat_signature, start_cheat_chain_id_global, + stop_cheat_chain_id_global, start_cheat_transaction_hash, stop_cheat_transaction_hash, + spy_events, Event, EventSpyTrait, test_address, cheat_caller_address, CheatSpan, store, load, + EventSpyAssertionsTrait, start_mock_call, stop_mock_call }; use snforge_utils::snforge_utils::{ EventsFilterBuilderTrait, ContractEvents, ContractEventsTrait, store_evm }; use starknet::storage::StorageTrait; -use utils::eth_transaction::{EthereumTransaction, EthereumTransactionTrait, LegacyTransaction}; +use utils::eth_transaction::common::{TxKind, TxKindTrait}; +use utils::eth_transaction::legacy::TxLegacy; +use utils::eth_transaction::transaction::{Transaction, TransactionTrait}; use utils::helpers::{EthAddressExTrait, u256_to_bytes_array}; #[test] @@ -195,8 +197,8 @@ fn test_eth_send_transaction_non_deploy_tx() { let tx = contract_utils::call_transaction( chain_id(), Option::Some(counter_address), data_get_tx ); - let (_, return_data) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx)); + let (_, return_data, _) = kakarot_core + .eth_call(origin: evm_address, tx: Transaction::Legacy(tx)); assert_eq!(return_data, u256_to_bytes_array(0).span()); @@ -206,18 +208,17 @@ fn test_eth_send_transaction_non_deploy_tx() { // When start_cheat_caller_address(kakarot_core.contract_address, eoa); - let tx = LegacyTransaction { - chain_id: chain_id(), + let tx = TxLegacy { + chain_id: Option::Some(chain_id()), nonce: 0, - destination: Option::Some(counter_address), - amount: value, + to: counter_address.into(), + value, gas_price, gas_limit, - calldata: data_increment_counter + input: data_increment_counter }; - let (success, _) = kakarot_core - .eth_send_transaction(EthereumTransaction::LegacyTransaction(tx)); + let (success, _, _) = kakarot_core.eth_send_transaction(Transaction::Legacy(tx)); assert!(success); // Then @@ -228,10 +229,8 @@ fn test_eth_send_transaction_non_deploy_tx() { let tx = contract_utils::call_transaction( chain_id(), Option::Some(counter_address), data_get_tx ); - let (_, _) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx)); - let (_, return_data) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx)); + let (_, return_data, _) = kakarot_core + .eth_call(origin: evm_address, tx: Transaction::Legacy(tx)); // Then assert_eq!(return_data, u256_to_bytes_array(1).span()); @@ -260,8 +259,8 @@ fn test_eth_call() { // When let tx = contract_utils::call_transaction(chain_id(), to, calldata); - let (success, return_data) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx)); + let (success, return_data, _) = kakarot_core + .eth_call(origin: evm_address, tx: Transaction::Legacy(tx)); // Then assert_eq!(success, true); @@ -271,7 +270,7 @@ fn test_eth_call() { #[test] fn test_process_transaction() { // Pre - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let chain_id = chain_id(); // Given @@ -295,18 +294,21 @@ fn test_process_transaction() { start_mock_call::(contract_starknet_address, selector!("storage"), 0); let nonce = 0; - let to = Option::Some(contract_evm_address); let gas_limit = test_utils::tx_gas_limit(); let gas_price = test_utils::gas_price(); let value = 0; // selector: function get() - let calldata = [0x6d, 0x4c, 0xe6, 0x3c].span(); + let input = [0x6d, 0x4c, 0xe6, 0x3c].span(); - let tx = EthereumTransaction::LegacyTransaction( - LegacyTransaction { - chain_id, nonce, destination: to, amount: value, gas_price, gas_limit, calldata - } - ); + 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(); @@ -315,7 +317,8 @@ fn test_process_transaction() { >(test_utils::native_token(), selector!("balanceOf"), 0xfffffffffffffffffffffffffff); let result = kakarot_core .process_transaction( - origin: Address { evm: eoa_evm_address, starknet: eoa_starknet_address }, :tx + origin: Address { evm: eoa_evm_address, starknet: eoa_starknet_address }, + tx: Transaction::Legacy(tx) ); let return_data = result.return_data; @@ -342,18 +345,17 @@ fn test_eth_send_transaction_deploy_tx() { // When // Set the contract address to the EOA address, so that the caller of the `eth_send_transaction` // is an eoa - let tx = LegacyTransaction { - chain_id: chain_id(), + let tx = TxLegacy { + chain_id: Option::Some(chain_id()), nonce: 0, - destination: Option::None, - amount: value, + to: Option::None.into(), + value, gas_price, gas_limit, - calldata: deploy_counter_calldata() + input: deploy_counter_calldata() }; start_cheat_caller_address(kakarot_core.contract_address, eoa); - let (_, deploy_result) = kakarot_core - .eth_send_transaction(EthereumTransaction::LegacyTransaction(tx)); + let (_, deploy_result, _) = kakarot_core.eth_send_transaction(Transaction::Legacy(tx)); // Then let expected_address: EthAddress = 0x19587b345dcadfe3120272bd0dbec24741891759 @@ -369,21 +371,19 @@ fn test_eth_send_transaction_deploy_tx() { assert(bytecode == counter_evm_bytecode(), 'wrong bytecode'); // Check that the account was created and `get` returns 0. - let calldata = [0x6d, 0x4c, 0xe6, 0x3c].span(); - let to = Option::Some(expected_address); + let input = [0x6d, 0x4c, 0xe6, 0x3c].span(); // No need to set address back to eoa, as eth_call doesn't use the caller address. - let tx = LegacyTransaction { - chain_id: chain_id(), + let tx = TxLegacy { + chain_id: Option::Some(chain_id()), nonce: 0, - destination: to, - amount: value, + to: expected_address.into(), + value, gas_price, gas_limit, - calldata + input }; - let (_, result) = kakarot_core - .eth_call(origin: evm_address, tx: EthereumTransaction::LegacyTransaction(tx)); + let (_, result, _) = kakarot_core.eth_call(origin: evm_address, tx: Transaction::Legacy(tx)); // Then assert(result == u256_to_bytes_array(0).span(), 'wrong result'); } diff --git a/crates/contracts/tests/test_utils.cairo b/crates/contracts/tests/test_utils.cairo deleted file mode 100644 index f87838080..000000000 --- a/crates/contracts/tests/test_utils.cairo +++ /dev/null @@ -1,149 +0,0 @@ -use contracts::account_contract::{AccountContract}; -use contracts::account_contract::{IAccountDispatcher, IAccountDispatcherTrait}; -use contracts::kakarot_core::{ - interface::IExtendedKakarotCoreDispatcher, interface::IExtendedKakarotCoreDispatcherTrait, - KakarotCore -}; -use contracts::{UninitializedAccount}; -use core::fmt::Debug; -use core::result::ResultTrait; -use core::starknet::ClassHash; -use core::starknet::syscalls::deploy_syscall; -use core::starknet::{ - testing, contract_address_const, EthAddress, ContractAddress, get_contract_address -}; -use evm::backend::starknet_backend; -use evm::model::{Address}; - -use evm::test_utils::{ca_address, other_starknet_address, chain_id, sequencer_evm_address}; -use openzeppelin::token::erc20::ERC20; -use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; -use snforge_std::{ - declare, DeclareResult, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, - start_cheat_sequencer_address_global, stop_cheat_caller_address, CheatSpan, - start_cheat_caller_address_global -}; -use utils::constants::BLOCK_GAS_LIMIT; -use utils::eth_transaction::LegacyTransaction; - - -mod constants { - use core::starknet::{EthAddress, testing, contract_address_const, ContractAddress}; - fn ZERO() -> ContractAddress { - contract_address_const::<0>() - } - - fn OWNER() -> ContractAddress { - contract_address_const::<0xabde1>() - } - - fn OTHER() -> ContractAddress { - contract_address_const::<0xe1145>() - } - - pub(crate) fn EVM_ADDRESS() -> EthAddress { - 0xc0ffee.try_into().unwrap() - } - - pub(crate) fn ETH_BANK() -> ContractAddress { - contract_address_const::<0x777>() - } -} - -fn deploy_native_token() -> IERC20CamelDispatcher { - let calldata: Array = array![ - 'STARKNET_ETH', 'ETH', 0x00, 0xfffffffffffffffffffffffffff, constants::ETH_BANK().into() - ]; - let class = declare("ERC20").unwrap().contract_class().class_hash; - let maybe_address = deploy_syscall(*class, 0, calldata.span(), false); - match maybe_address { - Result::Ok((contract_address, _)) => { IERC20CamelDispatcher { contract_address } }, - Result::Err(err) => panic(err) - } -} - -fn deploy_kakarot_core( - native_token: ContractAddress, mut eoas: Span -) -> IExtendedKakarotCoreDispatcher { - let account_contract_class_hash = declare("AccountContract") - .unwrap() - .contract_class() - .class_hash; - let uninitialized_account_class_hash = declare("UninitializedAccount") - .unwrap() - .contract_class() - .class_hash; - let kakarot_core_class_hash = declare("KakarotCore").unwrap().contract_class().class_hash; - let mut calldata: Array = array![ - other_starknet_address().into(), - native_token.into(), - (*account_contract_class_hash).into(), - (*uninitialized_account_class_hash).into(), - 'coinbase', - BLOCK_GAS_LIMIT.into(), - ]; - - Serde::serialize(@eoas, ref calldata); - - let maybe_address = deploy_syscall( - (*kakarot_core_class_hash).into(), 0, calldata.span(), false - ); - - match maybe_address { - Result::Ok(( - contract_address, _ - )) => { IExtendedKakarotCoreDispatcher { contract_address } }, - Result::Err(err) => panic(err) - } -} - -pub(crate) fn deploy_contract_account( - kakarot_core: IExtendedKakarotCoreDispatcher, evm_address: EthAddress, bytecode: Span -) -> Address { - let eoa = deploy_eoa(kakarot_core, evm_address); - let starknet_address = eoa.contract_address; - start_cheat_caller_address(starknet_address, kakarot_core.contract_address); - IAccountDispatcher { contract_address: starknet_address }.set_nonce(1); - IAccountDispatcher { contract_address: starknet_address }.write_bytecode(bytecode); - stop_cheat_caller_address(starknet_address); - Address { evm: evm_address, starknet: starknet_address } -} - -fn deploy_eoa( - kakarot_core: IExtendedKakarotCoreDispatcher, evm_address: EthAddress -) -> IAccountDispatcher { - let starknet_address = kakarot_core.deploy_externally_owned_account(evm_address); - IAccountDispatcher { contract_address: starknet_address } -} - -fn call_transaction( - chain_id: u128, destination: Option, calldata: Span -) -> LegacyTransaction { - LegacyTransaction { - chain_id, nonce: 0, gas_price: 0, gas_limit: 500000000, destination, amount: 0, calldata - } -} - -fn fund_account_with_native_token( - contract_address: ContractAddress, native_token: IERC20CamelDispatcher, amount: u256, -) { - start_cheat_caller_address(native_token.contract_address, constants::ETH_BANK()); - native_token.transfer(contract_address, amount); - stop_cheat_caller_address(native_token.contract_address); -} - -pub(crate) fn setup_contracts_for_testing() -> ( - IERC20CamelDispatcher, IExtendedKakarotCoreDispatcher -) { - let native_token = deploy_native_token(); - let kakarot_core = deploy_kakarot_core( - native_token.contract_address, [sequencer_evm_address()].span() - ); - - let sequencer: EthAddress = sequencer_evm_address(); - - let sequencer_sn_address = kakarot_core.address_registry(sequencer); - start_cheat_sequencer_address_global(sequencer_sn_address); - start_cheat_caller_address_global(kakarot_core.contract_address); - return (native_token, kakarot_core); -} diff --git a/crates/evm/src/backend/starknet_backend.cairo b/crates/evm/src/backend/starknet_backend.cairo index 724407ddf..128a92a7b 100644 --- a/crates/evm/src/backend/starknet_backend.cairo +++ b/crates/evm/src/backend/starknet_backend.cairo @@ -251,7 +251,7 @@ mod tests { use evm::state::{State, StateTrait}; use evm::test_utils::evm_address; use evm::test_utils::{ - setup_test_storages, uninitialized_account, account_contract, register_account + setup_test_environment, uninitialized_account, account_contract, register_account }; use snforge_std::{test_address, start_mock_call, get_class_hash}; use snforge_utils::snforge_utils::{assert_not_called, assert_called}; @@ -264,7 +264,7 @@ mod tests { fn test_deploy() { // store the classes in the context of the local execution, to be used for deploying the // account class - setup_test_storages(); + setup_test_environment(); let test_address = test_address(); start_mock_call::< @@ -283,7 +283,7 @@ mod tests { //TODO(starknet-foundry): it's impossible to deploy an un-declared class, nor is it possible to //mock_deploy. fn test_account_commit_undeployed_create_should_change_set_all() { - setup_test_storages(); + setup_test_environment(); let test_address = test_address(); let evm_address = evm_address(); let starknet_address = compute_starknet_address( @@ -315,7 +315,7 @@ mod tests { #[test] fn test_account_commit_deployed_and_created_should_write_code() { - setup_test_storages(); + setup_test_environment(); let test_address = test_address(); let evm_address = evm_address(); let starknet_address = compute_starknet_address( @@ -347,7 +347,7 @@ mod tests { //TODO(starknet-foundry): it's impossible to deploy an un-declared class, nor is it possible to //mock_deploy. fn test_exec_sstore_finalized() { // // Given - // setup_test_storages(); + // setup_test_environment(); // let mut vm = VMBuilderTrait::new_with_presets().build(); // let evm_address = vm.message().target.evm; // let starknet_address = compute_starknet_address( diff --git a/crates/evm/src/call_helpers.cairo b/crates/evm/src/call_helpers.cairo index e9f70e75b..ae1b47a32 100644 --- a/crates/evm/src/call_helpers.cairo +++ b/crates/evm/src/call_helpers.cairo @@ -48,7 +48,7 @@ pub impl CallHelpersImpl of CallHelpers { /// Then, the EVM execution loop will start on this new execution context. fn generic_call( ref self: VM, - gas: u128, + gas: u64, value: u256, caller: EthAddress, to: EthAddress, diff --git a/crates/evm/src/gas.cairo b/crates/evm/src/gas.cairo index 7d936e532..8819d7d65 100644 --- a/crates/evm/src/gas.cairo +++ b/crates/evm/src/gas.cairo @@ -1,58 +1,60 @@ use core::cmp::min; -use utils::eth_transaction::{AccessListItem, EthereumTransaction, EthereumTransactionTrait}; +use utils::eth_transaction::common::TxKindTrait; +use utils::eth_transaction::eip2930::{AccessListItem}; +use utils::eth_transaction::transaction::{Transaction, TransactionTrait}; use utils::helpers; //! Gas costs for EVM operations //! Code is based on alloy project //! Source: -pub const ZERO: u128 = 0; -pub const BASE: u128 = 2; -pub const VERYLOW: u128 = 3; -pub const LOW: u128 = 5; -pub const MID: u128 = 8; -pub const HIGH: u128 = 10; -pub const JUMPDEST: u128 = 1; -pub const SELFDESTRUCT: u128 = 5000; -pub const CREATE: u128 = 32000; -pub const CALLVALUE: u128 = 9000; -pub const NEWACCOUNT: u128 = 25000; -pub const EXP: u128 = 10; -pub const EXP_GAS_PER_BYTE: u128 = 50; -pub const MEMORY: u128 = 3; -pub const LOG: u128 = 375; -pub const LOGDATA: u128 = 8; -pub const LOGTOPIC: u128 = 375; -pub const KECCAK256: u128 = 30; -pub const KECCAK256WORD: u128 = 6; -pub const COPY: u128 = 3; -pub const BLOCKHASH: u128 = 20; -pub const CODEDEPOSIT: u128 = 200; - -pub const SSTORE_SET: u128 = 20000; -pub const SSTORE_RESET: u128 = 5000; -pub const REFUND_SSTORE_CLEARS: u128 = 4800; - -pub const TRANSACTION_ZERO_DATA: u128 = 4; -pub const TRANSACTION_NON_ZERO_DATA_INIT: u128 = 16; -pub const TRANSACTION_NON_ZERO_DATA_FRONTIER: u128 = 68; -pub const TRANSACTION_BASE_COST: u128 = 21000; -pub const TRANSACTION_CREATE_COST: u128 = 32000; +pub const ZERO: u64 = 0; +pub const BASE: u64 = 2; +pub const VERYLOW: u64 = 3; +pub const LOW: u64 = 5; +pub const MID: u64 = 8; +pub const HIGH: u64 = 10; +pub const JUMPDEST: u64 = 1; +pub const SELFDESTRUCT: u64 = 5000; +pub const CREATE: u64 = 32000; +pub const CALLVALUE: u64 = 9000; +pub const NEWACCOUNT: u64 = 25000; +pub const EXP: u64 = 10; +pub const EXP_GAS_PER_BYTE: u64 = 50; +pub const MEMORY: u64 = 3; +pub const LOG: u64 = 375; +pub const LOGDATA: u64 = 8; +pub const LOGTOPIC: u64 = 375; +pub const KECCAK256: u64 = 30; +pub const KECCAK256WORD: u64 = 6; +pub const COPY: u64 = 3; +pub const BLOCKHASH: u64 = 20; +pub const CODEDEPOSIT: u64 = 200; + +pub const SSTORE_SET: u64 = 20000; +pub const SSTORE_RESET: u64 = 5000; +pub const REFUND_SSTORE_CLEARS: u64 = 4800; + +pub const TRANSACTION_ZERO_DATA: u64 = 4; +pub const TRANSACTION_NON_ZERO_DATA_INIT: u64 = 16; +pub const TRANSACTION_NON_ZERO_DATA_FRONTIER: u64 = 68; +pub const TRANSACTION_BASE_COST: u64 = 21000; +pub const TRANSACTION_CREATE_COST: u64 = 32000; // Berlin EIP-2929 constants -pub const ACCESS_LIST_ADDRESS: u128 = 2400; -pub const ACCESS_LIST_STORAGE_KEY: u128 = 1900; -pub const COLD_SLOAD_COST: u128 = 2100; -pub const COLD_ACCOUNT_ACCESS_COST: u128 = 2600; -pub const WARM_ACCESS_COST: u128 = 100; +pub const ACCESS_LIST_ADDRESS: u64 = 2400; +pub const ACCESS_LIST_STORAGE_KEY: u64 = 1900; +pub const COLD_SLOAD_COST: u64 = 2100; +pub const COLD_ACCOUNT_ACCESS_COST: u64 = 2600; +pub const WARM_ACCESS_COST: u64 = 100; /// EIP-3860 : Limit and meter initcode -pub const INITCODE_WORD_COST: u128 = 2; +pub const INITCODE_WORD_COST: u64 = 2; -pub const CALL_STIPEND: u128 = 2300; +pub const CALL_STIPEND: u64 = 2300; // EIP-4844 -pub const BLOB_HASH_COST: u128 = 3; +pub const BLOB_HASH_COST: u64 = 3; /// Defines the gas cost and stipend for executing call opcodes. /// @@ -62,8 +64,8 @@ pub const BLOB_HASH_COST: u128 = 3; /// * `stipend`: The portion of gas available to sub-calls that is refundable if not consumed. #[derive(Drop)] pub struct MessageCallGas { - pub cost: u128, - pub stipend: u128, + pub cost: u64, + pub stipend: u64, } /// Defines the new size and the expansion cost after memory expansion. @@ -75,7 +77,7 @@ pub struct MessageCallGas { #[derive(Drop)] pub struct MemoryExpansion { pub new_size: u32, - pub expansion_cost: u128, + pub expansion_cost: u64, } /// Calculates the maximum gas that is allowed for making a message call. @@ -85,7 +87,7 @@ pub struct MemoryExpansion { /// /// # Returns /// * The maximum gas allowed for the message call. -pub fn max_message_call_gas(gas: u128) -> u128 { +pub fn max_message_call_gas(gas: u64) -> u64 { gas - (gas / 64) } @@ -106,7 +108,7 @@ pub fn max_message_call_gas(gas: u128) -> u128 { /// /// * `message_call_gas`: `MessageCallGas` pub fn calculate_message_call_gas( - value: u256, gas: u128, gas_left: u128, memory_cost: u128, extra_gas: u128 + value: u256, gas: u64, gas_left: u64, memory_cost: u64, extra_gas: u64 ) -> MessageCallGas { let call_stipend = if value == 0 { 0 @@ -141,8 +143,8 @@ pub fn calculate_message_call_gas( /// # Returns /// /// * `total_gas_cost` - The gas cost for storing data in memory. -pub fn calculate_memory_gas_cost(size_in_bytes: usize) -> u128 { - let _512: NonZero = 512_u128.try_into().unwrap(); +pub fn calculate_memory_gas_cost(size_in_bytes: usize) -> u64 { + let _512: NonZero = 512_u64.try_into().unwrap(); let size_in_words = (size_in_bytes + 31) / 32; let linear_cost = size_in_words.into() * MEMORY; @@ -201,7 +203,7 @@ pub fn memory_expansion(current_size: usize, operations: Span<(usize, usize)>) - /// /// * `init_code_gas` - The gas to be charged for the init code. #[inline(always)] -pub fn init_code_cost(code_size: usize) -> u128 { +pub fn init_code_cost(code_size: usize) -> u64 { let code_size_in_words = helpers::ceil32(code_size) / 32; code_size_in_words.into() * INITCODE_WORD_COST } @@ -219,11 +221,10 @@ pub fn init_code_cost(code_size: usize) -> u128 { /// /// Reference: /// https://github.com/ethereum/execution-specs/blob/master/src/ethereum/shanghai/fork.py#L689 -pub fn calculate_intrinsic_gas_cost(tx: @EthereumTransaction) -> u128 { - let mut data_cost: u128 = 0; +pub fn calculate_intrinsic_gas_cost(tx: @Transaction) -> u64 { + let mut data_cost: u64 = 0; - let target = tx.destination(); - let mut calldata = tx.calldata(); + let mut calldata = tx.input(); let calldata_len: usize = calldata.len(); for data in calldata { @@ -235,14 +236,14 @@ pub fn calculate_intrinsic_gas_cost(tx: @EthereumTransaction) -> u128 { }; }; - let create_cost = if target.is_none() { + let create_cost: u64 = if tx.kind().is_create() { TRANSACTION_CREATE_COST + init_code_cost(calldata_len) } else { 0 }; - let access_list_cost = if let Option::Some(mut access_list) = tx.try_access_list() { - let mut access_list_cost: u128 = 0; + let access_list_cost = if let Option::Some(mut access_list) = tx.access_list() { + let mut access_list_cost: u64 = 0; for access_list_item in access_list { let AccessListItem { ethereum_address: _, storage_keys } = *access_list_item; access_list_cost += ACCESS_LIST_ADDRESS @@ -265,9 +266,9 @@ mod tests { ACCESS_LIST_STORAGE_KEY }; use evm::test_utils::evm_address; - use utils::eth_transaction::{ - EthereumTransaction, LegacyTransaction, AccessListTransaction, AccessListItem - }; + use utils::eth_transaction::eip2930::{AccessListItem, TxEip2930}; + use utils::eth_transaction::legacy::TxLegacy; + use utils::eth_transaction::transaction::Transaction; use utils::helpers::ToBytes; #[test] @@ -281,23 +282,23 @@ mod tests { // = 21136 let rlp_encoded: u256 = 0xc981f781808184000012; - let calldata = rlp_encoded.to_be_bytes(); - let destination: Option = 'vitalik.eth'.try_into(); + let input = rlp_encoded.to_be_bytes(); + let to: EthAddress = 'vitalik.eth'.try_into().unwrap(); - let tx: EthereumTransaction = EthereumTransaction::LegacyTransaction( - LegacyTransaction { + let tx: Transaction = Transaction::Legacy( + TxLegacy { + to: to.into(), nonce: 0, gas_price: 50, gas_limit: 433926, - destination, - amount: 1, - calldata, - chain_id: 0x1 + value: 1, + input, + chain_id: Option::Some(0x1) } ); - let expected_cost: u128 = 21136; - let out_cost: u128 = calculate_intrinsic_gas_cost(@tx); + let expected_cost: u64 = 21136; + let out_cost: u64 = calculate_intrinsic_gas_cost(@tx); assert_eq!(out_cost, expected_cost, "wrong cost"); } @@ -313,28 +314,28 @@ mod tests { // = 21136 let rlp_encoded: u256 = 0xc981f781808184000012; - let calldata = rlp_encoded.to_be_bytes(); - let destination: Option = 'vitalik.eth'.try_into(); + let input = rlp_encoded.to_be_bytes(); + let to: EthAddress = 'vitalik.eth'.try_into().unwrap(); let access_list = [ AccessListItem { ethereum_address: evm_address(), storage_keys: [1, 2, 3, 4, 5].span() } ].span(); - let tx: EthereumTransaction = EthereumTransaction::AccessListTransaction( - AccessListTransaction { + let tx: Transaction = Transaction::Eip2930( + TxEip2930 { + to: to.into(), nonce: 0, gas_price: 50, gas_limit: 433926, - destination, - amount: 1, - calldata, + value: 1, + input, chain_id: 0x1, access_list } ); - let expected_cost: u128 = 21136 + ACCESS_LIST_ADDRESS + 5 * ACCESS_LIST_STORAGE_KEY; - let out_cost: u128 = calculate_intrinsic_gas_cost(@tx); + let expected_cost: u64 = 21136 + ACCESS_LIST_ADDRESS + 5 * ACCESS_LIST_STORAGE_KEY; + let out_cost: u64 = calculate_intrinsic_gas_cost(@tx); assert_eq!(out_cost, expected_cost, "wrong cost"); } @@ -351,22 +352,22 @@ mod tests { // = 53138 let rlp_encoded: u256 = 0xc981f781808184000012; - let calldata = rlp_encoded.to_be_bytes(); + let input = rlp_encoded.to_be_bytes(); - let tx: EthereumTransaction = EthereumTransaction::LegacyTransaction( - LegacyTransaction { + let tx: Transaction = Transaction::Legacy( + TxLegacy { + to: Option::None.into(), nonce: 0, gas_price: 50, gas_limit: 433926, - destination: Option::None(()), - amount: 1, - calldata, - chain_id: 0x1 + value: 1, + input, + chain_id: Option::Some(0x1) } ); - let expected_cost: u128 = 53138; - let out_cost: u128 = calculate_intrinsic_gas_cost(@tx); + let expected_cost: u64 = 53138; + let out_cost: u64 = calculate_intrinsic_gas_cost(@tx); assert_eq!(out_cost, expected_cost, "wrong cost"); } @@ -374,8 +375,8 @@ mod tests { #[test] fn test_calculate_memory_allocation_cost() { let size_in_bytes: usize = 10018613; - let expected_cost: u128 = 192385220; - let out_cost: u128 = calculate_memory_gas_cost(size_in_bytes); + let expected_cost: u64 = 192385220; + let out_cost: u64 = calculate_memory_gas_cost(size_in_bytes); assert_eq!(out_cost, expected_cost, "wrong cost"); } } diff --git a/crates/evm/src/instructions/block_information.cairo b/crates/evm/src/instructions/block_information.cairo index 3f46957d9..953abd0c1 100644 --- a/crates/evm/src/instructions/block_information.cairo +++ b/crates/evm/src/instructions/block_information.cairo @@ -146,7 +146,7 @@ mod tests { use evm::model::vm::VMTrait; use evm::stack::StackTrait; use evm::state::StateTrait; - use evm::test_utils::{VMBuilderTrait, gas_price, setup_test_storages}; + use evm::test_utils::{VMBuilderTrait, gas_price, setup_test_environment}; use snforge_std::{start_cheat_block_number_global, start_cheat_block_timestamp_global}; use utils::constants; use utils::traits::{EthAddressIntoU256}; @@ -261,7 +261,7 @@ mod tests { #[test] fn test_exec_selfbalance_should_push_balance() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); let account = Account { address: vm.message().target, diff --git a/crates/evm/src/instructions/environmental_information.cairo b/crates/evm/src/instructions/environmental_information.cairo index 6f086f5c2..4bad52f6f 100644 --- a/crates/evm/src/instructions/environmental_information.cairo +++ b/crates/evm/src/instructions/environmental_information.cairo @@ -116,7 +116,7 @@ pub impl EnvironmentInformationImpl of EnvironmentInformationTrait { let offset = self.stack.pop_usize()?; let size = self.stack.pop_usize()?; - let words_size: u128 = (ceil32(size) / 32).into(); + let words_size = (ceil32(size) / 32).into(); let copy_gas_cost = gas::COPY * words_size; let memory_expansion = gas::memory_expansion( self.memory.size(), [(dest_offset, size)].span() @@ -146,7 +146,7 @@ pub impl EnvironmentInformationImpl of EnvironmentInformationTrait { let offset = self.stack.pop_usize()?; let size = self.stack.pop_usize()?; - let words_size: u128 = (ceil32(size) / 32).into(); + let words_size = (ceil32(size) / 32).into(); let copy_gas_cost = gas::COPY * words_size; let memory_expansion = gas::memory_expansion( self.memory.size(), [(dest_offset, size)].span() @@ -196,7 +196,7 @@ pub impl EnvironmentInformationImpl of EnvironmentInformationTrait { let size = self.stack.pop_usize()?; // GAS - let words_size: u128 = (ceil32(size) / 32).into(); + let words_size = (ceil32(size) / 32).into(); let memory_expansion = gas::memory_expansion( self.memory.size(), [(dest_offset, size)].span() ); @@ -240,7 +240,7 @@ pub impl EnvironmentInformationImpl of EnvironmentInformationTrait { ensure(!(last_returndata_index > return_data.len()), EVMError::ReturnDataOutOfBounds)?; //TODO: handle overflow in ceil32 function. - let words_size: u128 = (ceil32(size.into()) / 32).into(); + let words_size = (ceil32(size.into()) / 32).into(); let copy_gas_cost = gas::COPY * words_size; let memory_expansion = gas::memory_expansion( diff --git a/crates/evm/src/instructions/memory_operations.cairo b/crates/evm/src/instructions/memory_operations.cairo index 03aa1c0b3..0be675888 100644 --- a/crates/evm/src/instructions/memory_operations.cairo +++ b/crates/evm/src/instructions/memory_operations.cairo @@ -316,7 +316,7 @@ mod tests { use evm::stack::StackTrait; use evm::state::StateTrait; use evm::test_utils::{ - VMBuilderTrait, MemoryTestUtilsTrait, setup_test_storages, uninitialized_account, + VMBuilderTrait, MemoryTestUtilsTrait, setup_test_environment, uninitialized_account, native_token }; use snforge_std::{test_address, start_mock_call}; @@ -780,7 +780,7 @@ mod tests { #[test] fn test_exec_sload_should_push_value_on_stack() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); let evm_address = vm.message().target.evm; @@ -801,7 +801,7 @@ mod tests { #[test] fn test_exec_sstore_on_account_in_st() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); let test_address = test_address(); let evm_address = vm.message().target.evm; @@ -833,7 +833,7 @@ mod tests { #[test] fn test_exec_sstore_on_account_undeployed() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); let evm_address = vm.message().target.evm; let key: u256 = 0x100000000000000000000000000000001; @@ -854,7 +854,7 @@ mod tests { #[test] fn test_exec_sstore_on_contract_account_alive() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); let test_address = test_address(); let evm_address = vm.message().target.evm; @@ -887,7 +887,7 @@ mod tests { #[test] fn test_exec_sstore_should_fail_static_call() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().with_read_only().build(); let key: u256 = 0x100000000000000000000000000000001; let value: u256 = 0xABDE1E11A5; @@ -907,7 +907,7 @@ mod tests { #[test] fn test_exec_sstore_should_fail_gas_left_inf_call_stipend_eip_1706() { // Given - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().with_gas_left(gas::CALL_STIPEND).build(); let test_address = test_address(); let evm_address = vm.message().target.evm; diff --git a/crates/evm/src/instructions/sha3.cairo b/crates/evm/src/instructions/sha3.cairo index eb730916e..0928f7611 100644 --- a/crates/evm/src/instructions/sha3.cairo +++ b/crates/evm/src/instructions/sha3.cairo @@ -24,7 +24,7 @@ pub impl Sha3Impl of Sha3Trait { let offset: usize = self.stack.pop_usize()?; let mut size: usize = self.stack.pop_usize()?; - let words_size: u128 = (ceil32(size) / 32).into(); + let words_size = (ceil32(size) / 32).into(); let word_gas_cost = gas::KECCAK256WORD * words_size; let memory_expansion = gas::memory_expansion(self.memory.size(), [(offset, size)].span()); self.memory.ensure_length(memory_expansion.new_size); diff --git a/crates/evm/src/instructions/system_operations.cairo b/crates/evm/src/instructions/system_operations.cairo index 42fa5156a..d8338c934 100644 --- a/crates/evm/src/instructions/system_operations.cairo +++ b/crates/evm/src/instructions/system_operations.cairo @@ -25,7 +25,7 @@ pub impl SystemOperations of SystemOperationsTrait { /// CALL /// # Specification: https://www.evm.codes/#f1?fork=shanghai fn exec_call(ref self: VM) -> Result<(), EVMError> { - let gas = self.stack.pop_saturating_u128()?; + let gas = self.stack.pop_saturating_u64()?; let to = self.stack.pop_eth_address()?; let value = self.stack.pop()?; let args_offset = self.stack.pop_usize()?; @@ -104,7 +104,7 @@ pub impl SystemOperations of SystemOperationsTrait { /// CALLCODE /// # Specification: https://www.evm.codes/#f2?fork=shanghai fn exec_callcode(ref self: VM) -> Result<(), EVMError> { - let gas = self.stack.pop_saturating_u128()?; + let gas = self.stack.pop_saturating_u64()?; let code_address = self.stack.pop_eth_address()?; let value = self.stack.pop()?; let args_offset = self.stack.pop_usize()?; @@ -189,7 +189,7 @@ pub impl SystemOperations of SystemOperationsTrait { /// DELEGATECALL /// # Specification: https://www.evm.codes/#f4?fork=shanghai fn exec_delegatecall(ref self: VM) -> Result<(), EVMError> { - let gas = self.stack.pop_saturating_u128()?; + let gas = self.stack.pop_saturating_u64()?; let code_address = self.stack.pop_eth_address()?; let args_offset = self.stack.pop_usize()?; let args_size = self.stack.pop_usize()?; @@ -242,7 +242,7 @@ pub impl SystemOperations of SystemOperationsTrait { /// STATICCALL /// # Specification: https://www.evm.codes/#fa?fork=shanghai fn exec_staticcall(ref self: VM) -> Result<(), EVMError> { - let gas = self.stack.pop_saturating_u128()?; + let gas = self.stack.pop_saturating_u64()?; let to = self.stack.pop_eth_address()?; let args_offset = self.stack.pop_usize()?; let args_size = self.stack.pop_usize()?; @@ -380,7 +380,7 @@ mod tests { use evm::stack::StackTrait; use evm::state::{StateTrait}; use evm::test_utils::{ - VMBuilderTrait, MemoryTestUtilsTrait, native_token, evm_address, setup_test_storages, + VMBuilderTrait, MemoryTestUtilsTrait, native_token, evm_address, setup_test_environment, origin, uninitialized_account }; use snforge_std::{test_address, start_mock_call}; @@ -901,7 +901,7 @@ mod tests { #[test] fn test_exec_create_no_value_transfer() { // Given - setup_test_storages(); + setup_test_environment(); let deployed_bytecode = [0xff].span(); let eth_address: EthAddress = evm_address(); @@ -969,7 +969,7 @@ mod tests { #[test] fn test_exec_create_failure() { // Given - setup_test_storages(); + setup_test_environment(); let deployed_bytecode = [0xFF].span(); let eth_address: EthAddress = evm_address(); @@ -1030,7 +1030,7 @@ mod tests { #[test] fn test_exec_create2() { // Given - setup_test_storages(); + setup_test_environment(); let deployed_bytecode = [0xff].span(); let eth_address: EthAddress = evm_address(); diff --git a/crates/evm/src/model.cairo b/crates/evm/src/model.cairo index fc4aef8d6..bf70863fe 100644 --- a/crates/evm/src/model.cairo +++ b/crates/evm/src/model.cairo @@ -19,20 +19,20 @@ use utils::traits::{EthAddressDefault, ContractAddressDefault, SpanDefault}; pub struct Environment { pub origin: EthAddress, pub gas_price: u128, - pub chain_id: u128, + pub chain_id: u64, pub prevrandao: u256, pub block_number: u64, - pub block_gas_limit: u128, + pub block_gas_limit: u64, pub block_timestamp: u64, pub coinbase: EthAddress, - pub base_fee: u128, + pub base_fee: u64, pub state: State } #[derive(Copy, Drop, Default, PartialEq, Debug)] pub struct Message { pub caller: Address, pub target: Address, - pub gas_limit: u128, + pub gas_limit: u64, pub data: Span, pub code: Span, pub code_address: Address, @@ -48,10 +48,10 @@ pub struct Message { pub struct ExecutionResult { pub status: ExecutionResultStatus, pub return_data: Span, - pub gas_left: u128, + pub gas_left: u64, pub accessed_addresses: SpanSet, pub accessed_storage_keys: SpanSet<(EthAddress, u256)>, - pub gas_refund: u128, + pub gas_refund: u64, } #[derive(Copy, Drop, PartialEq, Debug)] @@ -81,7 +81,7 @@ pub impl ExecutionResultImpl of ExecutionResultTrait { /// Decrements the gas_left field of the current execution context by the value amount. /// # Error : returns `EVMError::OutOfGas` if gas_left - value < 0 #[inline(always)] - fn charge_gas(ref self: ExecutionResult, value: u128) -> Result<(), EVMError> { + fn charge_gas(ref self: ExecutionResult, value: u64) -> Result<(), EVMError> { self.gas_left = self.gas_left.checked_sub(value).ok_or(EVMError::OutOfGas)?; Result::Ok(()) } @@ -103,9 +103,9 @@ pub impl ExecutionResultImpl of ExecutionResultTrait { pub struct ExecutionSummary { pub status: ExecutionResultStatus, pub return_data: Span, - pub gas_left: u128, + pub gas_left: u64, pub state: State, - pub gas_refund: u128 + pub gas_refund: u64 } #[generate_trait] @@ -136,13 +136,13 @@ pub impl ExecutionSummaryImpl of ExecutionSummaryTrait { pub struct TransactionResult { pub success: bool, pub return_data: Span, - pub gas_used: u128, + pub gas_used: u64, pub state: State } #[generate_trait] pub impl TransactionResultImpl of TransactionResultTrait { - fn exceptional_failure(error: Span, gas_used: u128) -> TransactionResult { + fn exceptional_failure(error: Span, gas_used: u64) -> TransactionResult { TransactionResult { success: false, return_data: error, gas_used, state: Default::default() } @@ -214,7 +214,7 @@ mod tests { #[test] fn test_is_deployed_returns_true_if_in_registry() { // Given - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let starknet_address = compute_starknet_address( test_address(), test_utils::evm_address(), test_utils::uninitialized_account() ); @@ -230,7 +230,7 @@ mod tests { #[test] fn test_is_deployed_undeployed() { // Given - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); // When let is_deployed = test_utils::evm_address().is_deployed(); diff --git a/crates/evm/src/model/account.cairo b/crates/evm/src/model/account.cairo index 62226cd68..fdd4bb8d6 100644 --- a/crates/evm/src/model/account.cairo +++ b/crates/evm/src/model/account.cairo @@ -297,7 +297,8 @@ mod tests { mod test_fetch { use evm::model::account::{Account, AccountTrait, Address}; use evm::test_utils::{ - register_account, setup_test_storages, uninitialized_account, evm_address, native_token, + register_account, setup_test_environment, uninitialized_account, evm_address, + native_token, }; use snforge_std::{test_address, start_mock_call}; use snforge_utils::snforge_utils::assert_called; @@ -306,7 +307,7 @@ mod tests { #[test] fn test_should_fetch_data_from_storage_if_registered() { // Given - setup_test_storages(); + setup_test_environment(); let starknet_address = compute_starknet_address( test_address(), evm_address(), uninitialized_account() ); @@ -339,7 +340,7 @@ mod tests { #[test] fn test_should_return_none_if_not_registered() { // Given - setup_test_storages(); + setup_test_environment(); let _starknet_address = compute_starknet_address( test_address(), evm_address(), uninitialized_account() ); @@ -351,7 +352,8 @@ mod tests { mod test_fetch_or_create { use evm::model::account::{Account, AccountTrait, Address}; use evm::test_utils::{ - register_account, setup_test_storages, uninitialized_account, evm_address, native_token, + register_account, setup_test_environment, uninitialized_account, evm_address, + native_token, }; use snforge_std::{test_address, start_mock_call}; use snforge_utils::snforge_utils::assert_called; @@ -360,7 +362,7 @@ mod tests { #[test] fn test_should_fetch_data_from_storage_if_registered() { // Given - setup_test_storages(); + setup_test_environment(); let starknet_address = compute_starknet_address( test_address(), evm_address(), uninitialized_account() ); @@ -393,7 +395,7 @@ mod tests { #[test] fn test_should_create_new_account_with_starknet_balance_if_not_registered() { // Given - setup_test_storages(); + setup_test_environment(); let starknet_address = compute_starknet_address( test_address(), evm_address(), uninitialized_account() ); diff --git a/crates/evm/src/model/vm.cairo b/crates/evm/src/model/vm.cairo index 993218eb4..3620fb491 100644 --- a/crates/evm/src/model/vm.cairo +++ b/crates/evm/src/model/vm.cairo @@ -17,12 +17,12 @@ pub struct VM { pub return_data: Span, pub env: Environment, pub message: Message, - pub gas_left: u128, + pub gas_left: u64, pub running: bool, pub error: bool, pub accessed_addresses: Set, pub accessed_storage_keys: Set<(EthAddress, u256)>, - pub gas_refund: u128 + pub gas_refund: u64 } @@ -50,7 +50,7 @@ pub impl VMImpl of VMTrait { /// Decrements the gas_left field of the current vm by the value amount. /// # Error : returns `EVMError::OutOfGas` if gas_left - value < 0 #[inline(always)] - fn charge_gas(ref self: VM, value: u128) -> Result<(), EVMError> { + fn charge_gas(ref self: VM, value: u64) -> Result<(), EVMError> { self.gas_left = match self.gas_left.checked_sub(value) { Option::Some(gas_left) => gas_left, Option::None => { return Result::Err(EVMError::OutOfGas); }, @@ -110,12 +110,12 @@ pub impl VMImpl of VMTrait { } #[inline(always)] - fn gas_left(self: @VM) -> u128 { + fn gas_left(self: @VM) -> u64 { *self.gas_left } #[inline(always)] - fn gas_refund(self: @VM) -> u128 { + fn gas_refund(self: @VM) -> u64 { *self.gas_refund } diff --git a/crates/evm/src/precompiles.cairo b/crates/evm/src/precompiles.cairo index 0bc7e9eee..686a44816 100644 --- a/crates/evm/src/precompiles.cairo +++ b/crates/evm/src/precompiles.cairo @@ -50,7 +50,7 @@ pub fn eth_precompile_addresses() -> Set { pub trait Precompile { fn address() -> EthAddress; - fn exec(input: Span) -> Result<(u128, Span), EVMError>; + fn exec(input: Span) -> Result<(u64, Span), EVMError>; } #[generate_trait] diff --git a/crates/evm/src/precompiles/blake2f.cairo b/crates/evm/src/precompiles/blake2f.cairo index 0f49a26fb..bc91001a1 100644 --- a/crates/evm/src/precompiles/blake2f.cairo +++ b/crates/evm/src/precompiles/blake2f.cairo @@ -16,7 +16,7 @@ pub impl Blake2f of Precompile { 0x9.try_into().unwrap() } - fn exec(input: Span) -> Result<(u128, Span), EVMError> { + fn exec(input: Span) -> Result<(u64, Span), EVMError> { ensure( input.len() == INPUT_LENGTH, EVMError::InvalidParameter('Blake2: wrong input length') )?; @@ -34,7 +34,7 @@ pub impl Blake2f of Precompile { .from_be_bytes() .ok_or(EVMError::TypeConversionError('extraction of u32 failed'))?; - let gas: u128 = (GF_ROUND * rounds.into()).into(); + let gas = (GF_ROUND * rounds.into()).into(); let mut h: Array = Default::default(); let mut m: Array = Default::default(); @@ -95,7 +95,7 @@ mod tests { blake2_precompile_fail_wrong_length_input_3_test_case, blake2_precompile_pass_1_test_case, blake2_precompile_pass_0_test_case, blake2_precompile_pass_2_test_case }; - use evm::test_utils::{VMBuilderTrait, native_token, setup_test_storages}; + use evm::test_utils::{VMBuilderTrait, native_token, setup_test_environment}; use snforge_std::start_mock_call; use utils::helpers::FromBytes; @@ -167,7 +167,7 @@ mod tests { // #[test] fn test_blake2_precompile_static_call() { - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); diff --git a/crates/evm/src/precompiles/ec_add.cairo b/crates/evm/src/precompiles/ec_add.cairo index 1e4f54b8a..58c49037b 100644 --- a/crates/evm/src/precompiles/ec_add.cairo +++ b/crates/evm/src/precompiles/ec_add.cairo @@ -15,7 +15,7 @@ use utils::helpers::ToBytes; use utils::helpers::load_word; -const BASE_COST: u128 = 150; +const BASE_COST: u64 = 150; const U256_BYTES_LEN: usize = 32; pub impl EcAdd of Precompile { @@ -24,7 +24,7 @@ pub impl EcAdd of Precompile { 0x6.try_into().unwrap() } - fn exec(mut input: Span) -> Result<(u128, Span), EVMError> { + fn exec(mut input: Span) -> Result<(u64, Span), EVMError> { let gas = BASE_COST; let x1_bytes = *(input.multi_pop_front::<32>().unwrap()); diff --git a/crates/evm/src/precompiles/ec_mul.cairo b/crates/evm/src/precompiles/ec_mul.cairo index 37807f203..3adbddbd2 100644 --- a/crates/evm/src/precompiles/ec_mul.cairo +++ b/crates/evm/src/precompiles/ec_mul.cairo @@ -9,7 +9,7 @@ use utils::helpers::{load_word, ToBytes}; // const BN254_ORDER: u256 = 0x30644E72E131A029B85045B68181585D2833E84879B9709143E1F593F0000001; -const BASE_COST: u128 = 6000; +const BASE_COST: u64 = 6000; const U256_BYTES_LEN: usize = 32; pub impl EcMul of Precompile { @@ -17,7 +17,7 @@ pub impl EcMul of Precompile { 0x7.try_into().unwrap() } - fn exec(mut input: Span) -> Result<(u128, Span), EVMError> { + fn exec(mut input: Span) -> Result<(u64, Span), EVMError> { let gas = BASE_COST; let x1_bytes = *(input.multi_pop_front::<32>().unwrap()); diff --git a/crates/evm/src/precompiles/ec_recover.cairo b/crates/evm/src/precompiles/ec_recover.cairo index 75e5ebeee..0262cd3d5 100644 --- a/crates/evm/src/precompiles/ec_recover.cairo +++ b/crates/evm/src/precompiles/ec_recover.cairo @@ -9,7 +9,7 @@ use utils::helpers::{ToBytes, FromBytes}; use utils::traits::BoolIntoNumeric; use utils::traits::EthAddressIntoU256; -const EC_RECOVER_PRECOMPILE_GAS_COST: u128 = 3000; +const EC_RECOVER_PRECOMPILE_GAS_COST: u64 = 3000; pub impl EcRecover of Precompile { #[inline(always)] @@ -17,8 +17,8 @@ pub impl EcRecover of Precompile { 0x1.try_into().unwrap() } - fn exec(input: Span) -> Result<(u128, Span), EVMError> { - let gas: u128 = EC_RECOVER_PRECOMPILE_GAS_COST; + fn exec(input: Span) -> Result<(u64, Span), EVMError> { + let gas = EC_RECOVER_PRECOMPILE_GAS_COST; let message_hash = input.slice(0, 32); let message_hash = match message_hash.from_be_bytes() { @@ -74,7 +74,7 @@ mod tests { use evm::precompiles::ec_recover::EcRecover; use evm::stack::StackTrait; - use evm::test_utils::setup_test_storages; + use evm::test_utils::setup_test_environment; use evm::test_utils::{VMBuilderTrait, MemoryTestUtilsTrait, native_token}; use snforge_std::start_mock_call; use utils::helpers::{ToBytes, FromBytes}; @@ -109,7 +109,7 @@ mod tests { // #[test] fn test_ec_precompile_static_call() { - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); vm diff --git a/crates/evm/src/precompiles/identity.cairo b/crates/evm/src/precompiles/identity.cairo index 932a7a3ad..80064381d 100644 --- a/crates/evm/src/precompiles/identity.cairo +++ b/crates/evm/src/precompiles/identity.cairo @@ -2,8 +2,8 @@ use core::starknet::EthAddress; use evm::errors::EVMError; use evm::precompiles::Precompile; -const BASE_COST: u128 = 15; -const COST_PER_WORD: u128 = 3; +const BASE_COST: u64 = 15; +const COST_PER_WORD: u64 = 3; pub impl Identity of Precompile { #[inline(always)] @@ -11,7 +11,7 @@ pub impl Identity of Precompile { 0x4.try_into().unwrap() } - fn exec(input: Span) -> Result<(u128, Span), EVMError> { + fn exec(input: Span) -> Result<(u64, Span), EVMError> { let data_word_size = ((input.len() + 31) / 32).into(); let gas = BASE_COST + data_word_size * COST_PER_WORD; @@ -27,7 +27,9 @@ mod tests { use evm::memory::MemoryTrait; use evm::precompiles::identity::Identity; use evm::stack::StackTrait; - use evm::test_utils::{VMBuilderTrait, MemoryTestUtilsTrait, native_token, setup_test_storages}; + use evm::test_utils::{ + VMBuilderTrait, MemoryTestUtilsTrait, native_token, setup_test_environment + }; use snforge_std::start_mock_call; // source: @@ -48,7 +50,7 @@ mod tests { //TODO(sn-foundry): fix or delete #[test] fn test_identity_precompile_static_call() { - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); vm.stack.push(0x20).unwrap(); // retSize diff --git a/crates/evm/src/precompiles/modexp.cairo b/crates/evm/src/precompiles/modexp.cairo index 0997e432a..ae088c220 100644 --- a/crates/evm/src/precompiles/modexp.cairo +++ b/crates/evm/src/precompiles/modexp.cairo @@ -16,7 +16,7 @@ use utils::crypto::modexp::lib::modexp; use utils::helpers::{U8SpanExTrait, FromBytes, BitsUsed}; const HEADER_LENGTH: usize = 96; -const MIN_GAS: u128 = 200; +const MIN_GAS: u64 = 200; pub impl ModExp of Precompile { #[inline(always)] @@ -24,7 +24,7 @@ pub impl ModExp of Precompile { 0x5.try_into().unwrap() } - fn exec(input: Span) -> Result<(u128, Span), EVMError> { + fn exec(input: Span) -> Result<(u64, Span), EVMError> { // The format of input is: // // Where every length is a 32-byte left-padded integer representing the number of bytes diff --git a/crates/evm/src/precompiles/p256verify.cairo b/crates/evm/src/precompiles/p256verify.cairo index 6f0e06e17..303597bb2 100644 --- a/crates/evm/src/precompiles/p256verify.cairo +++ b/crates/evm/src/precompiles/p256verify.cairo @@ -5,7 +5,7 @@ use evm::errors::{EVMError}; use evm::precompiles::Precompile; use utils::helpers::FromBytes; -const P256VERIFY_PRECOMPILE_GAS_COST: u128 = 3450; +const P256VERIFY_PRECOMPILE_GAS_COST: u64 = 3450; const ONE_32_BYTES: [ u8 @@ -50,8 +50,8 @@ pub impl P256Verify of Precompile { 0x100.try_into().unwrap() } - fn exec(input: Span) -> Result<(u128, Span), EVMError> { - let gas: u128 = P256VERIFY_PRECOMPILE_GAS_COST; + fn exec(input: Span) -> Result<(u64, Span), EVMError> { + let gas = P256VERIFY_PRECOMPILE_GAS_COST; if input.len() != 160 { return Result::Ok((gas, [].span())); @@ -111,7 +111,7 @@ mod tests { use evm::precompiles::p256verify::P256Verify; use evm::stack::StackTrait; use evm::test_utils::{VMBuilderTrait}; - use evm::test_utils::{setup_test_storages, native_token}; + use evm::test_utils::{setup_test_environment, native_token}; use snforge_std::start_mock_call; use utils::helpers::{ToBytes, FromBytes}; @@ -150,7 +150,7 @@ mod tests { #[test] //TODO(sn-foundry): fix or delete fn test_p256verify_precompile_static_call() { - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); @@ -214,7 +214,7 @@ mod tests { //TODO(sn-foundry): fix or delete #[test] fn test_p256verify_precompile_input_too_short_static_call() { - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); diff --git a/crates/evm/src/precompiles/sha256.cairo b/crates/evm/src/precompiles/sha256.cairo index 7737f3c71..c8a453786 100644 --- a/crates/evm/src/precompiles/sha256.cairo +++ b/crates/evm/src/precompiles/sha256.cairo @@ -5,8 +5,8 @@ use evm::precompiles::Precompile; use utils::helpers::{FromBytes, ToBytes}; use utils::math::Bitshift; -const BASE_COST: u128 = 60; -const COST_PER_WORD: u128 = 12; +const BASE_COST: u64 = 60; +const COST_PER_WORD: u64 = 12; pub impl Sha256 of Precompile { #[inline(always)] @@ -14,7 +14,7 @@ pub impl Sha256 of Precompile { 0x2.try_into().unwrap() } - fn exec(mut input: Span) -> Result<(u128, Span), EVMError> { + fn exec(mut input: Span) -> Result<(u64, Span), EVMError> { let data_word_size = ((input.len() + 31) / 32).into(); let gas = BASE_COST + data_word_size * COST_PER_WORD; @@ -56,7 +56,9 @@ mod tests { use evm::memory::MemoryTrait; use evm::precompiles::sha256::Sha256; use evm::stack::StackTrait; - use evm::test_utils::{VMBuilderTrait, MemoryTestUtilsTrait, native_token, setup_test_storages}; + use evm::test_utils::{ + VMBuilderTrait, MemoryTestUtilsTrait, native_token, setup_test_environment + }; use snforge_std::{start_mock_call}; use utils::helpers::ToBytes; use utils::helpers::{FromBytes}; @@ -147,7 +149,7 @@ mod tests { // #[test] fn test_sha_256_precompile_static_call() { - setup_test_storages(); + setup_test_environment(); let mut vm = VMBuilderTrait::new_with_presets().build(); diff --git a/crates/evm/src/stack.cairo b/crates/evm/src/stack.cairo index dbcfc8b67..4e3ad80a3 100644 --- a/crates/evm/src/stack.cairo +++ b/crates/evm/src/stack.cairo @@ -37,6 +37,7 @@ pub trait StackTrait { fn pop(ref self: Stack) -> Result; fn pop_usize(ref self: Stack) -> Result; fn pop_u64(ref self: Stack) -> Result; + fn pop_saturating_u64(ref self: Stack) -> Result; fn pop_u128(ref self: Stack) -> Result; fn pop_saturating_u128(ref self: Stack) -> Result; fn pop_i256(ref self: Stack) -> Result; @@ -121,6 +122,19 @@ impl StackImpl of StackTrait { Result::Ok(item) } + /// Calls `Stack::pop` and saturates the result to u64 + #[inline(always)] + fn pop_saturating_u64(ref self: Stack) -> Result { + let item: u256 = self.pop()?; + if item.high != 0 { + return Result::Ok(Bounded::::MAX); + }; + match item.low.try_into() { + Option::None => Result::Ok(Bounded::::MAX), + Option::Some(value) => Result::Ok(value), + } + } + /// Calls `Stack::pop` and convert it to i256 /// /// # Errors @@ -406,6 +420,32 @@ mod tests { ); } + #[test] + fn test_pop_saturating_u64_should_return_max_when_overflow() { + // Given + let mut stack = StackTrait::new(); + stack.push(Bounded::::MAX).unwrap(); + + // When + let result = stack.pop_saturating_u64(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Bounded::::MAX); + } + + #[test] + fn test_pop_saturating_u64_should_return_value_when_no_overflow() { + // Given + let mut stack = StackTrait::new(); + stack.push(1234567890).unwrap(); + + // When + let result = stack.pop_saturating_u64(); + + // Then + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1234567890); + } + #[test] fn test_pop_saturating_u128_should_return_max_when_overflow() { diff --git a/crates/evm/src/state.cairo b/crates/evm/src/state.cairo index 1e2e15321..af0bd5edf 100644 --- a/crates/evm/src/state.cairo +++ b/crates/evm/src/state.cairo @@ -317,7 +317,7 @@ mod tests { #[test] fn test_get_account_when_inexistant() { - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let mut state: State = Default::default(); @@ -344,7 +344,7 @@ mod tests { #[test] fn test_get_account_when_cached() { - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let mut state: State = Default::default(); @@ -367,7 +367,7 @@ mod tests { #[test] fn test_get_account_when_deployed() { - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let mut state: State = Default::default(); @@ -408,7 +408,7 @@ mod tests { #[test] fn test_read_state_from_sn_storage() { - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let starknet_address = compute_starknet_address( test_address(), test_utils::evm_address(), test_utils::uninitialized_account() ); @@ -444,7 +444,7 @@ mod tests { fn test_add_transfer() { //Given let mut state: State = Default::default(); - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let sender_evm_address = test_utils::evm_address(); @@ -501,7 +501,7 @@ mod tests { fn test_add_transfer_with_same_sender_and_recipient() { //Given let mut state: State = Default::default(); - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let sender_evm_address = test_utils::evm_address(); @@ -541,7 +541,7 @@ mod tests { fn test_add_transfer_when_amount_is_zero() { //Given let mut state: State = Default::default(); - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let sender_evm_address = test_utils::evm_address(); @@ -594,7 +594,7 @@ mod tests { #[test] fn test_read_balance_cached() { let mut state: State = Default::default(); - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let evm_address = test_utils::evm_address(); @@ -616,7 +616,7 @@ mod tests { #[test] fn test_read_balance_from_storage() { - test_utils::setup_test_storages(); + test_utils::setup_test_environment(); let deployer = test_address(); let evm_address = test_utils::evm_address(); let starknet_address = compute_starknet_address( diff --git a/crates/evm/src/test_utils.cairo b/crates/evm/src/test_utils.cairo index ec5477bd6..a1a264c73 100644 --- a/crates/evm/src/test_utils.cairo +++ b/crates/evm/src/test_utils.cairo @@ -8,6 +8,7 @@ use evm::memory::{Memory, MemoryTrait}; use evm::model::vm::{VM, VMTrait}; use evm::model::{Message, Environment, Address, AccountTrait}; +use snforge_std::start_cheat_chain_id_global; use snforge_std::test_address; use starknet::storage::StorageTraitMut; use utils::constants; @@ -21,12 +22,13 @@ pub fn account_contract() -> ClassHash { } -pub fn setup_test_storages() { +pub fn setup_test_environment() { let mut kakarot_core = KakarotCore::contract_state_for_testing(); let mut kakarot_storage = kakarot_core.deref_mut().storage_mut(); kakarot_storage.Kakarot_account_contract_class_hash.write(account_contract()); kakarot_storage.Kakarot_uninitialized_account_class_hash.write(uninitialized_account()); kakarot_storage.Kakarot_native_token_address.write(native_token()); + start_cheat_chain_id_global(chain_id().into()); } pub fn register_account(evm_address: EthAddress, starknet_address: ContractAddress) { @@ -57,7 +59,7 @@ struct VMBuilder { #[generate_trait] pub impl VMBuilderImpl of VMBuilderTrait { fn new() -> VMBuilder { - VMBuilder { vm: Default::default() }.with_gas_limit(0x100000000000000000000) + VMBuilder { vm: Default::default() }.with_gas_limit(0x1000000000000000) } fn new_with_presets() -> VMBuilder { @@ -89,7 +91,7 @@ pub impl VMBuilderImpl of VMBuilderTrait { self } - fn with_gas_limit(mut self: VMBuilder, gas_limit: u128) -> VMBuilder { + fn with_gas_limit(mut self: VMBuilder, gas_limit: u64) -> VMBuilder { self.vm.message.gas_limit = gas_limit; self } @@ -119,7 +121,7 @@ pub impl VMBuilderImpl of VMBuilderTrait { return self.vm; } - fn with_gas_left(mut self: VMBuilder, gas_left: u128) -> VMBuilder { + fn with_gas_left(mut self: VMBuilder, gas_left: u64) -> VMBuilder { self.vm.gas_left = gas_left; self } @@ -177,7 +179,7 @@ pub fn native_token() -> ContractAddress { contract_address_const::<'native_token'>() } -pub fn chain_id() -> u128 { +pub fn chain_id() -> u64 { 'KKRT'.try_into().unwrap() } @@ -194,8 +196,8 @@ pub fn eoa_address() -> EthAddress { evm_address } -pub fn tx_gas_limit() -> u128 { - 15000000000 +pub fn tx_gas_limit() -> u64 { + constants::BLOCK_GAS_LIMIT } pub fn gas_price() -> u128 { diff --git a/crates/utils/src/constants.cairo b/crates/utils/src/constants.cairo index 4206aebe4..bb97faa82 100644 --- a/crates/utils/src/constants.cairo +++ b/crates/utils/src/constants.cairo @@ -10,10 +10,9 @@ pub const CONTRACT_ADDRESS_PREFIX: felt252 = 'STARKNET_CONTRACT_ADDRESS'; // BLOCK -//TODO(gas): determine correct block gas limit -pub const BLOCK_GAS_LIMIT: u128 = 30_000_000; +pub const BLOCK_GAS_LIMIT: u64 = 7_000_000; // CHAIN_ID = KKRT (0x4b4b5254) in ASCII -pub const CHAIN_ID: u128 = 1263227476; +pub const CHAIN_ID: u64 = 1263227476; // STACK pub const STACK_MAX_DEPTH: usize = 1024; diff --git a/crates/utils/src/errors.cairo b/crates/utils/src/errors.cairo index 93a1000d9..357817c28 100644 --- a/crates/utils/src/errors.cairo +++ b/crates/utils/src/errors.cairo @@ -2,11 +2,12 @@ pub const RLP_EMPTY_INPUT: felt252 = 'KKT: EmptyInput'; pub const RLP_INPUT_TOO_SHORT: felt252 = 'KKT: InputTooShort'; -#[derive(Drop, Copy, PartialEq)] +#[derive(Drop, Copy, PartialEq, Debug)] pub enum RLPError { EmptyInput, InputTooShort, - InvalidInput + InvalidInput, + Custom: felt252 } @@ -15,7 +16,8 @@ pub impl RLPErrorIntoU256 of Into { match self { RLPError::EmptyInput => 'input is null'.into(), RLPError::InputTooShort => 'input too short'.into(), - RLPError::InvalidInput => 'rlp input not conform'.into() + RLPError::InvalidInput => 'rlp input not conform'.into(), + RLPError::Custom(msg) => msg.into() } } } @@ -31,7 +33,7 @@ pub impl RLPErrorImpl of RLPErrorTrait { } -#[derive(Drop, Copy, PartialEq)] +#[derive(Drop, Copy, PartialEq, Debug)] pub enum RLPHelpersError { NotAString, FailedParsingU128, @@ -52,7 +54,7 @@ pub impl RLPHelpersErrorImpl of RLPHelpersErrorTrait { } -#[derive(Drop, Copy, PartialEq)] +#[derive(Drop, Copy, PartialEq, Debug)] pub enum EthTransactionError { RLPError: RLPError, ExpectedRLPItemToBeList, @@ -67,5 +69,12 @@ pub enum EthTransactionError { TypedTxWrongPayloadLength: usize, IncorrectChainId, IncorrectAccountNonce, + /// If the transaction's fee is less than the base fee of the block + FeeCapTooLow, + /// Thrown to ensure no one is able to specify a transaction with a tip higher than the total + /// fee cap. + TipAboveFeeCap, + /// Thrown to ensure no one is able to specify a transaction with a tip that is too high. + TipVeryHigh, Other: felt252 } diff --git a/crates/utils/src/eth_transaction.cairo b/crates/utils/src/eth_transaction.cairo index 9833dc98b..95172c245 100644 --- a/crates/utils/src/eth_transaction.cairo +++ b/crates/utils/src/eth_transaction.cairo @@ -1,807 +1,58 @@ -use core::array::SpanTrait; +pub mod common; +pub mod eip1559; +pub mod eip2930; +pub mod legacy; +pub mod transaction; +pub mod tx_type; +pub mod validation; +use core::cmp::min; +use core::num::traits::{CheckedAdd, Zero}; use core::option::OptionTrait; -use core::starknet::{EthAddress, secp256_trait::Signature, eth_signature::{verify_eth_signature}}; -use core::traits::TryInto; -use utils::errors::RLPErrorTrait; - -use utils::errors::{EthTransactionError, RLPErrorImpl, RLPHelpersErrorImpl, RLPHelpersErrorTrait}; - -use utils::helpers::{U256Impl, ByteArrayExt, U8SpanExTrait}; - -use utils::rlp::RLPItem; -use utils::rlp::{RLPTrait, RLPHelpersTrait}; - -#[derive(Copy, Clone, Drop, Serde, PartialEq, Debug)] -pub struct AccessListItem { - pub ethereum_address: EthAddress, - pub storage_keys: Span -} - -#[generate_trait] -pub impl AccessListItemImpl of AccessListItemTrait { - fn to_storage_keys(self: @AccessListItem) -> Span<(EthAddress, u256)> { - let AccessListItem { ethereum_address, mut storage_keys } = *self; - - let mut storage_keys_arr = array![]; - while let Option::Some(storage_key) = storage_keys.pop_front() { - storage_keys_arr.append((ethereum_address, *storage_key)); - }; - - storage_keys_arr.span() - } -} +use core::starknet::{EthAddress, secp256_trait::Signature,}; +use utils::errors::{EthTransactionError, RLPErrorImpl, RLPHelpersErrorImpl}; +use utils::helpers::ByteArrayExt; #[derive(Drop)] pub struct TransactionMetadata { pub address: EthAddress, - pub account_nonce: u128, - pub chain_id: u128, + pub account_nonce: u64, + pub chain_id: u64, pub signature: Signature, } -#[derive(Drop, Copy, Clone, Serde, Debug)] -pub struct LegacyTransaction { - pub chain_id: u128, - pub nonce: u128, - pub gas_price: u128, - pub gas_limit: u128, - pub destination: Option, - pub amount: u256, - pub calldata: Span -} - -#[derive(Drop, Copy, Clone, Serde, Debug)] -pub struct AccessListTransaction { - pub chain_id: u128, - pub nonce: u128, - pub gas_price: u128, - pub gas_limit: u128, - pub destination: Option, - pub amount: u256, - pub calldata: Span, - pub access_list: Span -} - -#[derive(Drop, Copy, Clone, Serde, Debug)] -pub struct FeeMarketTransaction { - pub chain_id: u128, - pub nonce: u128, - pub max_priority_fee_per_gas: u128, - pub max_fee_per_gas: u128, - pub gas_limit: u128, - pub destination: Option, - pub amount: u256, - pub calldata: Span, - pub access_list: Span -} - -#[derive(Drop, Serde, Debug)] -pub enum EthereumTransaction { - LegacyTransaction: LegacyTransaction, - AccessListTransaction: AccessListTransaction, - FeeMarketTransaction: FeeMarketTransaction -} - -#[generate_trait] -pub impl EthereumTransactionImpl of EthereumTransactionTrait { - fn chain_id(self: @EthereumTransaction) -> u128 { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.chain_id }, - EthereumTransaction::AccessListTransaction(v) => { *v.chain_id }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.chain_id } - } - } - - fn nonce(self: @EthereumTransaction) -> u128 { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.nonce }, - EthereumTransaction::AccessListTransaction(v) => { *v.nonce }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.nonce } - } - } - - fn value(self: @EthereumTransaction) -> u256 { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.amount }, - EthereumTransaction::AccessListTransaction(v) => { *v.amount }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.amount } - } - } - - fn calldata(self: @EthereumTransaction) -> Span { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.calldata }, - EthereumTransaction::AccessListTransaction(v) => { *v.calldata }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.calldata } - } - } - - fn destination(self: @EthereumTransaction) -> Option { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.destination }, - EthereumTransaction::AccessListTransaction(v) => { *v.destination }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.destination } - } - } - - fn gas_price(self: @EthereumTransaction) -> u128 { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.gas_price }, - EthereumTransaction::AccessListTransaction(v) => { *v.gas_price }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.max_fee_per_gas } - } - } - - fn gas_limit(self: @EthereumTransaction) -> u128 { - match self { - EthereumTransaction::LegacyTransaction(v) => { *v.gas_limit }, - EthereumTransaction::AccessListTransaction(v) => { *v.gas_limit }, - EthereumTransaction::FeeMarketTransaction(v) => { *v.gas_limit } - } - } - - fn try_access_list(self: @EthereumTransaction) -> Option> { - match self { - EthereumTransaction::LegacyTransaction => { Option::None }, - EthereumTransaction::AccessListTransaction(tx) => { Option::Some(*tx.access_list) }, - EthereumTransaction::FeeMarketTransaction(tx) => { Option::Some(*tx.access_list) } - } - } - - fn try_into_legacy_transaction(self: @EthereumTransaction) -> Option { - match self { - EthereumTransaction::LegacyTransaction(v) => { Option::Some(*v) }, - EthereumTransaction::AccessListTransaction(_) => { Option::None }, - EthereumTransaction::FeeMarketTransaction(_) => { Option::None } - } - } - - fn try_into_access_list_transaction( - self: @EthereumTransaction - ) -> Option { - match self { - EthereumTransaction::LegacyTransaction(_) => { Option::None }, - EthereumTransaction::AccessListTransaction(v) => { Option::Some(*v) }, - EthereumTransaction::FeeMarketTransaction(_) => { Option::None } - } - } - - fn try_into_fee_market_transaction(self: @EthereumTransaction) -> Option { - match self { - EthereumTransaction::LegacyTransaction(_) => { Option::None }, - EthereumTransaction::AccessListTransaction(_) => { Option::None }, - EthereumTransaction::FeeMarketTransaction(v) => { Option::Some(*v) } - } - } -} - -#[derive(Drop, PartialEq)] -pub enum EncodedTransaction { - Legacy: Span, - EIP1559: Span, - EIP2930: Span, -} - -fn deserialize_encoded_transaction(self: Span) -> Option { - if self.is_empty() { - return Option::None; - } - if (EncodedTransactionTrait::is_legacy_tx(self)) { - Option::Some(EncodedTransaction::Legacy(self)) - } else { - let tx_type: u32 = (*self.at(0)).into(); - if (tx_type == 1) { - Option::Some(EncodedTransaction::EIP2930(self)) - } else if (tx_type == 2) { - Option::Some(EncodedTransaction::EIP1559(self)) - } else { - Option::None - } - } -} - -#[generate_trait] -pub impl EncodedTransactionImpl of EncodedTransactionTrait { - #[inline(always)] - fn decode(self: EncodedTransaction) -> Result { - match self { - EncodedTransaction::Legacy(encoded_tx_data) => { - Self::decode_legacy_tx(encoded_tx_data) - }, - EncodedTransaction::EIP1559(encoded_tx_data) => { - Self::decode_typed_tx(encoded_tx_data) - }, - EncodedTransaction::EIP2930(encoded_tx_data) => { - Self::decode_typed_tx(encoded_tx_data) - }, - } - } - - /// Decode a legacy Ethereum transaction - /// This function decodes a legacy Ethereum transaction in accordance with EIP-155. - /// It returns transaction details including nonce, gas price, gas limit, destination address, - /// amount, payload, message hash, chain id. The transaction hash is computed by keccak hashing - /// the signed transaction data, which includes the chain ID in accordance with EIP-155. - /// # Arguments - /// * encoded_tx_data - The raw rlp encoded transaction data - /// * encoded_tx_data - is of the format: rlp![nonce, gasPrice, gasLimit, to , value, data, - /// chainId, 0, 0] - /// Note: this function assumes that tx_type has been checked to make sure it is a legacy - /// transaction - fn decode_legacy_tx( - encoded_tx_data: Span - ) -> Result { - let decoded_data = RLPTrait::decode(encoded_tx_data); - let decoded_data = decoded_data.map_err()?; - - if (decoded_data.len() != 1) { - return Result::Err(EthTransactionError::TopLevelRlpListWrongLength(decoded_data.len())); - } - - let decoded_data = *decoded_data.at(0); - - let result: Result = match decoded_data { - RLPItem::String => { Result::Err(EthTransactionError::ExpectedRLPItemToBeList) }, - RLPItem::List(val) => { - if (val.len() != 9) { - return Result::Err(EthTransactionError::LegacyTxWrongPayloadLength(val.len())); - } - - let ( - nonce_idx, - gas_price_idx, - gas_limit_idx, - to_idx, - value_idx, - calldata_idx, - chain_id_idx - ) = - ( - 0, 1, 2, 3, 4, 5, 6 - ); - - let nonce = (*val.at(nonce_idx)).parse_u128_from_string().map_err()?; - let gas_price = (*val.at(gas_price_idx)).parse_u128_from_string().map_err()?; - let gas_limit = (*val.at(gas_limit_idx)).parse_u128_from_string().map_err()?; - let to = (*val.at(to_idx)).try_parse_address_from_string().map_err()?; - let amount = (*val.at(value_idx)).parse_u256_from_string().map_err()?; - let calldata = (*val.at(calldata_idx)).parse_bytes_from_string().map_err()?; - let chain_id = (*val.at(chain_id_idx)).parse_u128_from_string().map_err()?; - - Result::Ok( - EthereumTransaction::LegacyTransaction( - LegacyTransaction { - nonce, gas_price, gas_limit, destination: to, amount, calldata, chain_id - } - ) - ) - } - }; - - result - } - - /// Decode a modern Ethereum transaction - /// This function decodes a modern Ethereum transaction in accordance with EIP-2718. - /// It returns transaction details including nonce, gas price, gas limit, destination address, - /// amount, payload, message hash, and chain id. The transaction hash is computed by keccak - /// hashing the signed transaction data, which includes the chain ID as part of the transaction - /// data itself. - /// # Arguments - /// * `encoded_tx_data` - The raw rlp encoded transaction data - /// Note: this function assumes that tx_type has been checked to make sure it is either EIP-2930 - /// or EIP-1559 transaction - fn decode_typed_tx( - encoded_tx_data: Span - ) -> Result { - let tx_type: u8 = (*encoded_tx_data.at(0)).into(); - let tx_type: TransactionType = match tx_type.try_into() { - Option::Some(v) => { v }, - Option::None => { return Result::Err(EthTransactionError::TransactionTypeError); } - }; - - let rlp_encoded_data = encoded_tx_data.slice(1, encoded_tx_data.len() - 1); - - let decoded_data = RLPTrait::decode(rlp_encoded_data).map_err()?; - if (decoded_data.len() != 1) { - return Result::Err(EthTransactionError::TopLevelRlpListWrongLength(decoded_data.len())); - } - - let decoded_data = match *decoded_data.at(0) { - RLPItem::String => { - return Result::Err(EthTransactionError::ExpectedRLPItemToBeList); - }, - RLPItem::List(v) => { v } - }; - - match tx_type { - TransactionType::Legacy => { - return Result::Err(EthTransactionError::TransactionTypeError); - }, - // tx_format (EIP-2930, unsigned): 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, - // to, value, data, accessList]) - TransactionType::EIP2930 => { - let chain_id = (*decoded_data.at(0)).parse_u128_from_string().map_err()?; - let nonce = (*decoded_data.at(1)).parse_u128_from_string().map_err()?; - let gas_price = (*decoded_data.at(2)).parse_u128_from_string().map_err()?; - let gas_limit = (*decoded_data.at(3)).parse_u128_from_string().map_err()?; - let to = (*decoded_data.at(4)).try_parse_address_from_string().map_err()?; - let amount = (*decoded_data.at(5)).parse_u256_from_string().map_err()?; - let calldata = (*decoded_data.at(6)).parse_bytes_from_string().map_err()?; - let access_list = (*decoded_data.at(7)).parse_access_list().map_err()?; - - Result::Ok( - EthereumTransaction::AccessListTransaction( - AccessListTransaction { - chain_id, - nonce, - gas_price, - gas_limit, - destination: to, - amount, - calldata, - access_list - } - ) - ) - }, - // 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]) - TransactionType::EIP1559 => { - let chain_id = (*decoded_data.at(0)).parse_u128_from_string().map_err()?; - let nonce = (*decoded_data.at(1)).parse_u128_from_string().map_err()?; - let max_priority_fee_per_gas = (*decoded_data.at(2)) - .parse_u128_from_string() - .map_err()?; - let max_fee_per_gas = (*decoded_data.at(3)).parse_u128_from_string().map_err()?; - let gas_limit = (*decoded_data.at(4)).parse_u128_from_string().map_err()?; - let to = (*decoded_data.at(5)).try_parse_address_from_string().map_err()?; - let amount = (*decoded_data.at(6)).parse_u256_from_string().map_err()?; - let calldata = (*decoded_data.at(7)).parse_bytes_from_string().map_err()?; - - let access_list = (*decoded_data.at(8)).parse_access_list().map_err()?; - - Result::Ok( - EthereumTransaction::FeeMarketTransaction( - FeeMarketTransaction { - chain_id, - nonce, - max_priority_fee_per_gas, - max_fee_per_gas, - gas_limit, - destination: to, - amount, - calldata, - access_list - } - ) - ) - } - } - } - - /// Check if a raw transaction is a legacy Ethereum transaction - /// This function checks if a raw transaction is a legacy Ethereum transaction by checking the - /// transaction type according to EIP-2718. - /// # Arguments - /// * `encoded_tx_data` - The raw rlp encoded transaction data - #[inline(always)] - fn is_legacy_tx(encoded_tx_data: Span) -> bool { - // From EIP2718: if it starts with a value in the range [0xc0, 0xfe] then it is a legacy - // transaction type - if (*encoded_tx_data[0] > 0xbf && *encoded_tx_data[0] < 0xff) { - return true; - } - - return false; - } -} - -#[derive(Drop, PartialEq)] -pub enum TransactionType { - Legacy, - EIP2930, - EIP1559 -} - -pub impl TranscationTypeIntoU8Impl of Into { - fn into(self: TransactionType) -> u8 { - match self { - TransactionType::Legacy => { 0 }, - TransactionType::EIP2930 => { 1 }, - TransactionType::EIP1559 => { 2 } - } - } -} - -pub impl TryIntoTransactionTypeImpl of TryInto { - fn try_into(self: u8) -> Option { - if (self == 0) { - return Option::Some(TransactionType::Legacy); - } - if (self == 1) { - return Option::Some(TransactionType::EIP2930); - } - if (self == 2) { - return Option::Some(TransactionType::EIP1559); - } - - Option::None - } -} - -#[generate_trait] -pub impl EthTransactionImpl of EthTransactionTrait { - /// Decode a raw Ethereum transaction - /// This function decodes a raw Ethereum transaction. It checks if the transaction - /// is a legacy transaction or a modern transaction, and calls the appropriate decode function - /// resp. `decode_legacy_tx` or `decode_tx` based on the result. - /// # Arguments - /// * `encoded_tx_data` - The raw transaction rlp encoded data - #[inline(always)] - fn decode(encoded_tx_data: Span) -> Result { - let encoded_tx: EncodedTransaction = deserialize_encoded_transaction(encoded_tx_data) - .ok_or(EthTransactionError::TransactionTypeError)?; - - encoded_tx.decode() - } - - /// Validate an Ethereum transaction - /// This function validates an Ethereum transaction by checking if the transaction - /// is correctly signed by the given address, and if the nonce in the transaction - /// matches the nonce of the account. - /// It decodes the transaction using the decode function, - /// and then verifies the Ethereum signature on the transaction hash. - /// # Arguments - /// * `tx_metadata` - The ethereum transaction metadata - /// * `encoded_tx_data` - The raw rlp encoded transaction data - fn validate_eth_tx( - tx_metadata: TransactionMetadata, encoded_tx_data: Span - ) -> Result { - let TransactionMetadata { address, account_nonce, chain_id, signature } = tx_metadata; - - let decoded_tx = Self::decode(encoded_tx_data)?; - - if (decoded_tx.nonce() != account_nonce) { - return Result::Err(EthTransactionError::IncorrectAccountNonce); - } - if (decoded_tx.chain_id() != chain_id) { - 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 necessary - - let msg_hash = encoded_tx_data.compute_keccak256_hash(); - - // this will panic if verification fails - verify_eth_signature(msg_hash, signature, address); - - Result::Ok(true) - } -} - -#[cfg(test)] -mod tests { - use core::option::OptionTrait; - use core::starknet::EthAddress; - use core::starknet::secp256_trait::{Signature}; - use evm::test_utils::chain_id; - - use utils::eth_transaction::{ - deserialize_encoded_transaction, EthTransactionTrait, EncodedTransactionTrait, - EncodedTransaction, TransactionMetadata, EthTransactionError, EthereumTransaction, - EthereumTransactionTrait, AccessListItem - }; - use utils::helpers::ToBytes; - use utils::test_data::{ - legacy_rlp_encoded_tx, legacy_rlp_encoded_deploy_tx, eip_2930_encoded_tx, - eip_1559_encoded_tx - }; - - - #[test] - fn test_decode_legacy_tx() { - // tx_format (EIP-155, unsigned): [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, - // 0] - // expected rlp decoding: [ '0x', '0x3b9aca00', '0x1e8480', - // '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984', '0x016345785d8a0000', '0xabcdef', - // '0x434841494e5f4944', '0x', '0x' ] - // message_hash: 0x1026be08dc5113457dc5550128d53b1d2b2b6418ffe098468f805ecdcf34efd1 - // chain id used: 0x434841494e5f4944 - let data = legacy_rlp_encoded_tx(); - - let encoded_tx: Option = deserialize_encoded_transaction(data); - let encoded_tx = encoded_tx.unwrap(); - assert(encoded_tx == EncodedTransaction::Legacy(data), 'encoded_tx is not Legacy'); - - let tx = encoded_tx.decode().expect('decode failed'); - let tx = match (tx) { - EthereumTransaction::LegacyTransaction(tx) => { tx }, - EthereumTransaction::AccessListTransaction(_) => { - return panic!("Not Legacy Transaction"); - }, - EthereumTransaction::FeeMarketTransaction(_) => { - return panic!("Not Legacy Transaction"); +/// Get the effective gas price of a transaction as specfified in EIP-1559 with relevant +/// checks. +fn get_effective_gas_price( + max_fee_per_gas: Option, max_priority_fee_per_gas: Option, block_base_fee: u256, +) -> Result { + match max_fee_per_gas { + Option::Some(max_fee) => { + let max_priority_fee_per_gas = max_priority_fee_per_gas.unwrap_or(0); + + // only enforce the fee cap if provided input is not zero + if !(max_fee.is_zero() && max_priority_fee_per_gas.is_zero()) + && max_fee < block_base_fee { + // `base_fee_per_gas` is greater than the `max_fee_per_gas` + return Result::Err(EthTransactionError::FeeCapTooLow); } - }; - - assert_eq!(tx.chain_id, chain_id()); - assert_eq!(tx.nonce, 0); - assert_eq!(tx.gas_price, 0x3b9aca00); - assert_eq!(tx.gas_limit, 0x1e8480); - assert_eq!(tx.destination.unwrap().into(), 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984,); - assert_eq!(tx.amount, 0x016345785d8a0000); - - let expected_calldata = 0xabcdef_u32.to_be_bytes(); - assert(tx.calldata == expected_calldata, 'calldata is not 0xabcdef'); - } - - #[test] - fn test_decode_legacy_deploy_tx() { - // tx_format (EIP-155, unsigned): [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, - // 0] - // expected rlp decoding: - // - // ["0x","0x0a","0x061a80","0x","0x0186a0","0x600160010a5060006000f3","0x4b4b5254","0x","0x"] - let data = legacy_rlp_encoded_deploy_tx(); - - let encoded_tx: Option = deserialize_encoded_transaction(data); - let encoded_tx = encoded_tx.unwrap(); - assert(encoded_tx == EncodedTransaction::Legacy(data), 'encoded_tx is not Legacy'); - - let tx = encoded_tx.decode().expect('decode failed').try_into_legacy_transaction().unwrap(); - - assert_eq!(tx.chain_id, 'KKRT'.try_into().unwrap()); - assert_eq!(tx.nonce, 0); - assert_eq!(tx.gas_price, 0x0a); - assert_eq!(tx.gas_limit, 0x061a80); - assert!(tx.destination.is_none()); - assert_eq!(tx.amount, 0x0186a0); - - let expected_calldata = 0x600160010a5060006000f3_u256.to_be_bytes(); - assert(tx.calldata == expected_calldata, 'calldata is not 0xabcdef'); - } - - #[test] - fn test_decode_eip_2930_tx() { - // tx_format (EIP-2930, unsigned): 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, - // value, data, accessList]) - // expected rlp decoding: [ "0x434841494e5f4944", "0x", "0x3b9aca00", "0x1e8480", - // "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "0x016345785d8a0000", "0xabcdef", - // [["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", - // ["0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65", - // "0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94"]]] ] - // message_hash: 0xc00f61dcc99a78934275c404267b9d035cad7f71cf3ae2ed2c5a55b601a5c107 - // chain id used: 0x434841494e5f4944 - let data = eip_2930_encoded_tx(); - - let encoded_tx: Option = deserialize_encoded_transaction(data); - let encoded_tx = encoded_tx.unwrap(); - assert(encoded_tx == EncodedTransaction::EIP2930(data), 'encoded_tx is not Eip2930'); - - let tx = encoded_tx - .decode() - .expect('decode failed') - .try_into_access_list_transaction() - .unwrap(); - - assert_eq!(tx.chain_id, chain_id()); - assert_eq!(tx.nonce, 0); - assert_eq!(tx.gas_price, 0x3b9aca00); - assert_eq!(tx.gas_limit, 0x1e8480); - assert_eq!(tx.destination.unwrap().into(), 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984); - assert_eq!(tx.amount, 0x016345785d8a0000); - - let expected_access_list = [ - AccessListItem { - ethereum_address: 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.try_into().unwrap(), - storage_keys: [ - 0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65, - 0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94 - ].span() - } - ].span(); - assert!(tx.access_list == expected_access_list, "access lists are not equal"); - - let expected_calldata = 0xabcdef_u32.to_be_bytes(); - assert(tx.calldata == expected_calldata, 'calldata is not 0xabcdef'); - } - - - #[test] - fn test_decode_eip_1559_tx() { - // 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]) - // expected rlp decoding: [ "0x434841494e5f4944", "0x", "0x", "0x3b9aca00", "0x1e8480", - // "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "0x016345785d8a0000", "0xabcdef", - // [[["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", - // ["0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65", - // "0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94"]]] ] ] - // message_hash: 0xa2de478d0c94b4be637523b818d03b6a1841fca63fd044976fcdbef3c57a87b0 - // chain id used: 0x434841494e5f4944 - let data = eip_1559_encoded_tx(); - - let encoded_tx: Option = deserialize_encoded_transaction(data); - let encoded_tx = encoded_tx.unwrap(); - assert(encoded_tx == EncodedTransaction::EIP1559(data), 'encoded_tx is not EIP1559'); - - let tx = encoded_tx - .decode() - .expect('decode failed') - .try_into_fee_market_transaction() - .unwrap(); - - assert_eq!(tx.chain_id, chain_id()); - assert_eq!(tx.nonce, 0); - assert_eq!(tx.max_priority_fee_per_gas, 0); - assert_eq!(tx.max_fee_per_gas, 0x3b9aca00); - assert_eq!(tx.gas_limit, 0x1e8480); - assert_eq!(tx.destination.unwrap().into(), 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984); - assert_eq!(tx.amount, 0x016345785d8a0000); - - let expected_access_list = [ - AccessListItem { - ethereum_address: 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.try_into().unwrap(), - storage_keys: [ - 0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65, - 0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94 - ].span() + if max_fee < max_priority_fee_per_gas { + // `max_priority_fee_per_gas` is greater than the `max_fee_per_gas` + return Result::Err(EthTransactionError::TipAboveFeeCap); } - ].span(); - assert!(tx.access_list == expected_access_list, "access lists are not equal"); - - let expected_calldata = 0xabcdef_u32.to_be_bytes(); - assert(tx.calldata == expected_calldata, 'calldata is not 0xabcdef'); - } - - - #[test] - fn test_is_legacy_tx_eip_155_tx() { - let encoded_tx_data = legacy_rlp_encoded_tx(); - let result = EncodedTransactionTrait::is_legacy_tx(encoded_tx_data); - - assert(result, 'is_legacy_tx expected true'); - } - - #[test] - fn test_is_legacy_tx_eip_1559_tx() { - let encoded_tx_data = eip_1559_encoded_tx(); - let result = EncodedTransactionTrait::is_legacy_tx(encoded_tx_data); - - assert(!result, 'is_legacy_tx expected false'); - } - - #[test] - fn test_is_legacy_tx_eip_2930_tx() { - let encoded_tx_data = eip_2930_encoded_tx(); - let result = EncodedTransactionTrait::is_legacy_tx(encoded_tx_data); - - assert(!result, 'is_legacy_tx expected false'); - } - - - #[test] - fn test_validate_legacy_tx() { - let encoded_tx_data = legacy_rlp_encoded_tx(); - let address: EthAddress = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf_u256.into(); - let account_nonce = 0x0; - let chain_id = chain_id(); - - // to reproduce locally: - // run: cp .env.example .env - // bun install & bun run scripts/compute_rlp_encoding.ts - let signature = Signature { - r: 0x5e5202c7e9d6d0964a1f48eaecf12eef1c3cafb2379dfeca7cbd413cedd4f2c7, - s: 0x66da52d0b666fc2a35895e0c91bc47385fe3aa347c7c2a129ae2b7b06cb5498b, - y_parity: false - }; - - let validate_tx_param = TransactionMetadata { address, account_nonce, chain_id, signature }; - - let result = EthTransactionTrait::validate_eth_tx(validate_tx_param, encoded_tx_data) - .expect('signature verification failed'); - assert(result, 'result is not true'); - } - - - #[test] - fn test_validate_eip_2930_tx() { - let encoded_tx_data = eip_2930_encoded_tx(); - let address: EthAddress = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf_u256.into(); - let account_nonce = 0x0; - let chain_id = chain_id(); - - // to reproduce locally: - // run: cp .env.example .env - // bun install & bun run scripts/compute_rlp_encoding.ts - let signature = Signature { - r: 0xbced8d81c36fe13c95b883b67898b47b4b70cae79e89fa27856ddf8c533886d1, - s: 0x3de0109f00bc3ed95ffec98edd55b6f750cb77be8e755935dbd6cfec59da7ad0, - y_parity: true - }; - - let validate_tx_param = TransactionMetadata { address, account_nonce, chain_id, signature }; - - let result = EthTransactionTrait::validate_eth_tx(validate_tx_param, encoded_tx_data) - .expect('signature verification failed'); - assert(result, 'result is not true'); - } - - - #[test] - fn test_validate_eip_1559_tx() { - let encoded_tx_data = eip_1559_encoded_tx(); - let address: EthAddress = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf_u256.into(); - let account_nonce = 0x0; - let chain_id = chain_id(); - - // to reproduce locally: - // run: cp .env.example .env - // bun install & bun run scripts/compute_rlp_encoding.ts - let signature = Signature { - r: 0x0f9a716653c19fefc240d1da2c5759c50f844fc8835c82834ea3ab7755f789a0, - s: 0x71506d904c05c6e5ce729b5dd88bcf29db9461c8d72413b864923e8d8f6650c0, - y_parity: true - }; - - let validate_tx_param = TransactionMetadata { address, account_nonce, chain_id, signature }; - - let result = EthTransactionTrait::validate_eth_tx(validate_tx_param, encoded_tx_data) - .expect('signature verification failed'); - assert(result, 'result is not true'); - } - - #[test] - fn test_validate_should_fail_for_wrong_account_id() { - let encoded_tx_data = eip_1559_encoded_tx(); - let address: EthAddress = 0x6Bd85F39321B00c6d603474C5B2fddEB9c92A466_u256.into(); - // the tx was signed for nonce 0x0 - let wrong_account_nonce = 0x1; - let chain_id = 0x1; - - // to reproduce locally: - // run: cp .env.example .env - // bun install & bun run scripts/compute_rlp_encoding.ts - let signature = Signature { - r: 0x141615694556f9078d9da3249e8aa1987524f57153121599cf36d7681b809858, - s: 0x052052478f912dbe80339e3f198be8c9e1cd44eaabb295d912087d975ef38192, - y_parity: false - }; - - let validate_tx_param = TransactionMetadata { - address, account_nonce: wrong_account_nonce, chain_id, signature - }; - - let result = EthTransactionTrait::validate_eth_tx(validate_tx_param, encoded_tx_data) - .expect_err('expected to fail'); - assert(result == EthTransactionError::IncorrectAccountNonce, 'result is not true'); - } - - #[test] - fn test_validate_should_fail_for_wrong_chain_id() { - let encoded_tx_data = eip_1559_encoded_tx(); - let address: EthAddress = 0x6Bd85F39321B00c6d603474C5B2fddEB9c92A466_u256.into(); - let account_nonce = 0x0; - // the tx was signed for chain_id 0x1 - let wrong_chain_id = 0x2; - - // to reproduce locally: - // run: cp .env.example .env - // bun install & bun run scripts/compute_rlp_encoding.ts - let signature = Signature { - r: 0x141615694556f9078d9da3249e8aa1987524f57153121599cf36d7681b809858, - s: 0x052052478f912dbe80339e3f198be8c9e1cd44eaabb295d912087d975ef38192, - y_parity: false - }; - - let validate_tx_param = TransactionMetadata { - address, account_nonce, chain_id: wrong_chain_id, signature - }; - - let result = EthTransactionTrait::validate_eth_tx(validate_tx_param, encoded_tx_data) - .expect_err('expected to fail'); - assert(result == EthTransactionError::IncorrectChainId, 'result is not true'); + Result::Ok( + min( + max_fee, + block_base_fee + .checked_add(max_priority_fee_per_gas) + .ok_or(EthTransactionError::TipVeryHigh)?, + ) + ) + }, + Option::None => Result::Ok( + block_base_fee + .checked_add(max_priority_fee_per_gas.unwrap_or(0)) + .ok_or(EthTransactionError::TipVeryHigh)? + ), } } diff --git a/crates/utils/src/eth_transaction/common.cairo b/crates/utils/src/eth_transaction/common.cairo new file mode 100644 index 000000000..92b012a3b --- /dev/null +++ b/crates/utils/src/eth_transaction/common.cairo @@ -0,0 +1,51 @@ +use core::starknet::EthAddress; + +/// The `to` field of a transaction. Either a target address, or empty for a +/// contract creation. +#[derive(Copy, Drop, Debug, Default, PartialEq, Serde)] +pub enum TxKind { + /// A transaction that creates a contract. + #[default] + Create, + /// A transaction that calls a contract or transfer. + Call: EthAddress, +} + +impl OptionAddressIntoTxKind of Into, TxKind> { + fn into(self: Option) -> TxKind { + match self { + Option::None => TxKind::Create, + Option::Some(address) => TxKind::Call(address), + } + } +} + +impl AddressIntoTxKind of Into { + fn into(self: EthAddress) -> TxKind { + TxKind::Call(self) + } +} + +#[generate_trait] +pub impl TxKindImpl of TxKindTrait { + fn is_create(self: @TxKind) -> bool { + match self { + TxKind::Create => true, + TxKind::Call(_) => false, + } + } + + fn is_call(self: @TxKind) -> bool { + match self { + TxKind::Create => false, + TxKind::Call(_) => true, + } + } + + fn to(self: @TxKind) -> Option { + match self { + TxKind::Create => Option::None, + TxKind::Call(address) => Option::Some(*address), + } + } +} diff --git a/crates/utils/src/eth_transaction/eip1559.cairo b/crates/utils/src/eth_transaction/eip1559.cairo new file mode 100644 index 000000000..de0fef5e4 --- /dev/null +++ b/crates/utils/src/eth_transaction/eip1559.cairo @@ -0,0 +1,127 @@ +use core::num::traits::SaturatingSub; +use crate::errors::{EthTransactionError, RLPError, RLPHelpersErrorTrait}; +use crate::eth_transaction::common::TxKind; +use crate::eth_transaction::eip2930::AccessListItem; +use crate::rlp::{RLPItem, RLPHelpersTrait}; +use crate::traits::SpanDefault; + +/// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)). +#[derive(Copy, Drop, Debug, Default, PartialEq, Serde)] +pub struct TxEip1559 { + /// EIP-155: Simple replay attack protection + pub chain_id: u64, + /// A scalar value equal to the number of transactions sent by the sender + pub nonce: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; + pub gas_limit: u64, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + pub max_fee_per_gas: u128, + /// Max Priority fee that transaction is paying + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + /// + pub max_priority_fee_per_gas: u128, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅ + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; + pub value: u256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: Span, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call, formally Td. + pub input: Span, +} + +#[generate_trait] +pub impl _impl of TxEip1559Trait { + /// Returns the effective gas price for the given `base_fee`. + fn effective_gas_price(self: @TxEip1559, base_fee: Option) -> u128 { + match base_fee { + Option::Some(base_fee) => { + let tip = (*self.max_fee_per_gas).saturating_sub(base_fee); + if tip > (*self.max_priority_fee_per_gas) { + (*self.max_priority_fee_per_gas) + base_fee + } else { + *self.max_fee_per_gas + } + }, + Option::None => { *self.max_fee_per_gas } + } + } + + fn decode_fields(ref data: Span) -> Result { + let boxed_fields = data + .multi_pop_front::<9>() + .ok_or(EthTransactionError::RLPError(RLPError::InputTooShort))?; + let [ + chain_id_encoded, + nonce_encoded, + max_priority_fee_per_gas_encoded, + max_fee_per_gas_encoded, + gas_limit_encoded, + to_encoded, + value_encoded, + input_encoded, + access_list_encoded + ] = + (*boxed_fields) + .unbox(); + + let chain_id = chain_id_encoded.parse_u64_from_string().map_err()?; + let nonce = nonce_encoded.parse_u64_from_string().map_err()?; + let max_priority_fee_per_gas = max_priority_fee_per_gas_encoded + .parse_u128_from_string() + .map_err()?; + let max_fee_per_gas = max_fee_per_gas_encoded.parse_u128_from_string().map_err()?; + let gas_limit = gas_limit_encoded.parse_u64_from_string().map_err()?; + let to = to_encoded.try_parse_address_from_string().map_err()?; + let value = value_encoded.parse_u256_from_string().map_err()?; + let input = input_encoded.parse_bytes_from_string().map_err()?; + let access_list = access_list_encoded.parse_access_list().map_err()?; + + let txkind_to = match to { + Option::Some(to) => { TxKind::Call(to) }, + Option::None => { TxKind::Create } + }; + + Result::Ok( + TxEip1559 { + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to: txkind_to, + value, + access_list, + input, + } + ) + } +} diff --git a/crates/utils/src/eth_transaction/eip2930.cairo b/crates/utils/src/eth_transaction/eip2930.cairo new file mode 100644 index 000000000..d7fa8f985 --- /dev/null +++ b/crates/utils/src/eth_transaction/eip2930.cairo @@ -0,0 +1,119 @@ +use core::starknet::EthAddress; +use crate::errors::{EthTransactionError, RLPError, RLPHelpersErrorTrait}; +use crate::eth_transaction::common::TxKind; +use crate::rlp::{RLPItem, RLPHelpersTrait}; +use crate::traits::SpanDefault; + + +#[derive(Copy, Drop, Serde, PartialEq, Debug)] +pub struct AccessListItem { + pub ethereum_address: EthAddress, + pub storage_keys: Span +} + +#[generate_trait] +pub impl AccessListItemImpl of AccessListItemTrait { + fn to_storage_keys(self: @AccessListItem) -> Span<(EthAddress, u256)> { + let AccessListItem { ethereum_address, mut storage_keys } = *self; + + let mut storage_keys_arr = array![]; + while let Option::Some(storage_key) = storage_keys.pop_front() { + storage_keys_arr.append((ethereum_address, *storage_key)); + }; + + storage_keys_arr.span() + } +} + + +/// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)). +#[derive(Copy, Drop, Debug, Default, PartialEq, Serde)] +pub struct TxEip2930 { + /// Added as EIP-pub 155: Simple replay attack protection + pub chain_id: u64, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; formally Tg. + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅, used here to denote the only member of B0 ; formally Tt. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; + pub value: u256, + /// The accessList specifies a list of addresses and storage keys; + /// these addresses and storage keys are added into the `accessed_addresses` + /// and `accessed_storage_keys` global sets (introduced in EIP-2929). + /// A gas cost is charged, though at a discount relative to the cost of + /// accessing outside the list. + pub access_list: Span, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call; + pub input: Span, +} + + +#[generate_trait] +pub impl _impl of TxEip2930Trait { + fn decode_fields(ref data: Span) -> Result { + let boxed_fields = data + .multi_pop_front::<8>() + .ok_or(EthTransactionError::RLPError(RLPError::InputTooShort))?; + let [ + chain_id_encoded, + nonce_encoded, + gas_price_encoded, + gas_limit_encoded, + to_encoded, + value_encoded, + input_encoded, + access_list_encoded + ] = + (*boxed_fields) + .unbox(); + + let chain_id = chain_id_encoded.parse_u64_from_string().map_err()?; + let nonce = nonce_encoded.parse_u64_from_string().map_err()?; + let gas_price = gas_price_encoded.parse_u128_from_string().map_err()?; + let gas_limit = gas_limit_encoded.parse_u64_from_string().map_err()?; + let to = to_encoded.try_parse_address_from_string().map_err()?; + let value = value_encoded.parse_u256_from_string().map_err()?; + let input = input_encoded.parse_bytes_from_string().map_err()?; + let access_list = access_list_encoded.parse_access_list().map_err()?; + + let txkind_to = match to { + Option::Some(to) => { TxKind::Call(to) }, + Option::None => { TxKind::Create } + }; + + Result::Ok( + TxEip2930 { + chain_id: chain_id, + nonce, + gas_price, + gas_limit, + input, + access_list, + to: txkind_to, + value, + } + ) + } +} diff --git a/crates/utils/src/eth_transaction/legacy.cairo b/crates/utils/src/eth_transaction/legacy.cairo new file mode 100644 index 000000000..3119ef4bc --- /dev/null +++ b/crates/utils/src/eth_transaction/legacy.cairo @@ -0,0 +1,39 @@ +use crate::eth_transaction::common::TxKind; +use crate::traits::SpanDefault; + + +#[derive(Copy, Drop, Debug, Default, PartialEq, Serde)] +pub struct TxLegacy { + /// Added as EIP-155: Simple replay attack protection + pub chain_id: Option, + /// A scalar value equal to the number of transactions sent by the sender; formally Tn. + pub nonce: u64, + /// A scalar value equal to the number of + /// Wei to be paid per unit of gas for all computation + /// costs incurred as a result of the execution of this transaction; formally Tp. + /// + /// As ethereum circulation is around 120mil eth as of 2022 that is around + /// 120000000000000000000000000 wei we are safe to use u128 as its max number is: + /// 340282366920938463463374607431768211455 + pub gas_price: u128, + /// A scalar value equal to the maximum + /// amount of gas that should be used in executing + /// this transaction. This is paid up-front, before any + /// computation is done and may not be increased + /// later; + pub gas_limit: u64, + /// The 160-bit address of the message call’s recipient or, for a contract creation + /// transaction, ∅. + pub to: TxKind, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account; + pub value: u256, + /// Input has two uses depending if transaction is Create or Call (if `to` field is None or + /// Some). pub init: An unlimited size byte array specifying the + /// EVM-code for the account initialisation procedure CREATE, + /// data: An unlimited size byte array specifying the + /// input data of the message call. + pub input: Span, +} diff --git a/crates/utils/src/eth_transaction/transaction.cairo b/crates/utils/src/eth_transaction/transaction.cairo new file mode 100644 index 000000000..cb9d9571b --- /dev/null +++ b/crates/utils/src/eth_transaction/transaction.cairo @@ -0,0 +1,532 @@ +use core::starknet::EthAddress; +use crate::errors::{RLPError, EthTransactionError, RLPErrorTrait, RLPHelpersErrorTrait}; +use crate::eth_transaction::common::{TxKind, TxKindTrait}; +use crate::eth_transaction::eip1559::{TxEip1559, TxEip1559Trait}; +use crate::eth_transaction::eip2930::{AccessListItem, TxEip2930, TxEip2930Trait}; +use crate::eth_transaction::legacy::TxLegacy; +use crate::eth_transaction::tx_type::{TxType}; +use crate::helpers::U8SpanExTrait; +use crate::rlp::{RLPItem, RLPTrait, RLPHelpersTrait}; +use crate::traits::{DefaultSignature}; + + +#[derive(Copy, Debug, Drop, PartialEq, Serde)] +pub enum Transaction { + /// Legacy transaction (type `0x0`). + /// + /// Traditional Ethereum transactions, containing parameters `nonce`, `gasPrice`, `gasLimit`, + /// `to`, `value`, `data`, `v`, `r`, and `s`. + /// + /// These transactions do not utilize access lists nor do they incorporate EIP-1559 fee market + /// changes. + #[default] + Legacy: TxLegacy, + /// Transaction with an [`AccessList`] ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)), + /// type `0x1`. + /// + /// The `accessList` specifies an array of addresses and storage keys that the transaction + /// plans to access, enabling gas savings on cross-contract calls by pre-declaring the accessed + /// contract and storage slots. + Eip2930: TxEip2930, + /// A transaction with a priority fee ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)), + /// type `0x2`. + /// + /// Unlike traditional transactions, EIP-1559 transactions use an in-protocol, dynamically + /// changing base fee per gas, adjusted at each block to manage network congestion. + /// + /// - `maxPriorityFeePerGas`, specifying the maximum fee above the base fee the sender is + /// willing to pay + /// - `maxFeePerGas`, setting the maximum total fee the sender is willing to pay. + /// + /// The base fee is burned, while the priority fee is paid to the miner who includes the + /// transaction, incentivizing miners to include transactions with higher priority fees per + /// gas. + Eip1559: TxEip1559, +} + +#[generate_trait] +pub impl _Transasction of TransactionTrait { + /// Get `chain_id`. + fn chain_id(self: @Transaction) -> Option { + match (*self) { + Transaction::Legacy(tx) => tx.chain_id, + Transaction::Eip2930(TxEip2930 { chain_id, .. }) | + Transaction::Eip1559(TxEip1559 { chain_id, .. }) => Option::Some(chain_id), + } + } + + /// Gets the transaction's [`TxKind`], which is the address of the recipient or + /// [`TxKind::Create`] if the transaction is a contract creation. + fn kind(self: @Transaction) -> TxKind { + match (*self) { + Transaction::Legacy(TxLegacy { to, .. }) | Transaction::Eip2930(TxEip2930 { to, .. }) | + Transaction::Eip1559(TxEip1559 { to, .. }) => to, + } + } + + /// Get the transaction's address of the contract that will be called, or the address that will + /// receive the transfer. + /// + /// Returns `None` if this is a `CREATE` transaction. + fn to(self: @Transaction) -> Option { + self.kind().to() + } + + /// Get the transaction's type + fn transaction_type(self: @Transaction) -> TxType { + match (*self) { + Transaction::Legacy(_) => TxType::Legacy, + Transaction::Eip2930(_) => TxType::Eip2930, + Transaction::Eip1559(_) => TxType::Eip1559, + } + } + + /// Gets the transaction's value field. + fn value(self: @Transaction) -> u256 { + match (*self) { + Transaction::Legacy(TxLegacy { value, .. }) | + Transaction::Eip2930(TxEip2930 { value, .. }) | + Transaction::Eip1559(TxEip1559 { value, .. }) => value, + } + } + + /// Get the transaction's nonce. + fn nonce(self: @Transaction) -> u64 { + match (*self) { + Transaction::Legacy(TxLegacy { nonce, .. }) | + Transaction::Eip2930(TxEip2930 { nonce, .. }) | + Transaction::Eip1559(TxEip1559 { nonce, .. }) => nonce, + } + } + + /// Returns the [`AccessList`] of the transaction. + /// + /// Returns `None` for legacy transactions. + fn access_list(self: @Transaction) -> Option> { + match (*self) { + Transaction::Eip2930(TxEip2930 { access_list, .. }) | + Transaction::Eip1559(TxEip1559 { access_list, .. }) => Option::Some(access_list), + _ => Option::None, + } + } + + /// Get the gas limit of the transaction. + fn gas_limit(self: @Transaction) -> u64 { + match (*self) { + Transaction::Legacy(TxLegacy { gas_limit, .. }) | + Transaction::Eip2930(TxEip2930 { gas_limit, .. }) | + Transaction::Eip1559(TxEip1559 { gas_limit, .. }) => gas_limit.try_into().unwrap(), + } + } + + /// Max fee per gas for eip1559 transaction, for legacy transactions this is `gas_price`. + fn max_fee_per_gas(self: @Transaction) -> u128 { + match (*self) { + Transaction::Eip1559(TxEip1559 { max_fee_per_gas, .. }) => max_fee_per_gas, + Transaction::Legacy(TxLegacy { gas_price, .. }) | + Transaction::Eip2930(TxEip2930 { gas_price, .. }) => gas_price, + } + } + + /// Max priority fee per gas for eip1559 transaction, for legacy and eip2930 transactions this + /// is `None` + fn max_priority_fee_per_gas(self: @Transaction) -> Option { + match (*self) { + Transaction::Eip1559(TxEip1559 { max_priority_fee_per_gas, + .. }) => Option::Some(max_priority_fee_per_gas), + _ => Option::None, + } + } + + /// Return the max priority fee per gas if the transaction is an EIP-1559 transaction, and + /// otherwise return the gas price. + /// + /// # Warning + /// + /// This is different than the `max_priority_fee_per_gas` method, which returns `None` for + /// non-EIP-1559 transactions. + fn priority_fee_or_price(self: @Transaction) -> u128 { + match (*self) { + Transaction::Eip1559(TxEip1559 { max_priority_fee_per_gas, + .. }) => max_priority_fee_per_gas, + Transaction::Legacy(TxLegacy { gas_price, .. }) | + Transaction::Eip2930(TxEip2930 { gas_price, .. }) => gas_price, + } + } + + /// Returns the effective gas price for the given base fee. + /// + /// If the transaction is a legacy or EIP2930 transaction, the gas price is returned. + fn effective_gas_price(self: @Transaction, base_fee: Option) -> u128 { + match (*self) { + Transaction::Legacy(tx) => tx.gas_price, + Transaction::Eip2930(tx) => tx.gas_price, + Transaction::Eip1559(tx) => tx.effective_gas_price(base_fee) + } + } + + /// Get the transaction's input field. + fn input(self: @Transaction) -> Span { + match (*self) { + Transaction::Legacy(tx) => tx.input, + Transaction::Eip2930(tx) => tx.input, + Transaction::Eip1559(tx) => tx.input, + } + } +} + + +#[derive(Copy, Drop, Debug, PartialEq)] +pub struct TransactionUnsigned { + /// Transaction hash + pub hash: u256, + /// Raw transaction info + pub transaction: Transaction, +} + +#[generate_trait] +pub impl _TransactionUnsigned of TransactionUnsignedTrait { + /// Decodes the "raw" format of transaction (similar to `eth_sendRawTransaction`). + /// + /// This should be used for any method that accepts a raw transaction. + /// * `eth_send_raw_transaction`. + /// + /// A raw transaction is either a legacy transaction or EIP-2718 typed transaction. + /// + /// For legacy transactions, the format is encoded as: `rlp(tx-data)`. This format will start + /// with a RLP list header. + /// + /// For EIP-2718 typed transactions, the format is encoded as the type of the transaction + /// followed by the rlp of the transaction: `type || rlp(tx-data)`. + /// + /// Both for legacy and EIP-2718 transactions, an error will be returned if there is an excess + /// of bytes in input data. + fn decode_enveloped( + ref tx_data: Span, + ) -> Result { + if tx_data.is_empty() { + return Result::Err(EthTransactionError::RLPError(RLPError::InputTooShort)); + } + + // Check if it's a list + let transaction_signed = if Self::is_legacy_tx(tx_data) { + // Decode as a legacy transaction + Self::decode_legacy_tx(ref tx_data)? + } else { + Self::decode_enveloped_typed_transaction(ref tx_data)? + }; + + //TODO: check that the entire input was consumed and that there are no extra bytes at the + //end. + + Result::Ok(transaction_signed) + } + + /// Decode a legacy Ethereum transaction + /// This function decodes a legacy Ethereum transaction in accordance with EIP-155. + /// It returns transaction details including nonce, gas price, gas limit, destination address, + /// amount, payload, message hash, chain id. The transaction hash is computed by keccak hashing + /// the signed transaction data, which includes the chain ID in accordance with EIP-155. + /// # Arguments + /// * encoded_tx_data - The raw rlp encoded transaction data + /// * encoded_tx_data - is of the format: rlp![nonce, gasPrice, gasLimit, to , value, data, + /// chainId, 0, 0] + /// Note: this function assumes that tx_type has been checked to make sure it is a legacy + /// transaction + fn decode_legacy_tx( + ref encoded_tx_data: Span + ) -> Result { + let rlp_decoded_data = RLPTrait::decode(encoded_tx_data); + let rlp_decoded_data = rlp_decoded_data.map_err()?; + + if (rlp_decoded_data.len() != 1) { + return Result::Err( + EthTransactionError::TopLevelRlpListWrongLength(rlp_decoded_data.len()) + ); + } + + let rlp_decoded_data = *rlp_decoded_data.at(0); + let legacy_tx: TxLegacy = match rlp_decoded_data { + RLPItem::String => { Result::Err(EthTransactionError::ExpectedRLPItemToBeList)? }, + RLPItem::List(mut val) => { + if (val.len() != 9) { + return Result::Err(EthTransactionError::LegacyTxWrongPayloadLength(val.len())); + } + + let boxed_fields = val + .multi_pop_front::<7>() + .ok_or(EthTransactionError::RLPError(RLPError::InputTooShort))?; + let [ + nonce_encoded, + gas_price_encoded, + gas_limit_encoded, + to_encoded, + value_encoded, + input_encoded, + chain_id_encoded + ] = + (*boxed_fields) + .unbox(); + + let nonce = nonce_encoded.parse_u64_from_string().map_err()?; + let gas_price = gas_price_encoded.parse_u128_from_string().map_err()?; + let gas_limit = gas_limit_encoded.parse_u64_from_string().map_err()?; + let to = to_encoded.try_parse_address_from_string().map_err()?; + let value = value_encoded.parse_u256_from_string().map_err()?; + let input = input_encoded.parse_bytes_from_string().map_err()?; + let chain_id = chain_id_encoded.parse_u64_from_string().map_err()?; + + let transact_to = match to { + Option::Some(to) => { TxKind::Call(to) }, + Option::None => { TxKind::Create } + }; + + TxLegacy { + nonce, + gas_price, + gas_limit, + to: transact_to, + value, + input, + chain_id: Option::Some(chain_id), + } + } + }; + + let tx_hash = Self::compute_hash(encoded_tx_data); + + Result::Ok( + TransactionUnsigned { transaction: Transaction::Legacy(legacy_tx), hash: tx_hash, } + ) + } + + /// Decodes an enveloped EIP-2718 typed transaction. + /// + /// This should _only_ be used internally in general transaction decoding methods, + /// which have already ensured that the input is a typed transaction with the following format: + /// `tx-type || rlp(tx-data)` + /// + /// Note that this format does not start with any RLP header, and instead starts with a single + /// byte indicating the transaction type. + /// + /// CAUTION: this expects that `data` is `tx-type || rlp(tx-data)` + fn decode_enveloped_typed_transaction( + ref encoded_tx_data: Span + ) -> Result { + // keep this around so we can use it to calculate the hash + let original_data = encoded_tx_data; + + let tx_type = encoded_tx_data + .pop_front() + .ok_or(EthTransactionError::RLPError(RLPError::InputTooShort))?; + let tx_type: TxType = (*tx_type) + .try_into() + .ok_or(EthTransactionError::RLPError(RLPError::Custom('unsupported tx type')))?; + + let rlp_decoded_data = RLPTrait::decode(encoded_tx_data).map_err()?; + if (rlp_decoded_data.len() != 1) { + return Result::Err( + EthTransactionError::RLPError(RLPError::Custom('not encoded as list')) + ); + } + + let mut rlp_decoded_data = match *rlp_decoded_data.at(0) { + RLPItem::String => { + return Result::Err( + EthTransactionError::RLPError(RLPError::Custom('not encoded as list')) + ); + }, + RLPItem::List(v) => { v } + }; + + let transaction = match tx_type { + TxType::Eip2930 => Transaction::Eip2930( + TxEip2930Trait::decode_fields(ref rlp_decoded_data)? + ), + TxType::Eip1559 => Transaction::Eip1559( + TxEip1559Trait::decode_fields(ref rlp_decoded_data)? + ), + TxType::Legacy => { + return Result::Err( + EthTransactionError::RLPError(RLPError::Custom('unexpected legacy tx type')) + ); + } + }; + + let tx_hash = Self::compute_hash(original_data); + Result::Ok(TransactionUnsigned { transaction, hash: tx_hash }) + } + + /// Returns the hash of the unsigned transaction + /// + /// The hash is used to recover the sender address when verifying the signature + /// attached to the transaction + #[inline(always)] + fn compute_hash(encoded_tx_data: Span) -> u256 { + encoded_tx_data.compute_keccak256_hash() + } + + /// Check if a raw transaction is a legacy Ethereum transaction + /// This function checks if a raw transaction is a legacy Ethereum transaction by checking the + /// transaction type according to EIP-2718. + /// # Arguments + /// * `encoded_tx_data` - The raw rlp encoded transaction data + #[inline(always)] + fn is_legacy_tx(encoded_tx_data: Span) -> bool { + // From EIP2718: if it starts with a value in the range [0xc0, 0xfe] then it is a legacy + // transaction type + if (*encoded_tx_data[0] > 0xbf && *encoded_tx_data[0] < 0xff) { + return true; + } + + return false; + } +} + +#[cfg(test)] +mod tests { + use crate::eth_transaction::common::TxKind; + use crate::eth_transaction::eip2930::AccessListItem; + use crate::eth_transaction::tx_type::TxType; + use crate::test_data::{ + legacy_rlp_encoded_tx, legacy_rlp_encoded_deploy_tx, eip_2930_encoded_tx, + eip_1559_encoded_tx + }; + use super::{TransactionTrait, TransactionUnsignedTrait}; + + + #[test] + fn test_decode_legacy_tx() { + // tx_format (EIP-155, unsigned): [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, + // 0] + // expected rlp decoding: [ "0x", "0x3b9aca00", "0x1e8480", + // "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "0x016345785d8a0000", "0xabcdef", + // "0x4b4b5254", "0x", "0x" ] + // message_hash: 0xcf71743e6e25fef715398915997f782b95554c8bbfb7b3f7701e007332ed31b4 + // chain id used: 'KKRT' + let mut encoded_tx_data = legacy_rlp_encoded_tx(); + let decoded = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data).unwrap(); + assert_eq!(decoded.transaction.nonce(), 0); + assert_eq!(decoded.transaction.max_fee_per_gas(), 0x3b9aca00); + assert_eq!(decoded.transaction.gas_limit(), 0x1e8480); + assert_eq!( + decoded.transaction.kind(), + TxKind::Call(0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.try_into().unwrap()) + ); + assert_eq!(decoded.transaction.value(), 0x016345785d8a0000); + assert_eq!(decoded.transaction.input(), [0xab, 0xcd, 0xef].span()); + assert_eq!(decoded.transaction.chain_id(), Option::Some(0x4b4b5254)); + assert_eq!(decoded.transaction.transaction_type(), TxType::Legacy); + } + + #[test] + fn test_decode_deploy_tx() { + // tx_format (EIP-155, unsigned): [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, + // 0] + // expected rlp decoding: + // ["0x","0x0a","0x061a80","0x","0x0186a0","0x600160010a5060006000f3","0x4b4b5254","0x","0x"] + let mut encoded_tx_data = legacy_rlp_encoded_deploy_tx(); + let decoded = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data).unwrap(); + assert_eq!(decoded.transaction.nonce(), 0); + assert_eq!(decoded.transaction.max_fee_per_gas(), 0x0a); + assert_eq!(decoded.transaction.gas_limit(), 0x061a80); + assert_eq!(decoded.transaction.kind(), TxKind::Create); + assert_eq!(decoded.transaction.value(), 0x0186a0); + assert_eq!( + decoded.transaction.input(), + [0x60, 0x01, 0x60, 0x01, 0x0a, 0x50, 0x60, 0x00, 0x60, 0x00, 0xf3].span() + ); + assert_eq!(decoded.transaction.chain_id(), Option::Some(0x4b4b5254)); + assert_eq!(decoded.transaction.transaction_type(), TxType::Legacy); + } + + #[test] + fn test_decode_eip2930_tx() { + // tx_format (EIP-2930, unsigned): 0x01 || rlp([chainId, nonce, gasPrice, gasLimit, to, + // value, data, accessList]) + // expected rlp decoding: [ "0x4b4b5254", "0x", "0x3b9aca00", "0x1e8480", + // "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "0x016345785d8a0000", "0xabcdef", + // [["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + // ["0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65", + // "0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94"]]] ] + // message_hash: 0xc00f61dcc99a78934275c404267b9d035cad7f71cf3ae2ed2c5a55b601a5c107 + // chain id used: 'KKRT' + + let mut encoded_tx_data = eip_2930_encoded_tx(); + let decoded = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data).unwrap(); + assert_eq!(decoded.transaction.chain_id(), Option::Some(0x4b4b5254)); + assert_eq!(decoded.transaction.nonce(), 0); + assert_eq!(decoded.transaction.max_fee_per_gas(), 0x3b9aca00); + assert_eq!(decoded.transaction.gas_limit(), 0x1e8480); + assert_eq!( + decoded.transaction.kind(), + TxKind::Call(0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.try_into().unwrap()) + ); + assert_eq!(decoded.transaction.value(), 0x016345785d8a0000); + assert_eq!(decoded.transaction.input(), [0xab, 0xcd, 0xef].span()); + assert_eq!(decoded.transaction.transaction_type(), TxType::Eip2930); + } + + #[test] + fn test_decode_eip1559_tx() { + // 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]) + // expected rlp decoding: [ "0x4b4b5254", "0x", "0x", "0x3b9aca00", "0x1e8480", + // "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "0x016345785d8a0000", "0xabcdef", + // [[["0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + // ["0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65", + // "0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94"]]] ] ] + // message_hash: 0xa2de478d0c94b4be637523b818d03b6a1841fca63fd044976fcdbef3c57a87b0 + // chain id used: 'KKRT' + + let mut encoded_tx_data = eip_1559_encoded_tx(); + let decoded = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data).unwrap(); + assert_eq!(decoded.transaction.chain_id(), Option::Some(0x4b4b5254)); + assert_eq!(decoded.transaction.nonce(), 0); + assert_eq!(decoded.transaction.max_fee_per_gas(), 0x3b9aca00); + assert_eq!(decoded.transaction.gas_limit(), 0x1e8480); + assert_eq!( + decoded.transaction.kind(), + TxKind::Call(0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.try_into().unwrap()) + ); + assert_eq!(decoded.transaction.value(), 0x016345785d8a0000); + assert_eq!(decoded.transaction.input(), [0xab, 0xcd, 0xef].span()); + let expected_access_list = [ + AccessListItem { + ethereum_address: 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984.try_into().unwrap(), + storage_keys: [ + 0xde9fbe35790b85c23f42b7430c78f122636750cc217a534c80a9a0520969fa65, + 0xd5362e94136f76bfc8dad0b510b94561af7a387f1a9d0d45e777c11962e5bd94 + ].span() + } + ].span(); + assert_eq!( + decoded.transaction.access_list().expect('access_list is none'), expected_access_list + ); + assert_eq!(decoded.transaction.transaction_type(), TxType::Eip1559); + } + + #[test] + fn test_is_legacy_tx_eip_155_tx() { + let encoded_tx_data = legacy_rlp_encoded_tx(); + let result = TransactionUnsignedTrait::is_legacy_tx(encoded_tx_data); + + assert(result, 'is_legacy_tx expected true'); + } + + #[test] + fn test_is_legacy_tx_eip_1559_tx() { + let encoded_tx_data = eip_1559_encoded_tx(); + let result = TransactionUnsignedTrait::is_legacy_tx(encoded_tx_data); + + assert(!result, 'is_legacy_tx expected false'); + } + + #[test] + fn test_is_legacy_tx_eip_2930_tx() { + let encoded_tx_data = eip_2930_encoded_tx(); + let result = TransactionUnsignedTrait::is_legacy_tx(encoded_tx_data); + + assert(!result, 'is_legacy_tx expected false'); + } +} diff --git a/crates/utils/src/eth_transaction/tx_type.cairo b/crates/utils/src/eth_transaction/tx_type.cairo new file mode 100644 index 000000000..3acaf3b72 --- /dev/null +++ b/crates/utils/src/eth_transaction/tx_type.cairo @@ -0,0 +1,23 @@ +/// Transaction Type +#[derive(Copy, Drop, Debug, PartialEq)] +pub enum TxType { + /// Legacy transaction pre EIP-2929 + #[default] + Legacy, + /// AccessList transaction + Eip2930, + /// Transaction with Priority fee + Eip1559, +} + + +impl _TryInto of TryInto { + fn try_into(self: u8) -> Option { + match self { + 0 => Option::Some(TxType::Legacy), + 1 => Option::Some(TxType::Eip2930), + 2 => Option::Some(TxType::Eip1559), + _ => Option::None, + } + } +} diff --git a/crates/utils/src/eth_transaction/validation.cairo b/crates/utils/src/eth_transaction/validation.cairo new file mode 100644 index 000000000..e38660a1c --- /dev/null +++ b/crates/utils/src/eth_transaction/validation.cairo @@ -0,0 +1,189 @@ +use core::starknet::eth_signature::verify_eth_signature; +use crate::eth_transaction::transaction::{TransactionUnsigned, TransactionTrait}; +use crate::eth_transaction::{TransactionMetadata, EthTransactionError}; + +/// Validate an Ethereum transaction +/// This function validates an Ethereum transaction by checking if the transaction +/// is correctly signed by the given address, and if the nonce in the transaction +/// matches the nonce of the account. +/// It decodes the transaction using the decode function, +/// and then verifies the Ethereum signature on the transaction hash. +/// # Arguments +/// * `tx_metadata` - The ethereum transaction metadata +/// * `encoded_tx_data` - The raw rlp encoded transaction data +pub fn validate_eth_tx( + tx_metadata: TransactionMetadata, unsigned_transaction: TransactionUnsigned +) -> Result { + let TransactionMetadata { address, account_nonce, chain_id, signature } = tx_metadata; + + if (unsigned_transaction.transaction.nonce() != account_nonce) { + return Result::Err(EthTransactionError::IncorrectAccountNonce); + } + //TODO(eip-155): support pre-eip155 transactions + let chain_id_from_tx = unsigned_transaction + .transaction + .chain_id() + .expect('Chain id should be set'); + if (chain_id_from_tx != chain_id) { + return Result::Err(EthTransactionError::IncorrectChainId); + } + //TODO: for max_fee = gas_price * gas_limit + //TODO: add checks for EIP-1559 transactions + + // this will panic if verification fails + verify_eth_signature(unsigned_transaction.hash, signature, address); + + Result::Ok(true) +} + + +#[cfg(test)] +mod tests { + use core::starknet::EthAddress; + use core::starknet::secp256_trait::{Signature}; + use crate::errors::EthTransactionError; + use crate::eth_transaction::transaction::TransactionUnsignedTrait; + use crate::eth_transaction::validation::validate_eth_tx; + use crate::eth_transaction::{TransactionMetadata}; + use crate::test_data::{legacy_rlp_encoded_tx, eip_2930_encoded_tx, eip_1559_encoded_tx}; + use evm::test_utils::chain_id; + + #[test] + fn test_validate_legacy_tx() { + let mut encoded_tx_data = legacy_rlp_encoded_tx(); + let address: EthAddress = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf_u256.into(); + let account_nonce = 0x0; + let chain_id = chain_id(); + + // to reproduce locally: + // run: cp .env.example .env + // bun install & bun run scripts/compute_rlp_encoding.ts + let signature = Signature { + r: 0x5e5202c7e9d6d0964a1f48eaecf12eef1c3cafb2379dfeca7cbd413cedd4f2c7, + s: 0x66da52d0b666fc2a35895e0c91bc47385fe3aa347c7c2a129ae2b7b06cb5498b, + y_parity: false + }; + + let validate_tx_param = TransactionMetadata { address, account_nonce, chain_id, signature }; + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data) + .expect('could not decode tx'); + + let result = validate_eth_tx(validate_tx_param, unsigned_transaction) + .expect('signature verification failed'); + assert(result, 'result is not true'); + } + + + #[test] + fn test_validate_eip_2930_tx() { + let mut encoded_tx_data = eip_2930_encoded_tx(); + let address: EthAddress = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf_u256.into(); + let account_nonce = 0x0; + let chain_id = chain_id(); + + // to reproduce locally: + // run: cp .env.example .env + // bun install & bun run scripts/compute_rlp_encoding.ts + let signature = Signature { + r: 0xbced8d81c36fe13c95b883b67898b47b4b70cae79e89fa27856ddf8c533886d1, + s: 0x3de0109f00bc3ed95ffec98edd55b6f750cb77be8e755935dbd6cfec59da7ad0, + y_parity: true + }; + + let validate_tx_param = TransactionMetadata { address, account_nonce, chain_id, signature }; + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data) + .expect('could not decode tx'); + + let maybe_result = validate_eth_tx(validate_tx_param, unsigned_transaction); + let result = match maybe_result { + Result::Ok(result) => result, + Result::Err(err) => panic!("decode failed: {:?}", err.into()), + }; + assert(result, 'result is not true'); + } + + + #[test] + fn test_validate_eip_1559_tx() { + let mut encoded_tx_data = eip_1559_encoded_tx(); + let address: EthAddress = 0xaA36F24f65b5F0f2c642323f3d089A3F0f2845Bf_u256.into(); + let account_nonce = 0x0; + let chain_id = chain_id(); + + // to reproduce locally: + // run: cp .env.example .env + // bun install & bun run scripts/compute_rlp_encoding.ts + let signature = Signature { + r: 0x0f9a716653c19fefc240d1da2c5759c50f844fc8835c82834ea3ab7755f789a0, + s: 0x71506d904c05c6e5ce729b5dd88bcf29db9461c8d72413b864923e8d8f6650c0, + y_parity: true + }; + + let validate_tx_param = TransactionMetadata { address, account_nonce, chain_id, signature }; + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data) + .expect('could not decode tx'); + + let maybe_result = validate_eth_tx(validate_tx_param, unsigned_transaction); + let result = match maybe_result { + Result::Ok(result) => result, + Result::Err(err) => panic!("decode failed: {:?}", err.into()), + }; + assert(result, 'result is not true'); + } + + + #[test] + fn test_validate_should_fail_for_wrong_account_id() { + let mut encoded_tx_data = eip_1559_encoded_tx(); + let address: EthAddress = 0x6Bd85F39321B00c6d603474C5B2fddEB9c92A466_u256.into(); + // the tx was signed for nonce 0x0 + let wrong_account_nonce = 0x1; + let chain_id = 0x1; + + // to reproduce locally: + // run: cp .env.example .env + // bun install & bun run scripts/compute_rlp_encoding.ts + let signature = Signature { + r: 0x141615694556f9078d9da3249e8aa1987524f57153121599cf36d7681b809858, + s: 0x052052478f912dbe80339e3f198be8c9e1cd44eaabb295d912087d975ef38192, + y_parity: false + }; + + let validate_tx_param = TransactionMetadata { + address, account_nonce: wrong_account_nonce, chain_id, signature + }; + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data) + .expect('could not decode tx'); + + let error = validate_eth_tx(validate_tx_param, unsigned_transaction).unwrap_err(); + assert_eq!(error, EthTransactionError::IncorrectAccountNonce); + } + + + #[test] + fn test_validate_should_fail_for_wrong_chain_id() { + let mut encoded_tx_data = eip_1559_encoded_tx(); + let address: EthAddress = 0x6Bd85F39321B00c6d603474C5B2fddEB9c92A466_u256.into(); + let account_nonce = 0x0; + // the tx was signed for chain_id 0x1 + let wrong_chain_id = 0x2; + + // to reproduce locally: + // run: cp .env.example .env + // bun install & bun run scripts/compute_rlp_encoding.ts + let signature = Signature { + r: 0x141615694556f9078d9da3249e8aa1987524f57153121599cf36d7681b809858, + s: 0x052052478f912dbe80339e3f198be8c9e1cd44eaabb295d912087d975ef38192, + y_parity: false + }; + + let validate_tx_param = TransactionMetadata { + address, account_nonce, chain_id: wrong_chain_id, signature + }; + let unsigned_transaction = TransactionUnsignedTrait::decode_enveloped(ref encoded_tx_data) + .expect('could not decode tx'); + + let error = validate_eth_tx(validate_tx_param, unsigned_transaction).unwrap_err(); + assert_eq!(error, EthTransactionError::IncorrectChainId); + } +} diff --git a/crates/utils/src/rlp.cairo b/crates/utils/src/rlp.cairo index cc79e70ca..caccfb64e 100644 --- a/crates/utils/src/rlp.cairo +++ b/crates/utils/src/rlp.cairo @@ -3,18 +3,18 @@ use core::array::SpanTrait; use core::option::OptionTrait; use core::panic_with_felt252; use core::starknet::EthAddress; -use utils::errors::{RLPError, RLPHelpersError}; -use utils::eth_transaction::AccessListItem; -use utils::helpers::{EthAddressExTrait, ArrayExtension, ToBytes, FromBytes}; +use crate::errors::{RLPError, RLPHelpersError}; +use crate::eth_transaction::eip2930::AccessListItem; +use crate::helpers::{EthAddressExTrait, ArrayExtension, ToBytes, FromBytes}; // Possible RLP types -#[derive(Drop, PartialEq)] +#[derive(Drop, PartialEq, Debug)] pub enum RLPType { String, List } -#[derive(Drop, Copy, PartialEq)] +#[derive(Drop, Copy, PartialEq, Debug)] pub enum RLPItem { String: Span, List: Span @@ -202,6 +202,20 @@ pub impl RLPImpl of RLPTrait { #[generate_trait] pub impl RLPHelpersImpl of RLPHelpersTrait { + fn parse_u64_from_string(self: RLPItem) -> Result { + match self { + RLPItem::String(bytes) => { + // Empty strings means 0 + if bytes.len() == 0 { + return Result::Ok(0); + } + let value = bytes.from_be_bytes_partial().expect('parse_u64_from_string'); + Result::Ok(value) + }, + RLPItem::List(_) => { Result::Err(RLPHelpersError::NotAString) } + } + } + fn parse_u128_from_string(self: RLPItem) -> Result { match self { RLPItem::String(bytes) => { @@ -337,8 +351,8 @@ mod tests { use core::option::OptionTrait; use core::result::ResultTrait; + use crate::eth_transaction::eip2930::AccessListItem; use utils::errors::RLPError; - use utils::eth_transaction::AccessListItem; use utils::rlp::{RLPType, RLPTrait, RLPItem, RLPHelpersTrait}; // Tests source : diff --git a/crates/utils/src/serialization.cairo b/crates/utils/src/serialization.cairo index 564153220..c29b2f9f2 100644 --- a/crates/utils/src/serialization.cairo +++ b/crates/utils/src/serialization.cairo @@ -1,8 +1,8 @@ use core::starknet::secp256_trait::{Signature}; -use utils::eth_transaction::TransactionType; +use utils::eth_transaction::tx_type::TxType; use utils::traits::BoolIntoNumeric; -pub fn deserialize_signature(signature: Span, chain_id: u128) -> Option { +pub fn deserialize_signature(signature: Span, chain_id: u64) -> Option { let r_low: u128 = (*signature.at(0)).try_into()?; let r_high: u128 = (*signature.at(1)).try_into()?; @@ -24,8 +24,8 @@ pub fn deserialize_signature(signature: Span, chain_id: u128) -> Option ) } -fn compute_y_parity(v: u128, chain_id: u128) -> Option { - let y_parity = v - (chain_id * 2 + 35); +fn compute_y_parity(v: u128, chain_id: u64) -> Option { + let y_parity = v - (chain_id.into() * 2 + 35); if (y_parity == 0 || y_parity == 1) { return Option::Some(y_parity == 1); } @@ -34,16 +34,15 @@ fn compute_y_parity(v: u128, chain_id: u128) -> Option { } pub fn serialize_transaction_signature( - sig: Signature, tx_type: TransactionType, chain_id: u128 + sig: Signature, tx_type: TxType, chain_id: u64 ) -> Array { let mut res: Array = array![ sig.r.low.into(), sig.r.high.into(), sig.s.low.into(), sig.s.high.into() ]; let value = match tx_type { - TransactionType::Legacy => { sig.y_parity.into() + 2 * chain_id + 35 }, - TransactionType::EIP2930 => { sig.y_parity.into() }, - TransactionType::EIP1559 => { sig.y_parity.into() } + TxType::Legacy(_) => { sig.y_parity.into() + 2 * chain_id + 35 }, + TxType::Eip2930(_) | TxType::Eip1559(_) => { sig.y_parity.into() } }; res.append(value.into()); @@ -92,7 +91,7 @@ pub fn serialize_bytes(self: Span) -> Array { mod tests { use core::starknet::secp256_trait::Signature; use utils::constants::CHAIN_ID; - use utils::eth_transaction::TransactionType; + use utils::eth_transaction::tx_type::TxType; use utils::serialization::{deserialize_signature, serialize_transaction_signature}; #[test] @@ -153,20 +152,13 @@ mod tests { 0x0_felt252, ].span(); - let result = serialize_transaction_signature(signature_0, TransactionType::Legacy, CHAIN_ID) - .span(); + let result = serialize_transaction_signature(signature_0, TxType::Legacy, CHAIN_ID).span(); assert_eq!(result, expected_signature_0); - let result = serialize_transaction_signature( - signature_1, TransactionType::EIP2930, CHAIN_ID - ) - .span(); + let result = serialize_transaction_signature(signature_1, TxType::Eip2930, CHAIN_ID).span(); assert_eq!(result, expected_signature_1); - let result = serialize_transaction_signature( - signature_2, TransactionType::EIP1559, CHAIN_ID - ) - .span(); + let result = serialize_transaction_signature(signature_2, TxType::Eip1559, CHAIN_ID).span(); assert_eq!(result, expected_signature_2); } diff --git a/crates/utils/src/traits.cairo b/crates/utils/src/traits.cairo index 5da27b18b..202a9f0d8 100644 --- a/crates/utils/src/traits.cairo +++ b/crates/utils/src/traits.cairo @@ -1,10 +1,18 @@ use core::array::SpanTrait; use core::num::traits::{Zero, One}; +use core::starknet::secp256_trait::{Signature}; use core::starknet::storage_access::{StorageBaseAddress, storage_address_from_base}; use core::starknet::{EthAddress, ContractAddress}; use evm::errors::{EVMError, ensure, TYPE_CONVERSION_ERROR}; use utils::math::{Bitshift}; +pub impl DefaultSignature of Default { + #[inline(always)] + fn default() -> Signature { + Signature { r: 0, s: 0, y_parity: false, } + } +} + pub impl SpanDefault> of Default> { #[inline(always)] fn default() -> Span {