From b77f88cc0f74978393e8adbde39923d476728fbb Mon Sep 17 00:00:00 2001 From: Mathieu <60658558+enitrat@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:37:02 +0700 Subject: [PATCH] feat: ContractAccount storage methods (#438) * feat: CA storage methods * test: CA storage methods * dev: inlining for perf * dev: remove deploy_contract_account(out of scope) * feat: map_err \n ContractAccount return Result \n ByteArraySerde testing \n rename Byte31Serde to U32Trait::from_bytes * fix: finalize_global returns Result<(), EVMError> * refactor: byte array serde --- crates/contracts/src/contract_account.cairo | 248 +++++++++++++++--- crates/contracts/src/kakarot_core.cairo | 2 +- .../src/kakarot_core/interface.cairo | 30 ++- .../contracts/src/kakarot_core/kakarot.cairo | 59 +++-- .../src/tests/test_contract_account.cairo | 106 +++++++- .../src/tests/test_kakarot_core.cairo | 3 + crates/evm/src/balance.cairo | 17 +- crates/evm/src/errors.cairo | 1 + .../src/instructions/block_information.cairo | 2 +- .../environmental_information.cairo | 4 +- crates/evm/src/storage_journal.cairo | 21 +- crates/evm/src/tests/test_balance.cairo | 4 +- crates/utils/src/helpers.cairo | 28 +- crates/utils/src/rlp.cairo | 6 +- crates/utils/src/tests/test_helpers.cairo | 57 +++- crates/utils/src/traits.cairo | 33 +++ 16 files changed, 517 insertions(+), 104 deletions(-) diff --git a/crates/contracts/src/contract_account.cairo b/crates/contracts/src/contract_account.cairo index 768af51f4..f8ca028ae 100644 --- a/crates/contracts/src/contract_account.cairo +++ b/crates/contracts/src/contract_account.cairo @@ -1,6 +1,9 @@ -//! Contract Account related functions, including bytecode storage +//! Contract Account related functions to interact with the storage of a +//! contract account. The storage of a contract account is embedded in +//! KakarotCore's storage. use alexandria_storage::list::{List, ListTrait}; +use evm::errors::{EVMError, READ_SYSCALL_FAILED, WRITE_SYSCALL_FAILED}; use hash::{HashStateTrait, HashStateExTrait}; use poseidon::PoseidonTrait; use starknet::{ @@ -8,41 +11,216 @@ use starknet::{ storage_write_syscall, storage_address_from_base, storage_read_syscall, storage_address_from_base_and_offset }; -use utils::helpers::{ByteArrayExTrait}; +use utils::helpers::{ByteArrayExTrait, ResultExTrait}; use utils::storage::{compute_storage_base_address}; use utils::traits::{StorageBaseAddressIntoFelt252, StoreBytes31}; -/// Stores the EVM bytecode of a contract account in Kakarot Core's contract storage. The bytecode is first packed -/// into a ByteArray and then stored in the contract storage. -/// # Arguments -/// * `evm_address` - The EVM address of the contract account -/// * `bytecode` - The bytecode to store -fn store_bytecode(evm_address: EthAddress, bytecode: Span) { - let packed_bytecode: ByteArray = ByteArrayExTrait::from_bytes(bytecode); - // data_address is h(h(sn_keccak("contract_account_bytecode")), evm_address) - let data_address = compute_storage_base_address( - selector!("contract_account_bytecode"), array![evm_address.into()].span() - ); - // We start storing the full 31-byte words of bytecode data at address - // `data_address`. The `pending_word` and `pending_word_len` are stored at - // address `data_address-2` and `data_address-1` respectively. - //TODO(eni) replace with ListTrait::new() once merged in alexandria - let mut stored_list: List = List { - address_domain: 0, base: data_address, len: 0, storage_size: Store::::size() - }; - let pending_word_addr: felt252 = data_address.into() - 2; - let pending_word_len_addr: felt252 = pending_word_addr + 1; - - // Store the `ByteArray` in the contract storage. - Store::< - felt252 - >::write(0, storage_base_address_from_felt252(pending_word_addr), packed_bytecode.pending_word); - Store::< - usize - >::write( - 0, - storage_base_address_from_felt252(pending_word_len_addr), - packed_bytecode.pending_word_len - ); - stored_list.from_span(packed_bytecode.data.span()); +/// Wrapper struct around an evm_address corresponding to a ContractAccount +#[derive(Copy, Drop)] +struct ContractAccount { + evm_address: EthAddress, } + +#[generate_trait] +impl ContractAccountImpl of ContractAccountTrait { + /// Creates a new ContractAccount instance from the given `evm_address`. + fn new(evm_address: EthAddress) -> ContractAccount { + ContractAccount { evm_address: evm_address, } + } + + /// Returns the nonce of a contract account. + /// # Arguments + /// * `self` - The contract account instance + #[inline(always)] + fn nonce(self: @ContractAccount) -> Result { + let storage_address = compute_storage_base_address( + selector!("contract_account_nonce"), array![(*self.evm_address).into()].span() + ); + Store::::read(0, storage_address).map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED)) + } + + /// Increments the nonce of a contract account. + /// The new nonce is written in Kakarot Core's contract storage. + /// The storage address used is h(sn_keccak("contract_account_nonce"), evm_address), where `h` is the poseidon hash function. + /// # Arguments + /// * `self` - The contract account instance + #[inline(always)] + fn increment_nonce(ref self: ContractAccount) -> Result<(), EVMError> { + let storage_address = compute_storage_base_address( + selector!("contract_account_nonce"), array![self.evm_address.into()].span() + ); + let nonce: u64 = Store::::read(0, storage_address) + .map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED))?; + Store::::write(0, storage_address, nonce + 1) + .map_err(EVMError::SyscallFailed(WRITE_SYSCALL_FAILED)) + } + + /// Returns the balance of a contract account. + /// * `self` - The contract account instance + #[inline(always)] + fn balance(self: @ContractAccount) -> Result { + let storage_address = compute_storage_base_address( + selector!("contract_account_balance"), array![(*self.evm_address).into()].span() + ); + Store::::read(0, storage_address) + .map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED)) + } + + /// Sets the balance of a contract account. + /// The new balance is written in Kakarot Core's contract storage. + /// The storage address used is h(sn_keccak("contract_account_balance"), evm_address), where `h` is the poseidon hash function. + /// # Arguments + /// * `self` - The contract account instance + /// * `balance` - The new balance + #[inline(always)] + fn set_balance(ref self: ContractAccount, balance: u256) -> Result<(), EVMError> { + let storage_address = compute_storage_base_address( + selector!("contract_account_balance"), array![self.evm_address.into()].span() + ); + Store::::write(0, storage_address, balance) + .map_err(EVMError::SyscallFailed(WRITE_SYSCALL_FAILED)) + } + + /// Returns the value stored at a `u256` key inside the Contract Account storage. + /// The new value is written in Kakarot Core's contract storage. + /// The storage address used is h(sn_keccak("contract_account_storage_keys"), evm_address, key), where `h` is the poseidon hash function. + #[inline(always)] + fn storage_at(self: @ContractAccount, key: u256) -> Result { + let storage_address = compute_storage_base_address( + selector!("contract_account_storage_keys"), + array![(*self.evm_address).into(), key.low.into(), key.high.into()].span() + ); + Store::::read(0, storage_address) + .map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED)) + } + + /// Sets the value stored at a `u256` key inside the Contract Account storage. + /// The new value is written in Kakarot Core's contract storage. + /// The storage address used is h(sn_keccak("contract_account_storage_keys"), evm_address, key), where `h` is the poseidon hash function. + /// # Arguments + /// * `self` - The contract account instance + /// * `key` - The key to set + /// * `value` - The value to set + #[inline(always)] + fn set_storage_at(ref self: ContractAccount, key: u256, value: u256) -> Result<(), EVMError> { + let storage_address = compute_storage_base_address( + selector!("contract_account_storage_keys"), + array![self.evm_address.into(), key.low.into(), key.high.into()].span() + ); + Store::::write(0, storage_address, value) + .map_err(EVMError::SyscallFailed(WRITE_SYSCALL_FAILED)) + } + + /// Stores the EVM bytecode of a contract account in Kakarot Core's contract storage. The bytecode is first packed + /// into a ByteArray and then stored in the contract storage. + /// # Arguments + /// * `evm_address` - The EVM address of the contract account + /// * `bytecode` - The bytecode to store + fn store_bytecode(ref self: ContractAccount, bytecode: Span) -> Result<(), EVMError> { + let packed_bytecode: ByteArray = ByteArrayExTrait::from_bytes(bytecode); + // data_address is h(h(sn_keccak("contract_account_bytecode")), evm_address) + let data_address = compute_storage_base_address( + selector!("contract_account_bytecode"), array![self.evm_address.into()].span() + ); + // We start storing the full 31-byte words of bytecode data at address + // `data_address`. The `pending_word` and `pending_word_len` are stored at + // address `data_address-2` and `data_address-1` respectively. + //TODO(eni) replace with ListTrait::new() once merged in alexandria + let mut stored_list: List = List { + address_domain: 0, base: data_address, len: 0, storage_size: Store::::size() + }; + let pending_word_addr: felt252 = data_address.into() - 2; + let pending_word_len_addr: felt252 = pending_word_addr + 1; + + // Store the `ByteArray` in the contract storage. + Store::< + felt252 + >::write( + 0, storage_base_address_from_felt252(pending_word_addr), packed_bytecode.pending_word + ) + .map_err(EVMError::SyscallFailed(WRITE_SYSCALL_FAILED))?; + Store::< + usize + >::write( + 0, + storage_base_address_from_felt252(pending_word_len_addr), + packed_bytecode.pending_word_len + ) + .map_err(EVMError::SyscallFailed(WRITE_SYSCALL_FAILED))?; + //TODO(eni) PR Alexandria so that from_span returns SyscallResult + stored_list.from_span(packed_bytecode.data.span()); + Result::Ok(()) + } + + /// Loads the bytecode of a ContractAccount from Kakarot Core's contract storage into a ByteArray. + /// # Arguments + /// * `self` - The Contract Account to load the bytecode from + /// # Returns + /// * The bytecode of the Contract Account as a ByteArray + fn load_bytecode(self: @ContractAccount) -> Result { + let data_address = compute_storage_base_address( + selector!("contract_account_bytecode"), array![(*self.evm_address).into()].span() + ); + // We start loading the full 31-byte words of bytecode data at address + // `data_address`. The `pending_word` and `pending_word_len` are stored at + // address `data_address-2` and `data_address-1` respectively. + //TODO(eni) replace with ListTrait::new() once merged in alexandria + let list_len = Store::::read(0, data_address) + .map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED))?; + let mut stored_list: List = List { + address_domain: 0, + base: data_address, + len: list_len, + storage_size: Store::::size() + }; + let pending_word_addr: felt252 = data_address.into() - 2; + let pending_word_len_addr: felt252 = pending_word_addr + 1; + + // Read the `ByteArray` in the contract storage. + let bytecode = ByteArray { + //TODO(eni) PR alexandria to make List methods return SyscallResult + data: stored_list.array(), + pending_word: Store::< + felt252 + >::read(0, storage_base_address_from_felt252(pending_word_addr)) + .map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED))?, + pending_word_len: Store::< + usize + >::read(0, storage_base_address_from_felt252(pending_word_len_addr)) + .map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED))? + }; + Result::Ok(bytecode) + } + + /// Returns true if the given `offset` is a valid jump destination in the bytecode. + /// The valid jump destinations are stored in Kakarot Core's contract storage first. + /// # Arguments + /// * `offset` - The offset to check + /// # Returns + /// * `true` - If the offset is a valid jump destination + /// * `false` - Otherwise + #[inline(always)] + fn is_valid_jump(self: @ContractAccount, offset: usize) -> Result { + let data_address = compute_storage_base_address( + selector!("contract_account_valid_jumps"), + array![(*self.evm_address).into(), offset.into()].span() + ); + Store::::read(0, data_address).map_err(EVMError::SyscallFailed(READ_SYSCALL_FAILED)) + } + + /// Sets the given `offset` as a valid jump destination in the bytecode. + /// The valid jump destinations are stored in Kakarot Core's contract storage. + /// # Arguments + /// * `self` - The ContractAccount + /// * `offset` - The offset to set as a valid jump destination + #[inline(always)] + fn set_valid_jump(ref self: ContractAccount, offset: usize) -> Result<(), EVMError> { + let data_address = compute_storage_base_address( + selector!("contract_account_valid_jumps"), + array![self.evm_address.into(), offset.into()].span() + ); + Store::::write(0, data_address, true) + .map_err(EVMError::SyscallFailed(WRITE_SYSCALL_FAILED)) + } +} + diff --git a/crates/contracts/src/kakarot_core.cairo b/crates/contracts/src/kakarot_core.cairo index 875c99008..1b2d35209 100644 --- a/crates/contracts/src/kakarot_core.cairo +++ b/crates/contracts/src/kakarot_core.cairo @@ -1,4 +1,4 @@ mod interface; mod kakarot; -use kakarot::{KakarotCore, ContractAccountStorage}; +use kakarot::KakarotCore; diff --git a/crates/contracts/src/kakarot_core/interface.cairo b/crates/contracts/src/kakarot_core/interface.cairo index 1975891d1..c61aa5e93 100644 --- a/crates/contracts/src/kakarot_core/interface.cairo +++ b/crates/contracts/src/kakarot_core/interface.cairo @@ -1,5 +1,5 @@ -use contracts::kakarot_core::ContractAccountStorage; use starknet::{ContractAddress, EthAddress, ClassHash}; +use utils::traits::ByteArraySerde; #[starknet::interface] trait IKakarotCore { @@ -29,10 +29,24 @@ trait IKakarotCore { /// Otherwise, returns 0 fn eoa_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress; - /// Gets the storage associated to a contract account - fn contract_account_storage( - self: @TContractState, evm_address: EthAddress - ) -> ContractAccountStorage; + /// Gets the nonce associated to a contract account + fn contract_account_nonce(self: @TContractState, evm_address: EthAddress) -> u64; + + /// Gets the balance associated to a contract account + fn contract_account_balance(self: @TContractState, evm_address: EthAddress) -> u256; + + /// Gets the value associated to a key in the contract account storage + fn contract_account_storage_at( + self: @TContractState, evm_address: EthAddress, key: u256 + ) -> u256; + + /// Gets the bytecode associated to a contract account + fn contract_account_bytecode(self: @TContractState, evm_address: EthAddress) -> ByteArray; + + /// Returns true if the given `offset` is a valid jump destination in the bytecode of a contract account. + fn contract_account_valid_jump( + self: @TContractState, evm_address: EthAddress, offset: usize + ) -> bool; /// Deploys an EOA for a particular EVM address fn deploy_eoa(ref self: TContractState, evm_address: EthAddress) -> ContractAddress; @@ -93,12 +107,6 @@ trait IExtendedKakarotCore { /// particular EVM address and if so, returns its corresponding Starknet Address fn eoa_starknet_address(self: @TContractState, evm_address: EthAddress) -> ContractAddress; - - /// Gets the storage associated to a contract account - fn contract_account_storage( - self: @TContractState, evm_address: EthAddress - ) -> ContractAccountStorage; - /// Deploys an EOA for a particular EVM address fn deploy_eoa(ref 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 0b16850bd..518d766b2 100644 --- a/crates/contracts/src/kakarot_core/kakarot.cairo +++ b/crates/contracts/src/kakarot_core/kakarot.cairo @@ -3,23 +3,12 @@ use starknet::{ContractAddress, EthAddress, ClassHash}; const INVOKE_ETH_CALL_FORBIDDEN: felt252 = 'KKT: Cannot invoke eth_call'; -#[derive(Copy, Drop, Serde, starknet::Store)] -struct ContractAccountStorage { - nonce: u64, - balance: u256, -// TODO: add bytecode as a field for ContractAccountStorage -// bytecode: List - -//TODO: add valid jumps as a field for ContractAccountStorage -// valid_jumps: LegacyMap -} - - #[starknet::contract] mod KakarotCore { use contracts::components::ownable::ownable_component::InternalTrait; use contracts::components::ownable::{ownable_component}; use contracts::components::upgradeable::{IUpgradeable, upgradeable_component}; + use contracts::contract_account::{ContractAccount, ContractAccountTrait}; use contracts::kakarot_core::interface::IKakarotCore; use contracts::kakarot_core::interface; use core::hash::{HashStateExTrait, HashStateTrait}; @@ -32,10 +21,9 @@ mod KakarotCore { use starknet::{ EthAddress, ContractAddress, ClassHash, get_tx_info, get_contract_address, deploy_syscall }; - use super::ContractAccountStorage; use super::INVOKE_ETH_CALL_FORBIDDEN; use utils::constants::{CONTRACT_ADDRESS_PREFIX, MAX_ADDRESS}; - use utils::traits::U256TryIntoContractAddress; + use utils::traits::{U256TryIntoContractAddress, ByteArraySerde}; component!(path: ownable_component, storage: ownable, event: OwnableEvent); component!(path: upgradeable_component, storage: upgradeable, event: UpgradeableEvent); @@ -50,8 +38,8 @@ mod KakarotCore { #[storage] struct Storage { /// Kakarot storage for accounts: Externally Owned Accounts (EOA) and Contract Accounts (CA) - /// CAs: - /// Map EVM address of a CA and the corresponding Kakarot Core storage -> + /// CAs storage is handled outside of the Storage struct (see contract_account.cairo) + /// It maps the EVM address of a CA and the corresponding Kakarot Core storage -> /// - nonce (note that this nonce is not the same as the Starknet protocol nonce) /// - current balance in native token (CAs can use this balance as an allowance to spend native Starknet token through Kakarot Core) /// - bytecode of the CA @@ -60,7 +48,6 @@ mod KakarotCore { /// EOAs: /// Map their EVM address and their Starknet address /// - starknet_address: the deterministic starknet address (31 bytes) computed given an EVM address (20 bytes) - contract_account_storage: LegacyMap::, eoa_address_registry: LegacyMap::, eoa_class_hash: ClassHash, // Utility storage @@ -179,11 +166,39 @@ mod KakarotCore { self.eoa_address_registry.read(evm_address) } - /// Gets the storage associated to a contract account - fn contract_account_storage( - self: @ContractState, evm_address: EthAddress - ) -> ContractAccountStorage { - self.contract_account_storage.read(evm_address) + /// Gets the nonce associated to a contract account + fn contract_account_nonce(self: @ContractState, evm_address: EthAddress) -> u64 { + let ca = ContractAccountTrait::new(evm_address); + ca.nonce().unwrap() + } + + /// Gets the balance associated to a contract account + fn contract_account_balance(self: @ContractState, evm_address: EthAddress) -> u256 { + let ca = ContractAccountTrait::new(evm_address); + ca.balance().unwrap() + } + + /// Gets the value associated to a key in the contract account storage + fn contract_account_storage_at( + self: @ContractState, evm_address: EthAddress, key: u256 + ) -> u256 { + let ca = ContractAccountTrait::new(evm_address); + ca.storage_at(key).unwrap() + } + + + /// Gets the bytecode associated to a contract account + fn contract_account_bytecode(self: @ContractState, evm_address: EthAddress) -> ByteArray { + let mut ca = ContractAccountTrait::new(evm_address); + ca.load_bytecode().unwrap() + } + + /// Returns true if the given `offset` is a valid jump destination in the bytecode of a contract account. + fn contract_account_valid_jump( + self: @ContractState, evm_address: EthAddress, offset: usize + ) -> bool { + let ca = ContractAccountTrait::new(evm_address); + ca.is_valid_jump(offset).unwrap() } /// Deploys an EOA for a particular EVM address diff --git a/crates/contracts/src/tests/test_contract_account.cairo b/crates/contracts/src/tests/test_contract_account.cairo index 01ea4c2df..67ed20538 100644 --- a/crates/contracts/src/tests/test_contract_account.cairo +++ b/crates/contracts/src/tests/test_contract_account.cairo @@ -1,17 +1,51 @@ use alexandria_storage::list::{List, ListTrait}; -use contracts::contract_account::{store_bytecode}; +use contracts::contract_account::{ContractAccount, ContractAccountTrait}; use contracts::tests::utils::constants::EVM_ADDRESS; use starknet::{storage_base_address_from_felt252, Store}; use utils::storage::{compute_storage_base_address}; use utils::traits::{StoreBytes31, StorageBaseAddressIntoFelt252}; +#[test] +#[available_gas(2000000)] +fn test_nonce() { + let evm_address = EVM_ADDRESS(); + let mut ca = ContractAccountTrait::new(evm_address); + assert(ca.nonce().unwrap() == 0, 'initial nonce not 0'); + ca.increment_nonce(); + assert(ca.nonce().unwrap() == 1, 'nonce not incremented'); +} + +#[test] +#[available_gas(2000000)] +fn test_balance() { + let evm_address = EVM_ADDRESS(); + let mut ca = ContractAccountTrait::new(evm_address); + assert(ca.balance().unwrap() == 0, 'initial balance not 0'); + ca.set_balance(1); + assert(ca.balance().unwrap() == 1, 'balance not incremented'); +} + +#[test] +#[available_gas(20000000)] +fn test_contract_storage() { + let evm_address = EVM_ADDRESS(); + let mut ca = ContractAccountTrait::new(evm_address); + let key = u256 { low: 10, high: 10 }; + assert(ca.storage_at(key).unwrap() == 0, 'initial key not null'); + let value = u256 { low: 0, high: 1 }; + ca.set_storage_at(key, value); + let value_read = ca.storage_at(key).unwrap(); + assert(value_read == value, 'value not read correctly'); +} + #[test] #[available_gas(20000000)] fn test_store_bytecode_word_not_full() { let byte_array: Array = array![0x01, 0x02, 0x03, // 3 elements ]; let evm_address = EVM_ADDRESS(); - store_bytecode(evm_address, byte_array.span()); + let mut ca = ContractAccountTrait::new(evm_address); + ca.store_bytecode(byte_array.span()); // Address at which the bytecode should be stored let data_addr = compute_storage_base_address( @@ -70,7 +104,8 @@ fn test_store_bytecode_one_word() { 0x1F, // 31 elements ]; let evm_address = EVM_ADDRESS(); - store_bytecode(evm_address, byte_array.span()); + let mut ca = ContractAccountTrait::new(evm_address); + ca.store_bytecode(byte_array.span()); // Address at which the bytecode should be stored let data_addr = compute_storage_base_address( @@ -137,7 +172,8 @@ fn test_store_bytecode_one_word_pending() { 0x21 // 33 elements ]; let evm_address = EVM_ADDRESS(); - store_bytecode(evm_address, byte_array.span()); + let mut ca = ContractAccountTrait::new(evm_address); + ca.store_bytecode(byte_array.span()); // Address at which the bytecode should be stored let data_addr = compute_storage_base_address( @@ -164,6 +200,68 @@ fn test_store_bytecode_one_word_pending() { i += 1; } } + +#[test] +#[available_gas(20000000)] +fn test_load_bytecode() { + let byte_array: Array = array![ + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + 0x09, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x10, + 0x11, + 0x12, + 0x13, + 0x14, + 0x15, + 0x16, + 0x17, + 0x18, + 0x19, + 0x1A, + 0x1B, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0x20, + 0x21 // 33 elements + ]; + let evm_address = EVM_ADDRESS(); + let mut ca = ContractAccountTrait::new(evm_address); + ca.store_bytecode(byte_array.span()); + let bytecode = ca.load_bytecode().unwrap(); + let mut i: u32 = 0; + loop { + if i == byte_array.len() { + break; + } + assert(bytecode[i] == *byte_array[i], 'loaded bytecode error'); + i += 1; + } +} + +#[test] +#[available_gas(2000000)] +fn test_valid_jumps() { + let evm_address = EVM_ADDRESS(); + let mut ca = ContractAccountTrait::new(evm_address); + assert(!ca.is_valid_jump(10).unwrap(), 'should default false'); + ca.set_valid_jump(10); + assert(ca.is_valid_jump(10).unwrap(), 'should be true') +} //TODO add a test with huge amount of bytecode - using SNFoundry and loading data from txt diff --git a/crates/contracts/src/tests/test_kakarot_core.cairo b/crates/contracts/src/tests/test_kakarot_core.cairo index 310456685..ceb720b6f 100644 --- a/crates/contracts/src/tests/test_kakarot_core.cairo +++ b/crates/contracts/src/tests/test_kakarot_core.cairo @@ -115,3 +115,6 @@ fn test_kakarot_core_upgrade_contract() { .version(); assert(version == 1, 'version is not 1'); } +// TODO add tests related to contract accounts once they can be deployed. + + diff --git a/crates/evm/src/balance.cairo b/crates/evm/src/balance.cairo index e10ea376d..2c75034d6 100644 --- a/crates/evm/src/balance.cairo +++ b/crates/evm/src/balance.cairo @@ -1,12 +1,14 @@ +use contracts::contract_account::{ContractAccount, ContractAccountTrait}; use contracts::kakarot_core::interface::{IKakarotCore}; -use contracts::kakarot_core::{ContractAccountStorage, KakarotCore}; +use contracts::kakarot_core::{KakarotCore}; +use evm::errors::EVMError; use openzeppelin::token::erc20::interface::{IERC20CamelDispatcher, IERC20CamelDispatcherTrait}; use starknet::EthAddress; /// Returns the balance in native token for a given EVM account (EOA or CA) /// This is equivalent to checking the balance in native coin, i.e. ETHER of an account in Ethereum -fn balance(evm_address: EthAddress) -> u256 { +fn balance(evm_address: EthAddress) -> Result { // Get access to Kakarot State locally let kakarot_state = KakarotCore::unsafe_new_contract_state(); @@ -20,19 +22,20 @@ fn balance(evm_address: EthAddress) -> u256 { // As native_token might become a snake_case implementation // instead of camelCase let native_token = IERC20CamelDispatcher { contract_address: native_token_address }; - return native_token.balanceOf(eoa_starknet_address); + return Result::Ok(native_token.balanceOf(eoa_starknet_address)); } // Case 2: EOA is not deployed and CA is deployed // We check if a contract account is initialized at evm_address // A good condition to check is nonce > 0, as deploying a contract account // will set its nonce to 1 - let ca_storage = kakarot_state.contract_account_storage(evm_address); - if ca_storage.nonce != 0 { - return ca_storage.balance; + let ca = ContractAccountTrait::new(evm_address); + let nonce = ca.nonce()?; + if nonce != 0 { + return ca.balance(); } // Case 3: No EOA nor CA are deployed at `evm_address` // Return 0 - 0 + Result::Ok(0) } diff --git a/crates/evm/src/errors.cairo b/crates/evm/src/errors.cairo index 6cbf69fc3..a6cecf9c6 100644 --- a/crates/evm/src/errors.cairo +++ b/crates/evm/src/errors.cairo @@ -19,6 +19,7 @@ const WRITE_IN_STATIC_CONTEXT: felt252 = 'KKT: WriteInStaticContext'; // STARKNET_SYSCALLS const READ_SYSCALL_FAILED: felt252 = 'KKT: read syscall failed'; +const WRITE_SYSCALL_FAILED: felt252 = 'KKT: write syscall failed'; #[derive(Drop, Copy, PartialEq)] enum EVMError { diff --git a/crates/evm/src/instructions/block_information.cairo b/crates/evm/src/instructions/block_information.cairo index 31a53326a..918183c2b 100644 --- a/crates/evm/src/instructions/block_information.cairo +++ b/crates/evm/src/instructions/block_information.cairo @@ -68,7 +68,7 @@ impl BlockInformation of BlockInformationTrait { fn exec_selfbalance(ref self: Machine) -> Result<(), EVMError> { let evm_address = self.evm_address(); - let balance = balance(evm_address); + let balance = balance(evm_address)?; self.stack.push(balance) } diff --git a/crates/evm/src/instructions/environmental_information.cairo b/crates/evm/src/instructions/environmental_information.cairo index 57e87aa4a..513f362c6 100644 --- a/crates/evm/src/instructions/environmental_information.cairo +++ b/crates/evm/src/instructions/environmental_information.cairo @@ -1,5 +1,5 @@ use contracts::kakarot_core::interface::{IKakarotCore}; -use contracts::kakarot_core::{ContractAccountStorage, KakarotCore}; +use contracts::kakarot_core::{KakarotCore}; use core::hash::{HashStateExTrait, HashStateTrait}; use evm::balance::balance; use evm::context::ExecutionContextTrait; @@ -29,7 +29,7 @@ impl EnvironmentInformationImpl of EnvironmentInformationTrait { fn exec_balance(ref self: Machine) -> Result<(), EVMError> { let evm_address = self.stack.pop_eth_address()?; - let balance = balance(evm_address); + let balance = balance(evm_address)?; return self.stack.push(balance); } diff --git a/crates/evm/src/storage_journal.cairo b/crates/evm/src/storage_journal.cairo index 0137a9b95..934e248e3 100644 --- a/crates/evm/src/storage_journal.cairo +++ b/crates/evm/src/storage_journal.cairo @@ -1,6 +1,7 @@ +use evm::errors::{EVMError, WRITE_SYSCALL_FAILED, READ_SYSCALL_FAILED}; use nullable::{match_nullable, FromNullableResult}; use starknet::{StorageBaseAddress, Store, storage_base_address_from_felt252}; -use utils::helpers::ArrayExtTrait; +use utils::helpers::{ArrayExtTrait, ResultExTrait}; use utils::traits::{StorageBaseAddressPartialEq, StorageBaseAddressIntoFelt252}; /// The Journal tracks the changes applied to storage during the execution of a transaction. /// Local changes tracks the changes applied inside a single execution context. @@ -63,7 +64,7 @@ impl JournalImpl of JournalTrait { /// Finalizes the global changes in the journal by writing them to the storage to be stored permanently onchain. /// Global changes are relative the the execution of an entire transaction. `finalize_global` must be called upon finishing the transaction. - fn finalize_global(ref self: Journal) { + fn finalize_global(ref self: Journal) -> Result<(), EVMError> { let mut global_keys = self.global_keys.span(); loop { match global_keys.pop_front() { @@ -74,14 +75,22 @@ impl JournalImpl of JournalTrait { FromNullableResult::Null => {}, FromNullableResult::NotNull(value) => { let value = value.unbox(); - Store::write(0, key, value); + match Store::write(0, key, value) { + Result::Ok(()) => {}, + Result::Err(error) => { + break Result::Err( + EVMError::SyscallFailed(WRITE_SYSCALL_FAILED) + ); + } + }; } }; }, - Option::None => { break; } - } - }; + Option::None => { break Result::Ok(()); } + }; + }?; self.global_keys = Default::default(); + Result::Ok(()) } fn clear_local(ref self: Journal) { diff --git a/crates/evm/src/tests/test_balance.cairo b/crates/evm/src/tests/test_balance.cairo index 851f4c9c8..68434ef35 100644 --- a/crates/evm/src/tests/test_balance.cairo +++ b/crates/evm/src/tests/test_balance.cairo @@ -19,7 +19,7 @@ fn test_balance_eoa() { // When set_contract_address(kakarot_core.contract_address); - let balance = balance(evm_address()); + let balance = balance(evm_address()).unwrap(); // Then assert(balance == native_token.balanceOf(eoa), 'wrong balance'); @@ -34,7 +34,7 @@ fn test_balance_zero() { // When set_contract_address(kakarot_core.contract_address); - let balance = balance(evm_address()); + let balance = balance(evm_address()).unwrap(); // Then assert(balance == 0x00, 'wrong balance'); diff --git a/crates/utils/src/helpers.cairo b/crates/utils/src/helpers.cairo index 9055a6729..b716f5f11 100644 --- a/crates/utils/src/helpers.cairo +++ b/crates/utils/src/helpers.cairo @@ -507,14 +507,15 @@ impl SpanExtension, +Drop> of SpanExtTrait { } } -trait BytesSerde { - /// Serialize/deserialize bytes into/from - /// an array of bytes. - fn deserialize(self: Span) -> Option; -} - -impl BytesSerdeU32Impl of BytesSerde { - fn deserialize(self: Span) -> Option { +#[generate_trait] +impl U32Impl of U32Trait { + /// Packs 4 bytes into a u32 + /// # Arguments + /// * `self` a Span of len <=4 + /// # Returns + /// * Option::Some(u32) if the operation succeeds + /// * Option::None otherwise + fn from_bytes(self: Span) -> Option { let len = self.len(); if len > 4 { return Option::None(()); @@ -596,3 +597,14 @@ impl ByteArrayExt of ByteArrayExTrait { arr } } + +#[generate_trait] +impl ResultExImpl, +Drop> of ResultExTrait { + /// Converts a Result to a Result + fn map_err>(self: Result, err: F) -> Result { + match self { + Result::Ok(val) => Result::Ok(val), + Result::Err(_) => Result::Err(err) + } + } +} diff --git a/crates/utils/src/rlp.cairo b/crates/utils/src/rlp.cairo index ab8ce13af..8bbc42379 100644 --- a/crates/utils/src/rlp.cairo +++ b/crates/utils/src/rlp.cairo @@ -4,7 +4,7 @@ use array::{Array, ArrayTrait, Span, SpanTrait}; use clone::Clone; use traits::{Into, TryInto}; use utils::errors::{RLPError, RLP_EMPTY_INPUT, RLP_INPUT_TOO_SHORT}; -use utils::helpers::BytesSerde; +use utils::helpers::U32Trait; // All possible RLP types #[derive(Drop, PartialEq)] @@ -81,7 +81,7 @@ fn rlp_decode(input: Span) -> Result<(RLPItem, usize), RLPError> { } let string_len_bytes = input.slice(1, len_bytes_count); - let string_len: u32 = string_len_bytes.deserialize().unwrap(); + let string_len: u32 = U32Trait::from_bytes(string_len_bytes).unwrap(); if input.len() <= string_len + len_bytes_count { return Result::Err(RLPError::RlpInputTooShort(RLP_INPUT_TOO_SHORT)); } @@ -107,7 +107,7 @@ fn rlp_decode(input: Span) -> Result<(RLPItem, usize), RLPError> { } let list_len_bytes = input.slice(1, len_bytes_count); - let list_len: u32 = list_len_bytes.deserialize().unwrap(); + let list_len: u32 = U32Trait::from_bytes(list_len_bytes).unwrap(); if input.len() < list_len + len_bytes_count + 1 { return Result::Err(RLPError::RlpInputTooShort(RLP_INPUT_TOO_SHORT)); } diff --git a/crates/utils/src/tests/test_helpers.cairo b/crates/utils/src/tests/test_helpers.cairo index 3c3e60a78..18a1e8c1c 100644 --- a/crates/utils/src/tests/test_helpers.cairo +++ b/crates/utils/src/tests/test_helpers.cairo @@ -1,8 +1,9 @@ use utils::helpers; use utils::helpers::{ - SpanExtension, SpanExtTrait, ArrayExtension, ArrayExtTrait, U256Trait, BytesSerde + SpanExtension, SpanExtTrait, ArrayExtension, ArrayExtTrait, U256Trait, U32Trait }; use utils::helpers::{ByteArrayExTrait}; +use utils::traits::{ByteArraySerde}; #[test] #[available_gas(2000000000)] @@ -275,9 +276,61 @@ fn test_pack_bytes_ge_bytes31() { #[available_gas(2000000000)] fn test_bytes_serde_u32_deserialize() { let input: Array = array![0xf4, 0x32, 0x15, 0x62]; - let res: Option = input.span().deserialize(); + let res: Option = U32Trait::from_bytes(input.span()); assert(res != Option::None, 'should have a value'); let res = res.unwrap(); assert(res == 0xf4321562, 'wrong result value'); } + +#[test] +#[available_gas(2000000000)] +fn test_bytearray_deserialize() { + let mut serialized: Span = array![ + 0x03, 0xabcdef, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + ] + .span(); + + let deserialized: ByteArray = Serde::::deserialize(ref serialized).unwrap(); + + let expected = ByteArray { + data: array![ + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.try_into().unwrap() + ], + pending_word_len: 3, + pending_word: 0xabcdef + }; + assert(expected.len() == deserialized.len(), 'len mismatch'); + let mut i = 0; + loop { + if i == deserialized.len() { + break; + } + + assert(expected[i] == deserialized[i], 'item mismatch'); + i += 1; + }; +} + +#[test] +#[available_gas(20000000)] +fn test_bytearray_serialize() { + let byte_arr = ByteArray { + data: array![ + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff.try_into().unwrap() + ], + pending_word_len: 3, + pending_word: 0xabcdef + }; + let mut serialized: Array = Default::default(); + byte_arr.serialize(ref serialized); + + // One extra element encodes the length of the pending word + assert(serialized.len() == 3, 'len mismatch'); + assert(*serialized[0] == 3, 'pending_word_len mismatch'); + assert(*serialized[1] == 0xabcdef, 'pending_word mismatch'); + assert( + *serialized[2] == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, + 'full_word mismatch' + ); +} diff --git a/crates/utils/src/traits.cairo b/crates/utils/src/traits.cairo index eea4afc0b..0691a7877 100644 --- a/crates/utils/src/traits.cairo +++ b/crates/utils/src/traits.cairo @@ -131,3 +131,36 @@ impl StoreBytes31 of Store { 1_u8 } } + +impl ByteArraySerde of Serde { + fn serialize(self: @ByteArray, ref output: Array) { + // First felt is number of bytes used in the last felt + // Second felt is the pending word + // Subsequent felts are the full 31-byte words + output.append((*self.pending_word_len).into()); + output.append((*self.pending_word).into()); + let mut i = 0; + loop { + if i == self.data.len() { + break; + } + output.append((*self.data[i]).into()); + i += 1; + }; + } + + fn deserialize(ref serialized: Span) -> Option { + let pending_word_len: u32 = (*serialized.pop_front()?).try_into()?; + let pending_word = *serialized.pop_front()?; + let mut data: Array = Default::default(); + loop { + match serialized.pop_front() { + Option::Some(val) => { data.append((*val).try_into().unwrap()); }, + Option::None => { break; } + } + }; + Option::Some( + ByteArray { data: data, pending_word: pending_word, pending_word_len: pending_word_len } + ) + } +}