diff --git a/crates/contracts/Scarb.toml b/crates/contracts/Scarb.toml index 6653e8dc2..ae5c7ec1a 100644 --- a/crates/contracts/Scarb.toml +++ b/crates/contracts/Scarb.toml @@ -8,6 +8,8 @@ version = "0.1.0" starknet.workspace = true evm = { path = "../evm" } openzeppelin = { path = "../openzeppelin" } +utils = { path = "../utils" } +alexandria_storage = { git = "https://github.com/keep-starknet-strange/alexandria.git", rev = "3356bf0c5c1a089167d7d3c28d543e195325e596" } [tool.fmt] sort-module-level-items = true diff --git a/crates/contracts/src/contract_account.cairo b/crates/contracts/src/contract_account.cairo new file mode 100644 index 000000000..768af51f4 --- /dev/null +++ b/crates/contracts/src/contract_account.cairo @@ -0,0 +1,48 @@ +//! Contract Account related functions, including bytecode storage + +use alexandria_storage::list::{List, ListTrait}; +use hash::{HashStateTrait, HashStateExTrait}; +use poseidon::PoseidonTrait; +use starknet::{ + StorageBaseAddress, storage_base_address_from_felt252, Store, EthAddress, SyscallResult, + storage_write_syscall, storage_address_from_base, storage_read_syscall, + storage_address_from_base_and_offset +}; +use utils::helpers::{ByteArrayExTrait}; +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()); +} diff --git a/crates/contracts/src/lib.cairo b/crates/contracts/src/lib.cairo index a431c0599..e5c288081 100644 --- a/crates/contracts/src/lib.cairo +++ b/crates/contracts/src/lib.cairo @@ -1,5 +1,8 @@ mod components; +// Contract account-related module +mod contract_account; + // Kakarot smart contract mod kakarot_core; diff --git a/crates/contracts/src/tests.cairo b/crates/contracts/src/tests.cairo index 3f906951a..7a208e4c6 100644 --- a/crates/contracts/src/tests.cairo +++ b/crates/contracts/src/tests.cairo @@ -1,5 +1,8 @@ #[cfg(test)] +mod test_contract_account; +#[cfg(test)] mod test_kakarot_core; + #[cfg(test)] mod test_ownable; diff --git a/crates/contracts/src/tests/test_contract_account.cairo b/crates/contracts/src/tests/test_contract_account.cairo new file mode 100644 index 000000000..01ea4c2df --- /dev/null +++ b/crates/contracts/src/tests/test_contract_account.cairo @@ -0,0 +1,169 @@ +use alexandria_storage::list::{List, ListTrait}; +use contracts::contract_account::{store_bytecode}; +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(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()); + + // Address at which the bytecode should be stored + let data_addr = compute_storage_base_address( + selector!("contract_account_bytecode"), array![evm_address.into()].span() + ); + let pending_word_addr = storage_base_address_from_felt252(data_addr.into() - 2_felt252); + let pending_word_len_addr = storage_base_address_from_felt252(data_addr.into() - 1_felt252); + + let pending_word = Store::::read(0, pending_word_addr).unwrap(); + let pending_word_len = Store::::read(0, pending_word_len_addr).unwrap(); + let list: List = Store::>::read(0, data_addr).unwrap(); + let bytecode: ByteArray = ByteArray { + data: list.array(), pending_word: pending_word, pending_word_len: pending_word_len + }; + + assert(bytecode.pending_word_len == 3, 'pending word not 3'); + assert(bytecode.pending_word == 0x010203, 'pending word not restituted'); + assert(bytecode.data.len() == 0, 'data not empty'); +} + + +#[test] +#[available_gas(20000000)] +fn test_store_bytecode_one_word() { + 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, // 31 elements + ]; + let evm_address = EVM_ADDRESS(); + store_bytecode(evm_address, byte_array.span()); + + // Address at which the bytecode should be stored + let data_addr = compute_storage_base_address( + selector!("contract_account_bytecode"), array![evm_address.into()].span() + ); + let pending_word_addr = storage_base_address_from_felt252(data_addr.into() - 2_felt252); + let pending_word_len_addr = storage_base_address_from_felt252(data_addr.into() - 1_felt252); + + let pending_word = Store::::read(0, pending_word_addr).unwrap(); + let pending_word_len = Store::::read(0, pending_word_len_addr).unwrap(); + let list: List = Store::>::read(0, data_addr).unwrap(); + let bytecode: ByteArray = ByteArray { + data: list.array(), pending_word: pending_word, pending_word_len: pending_word_len + }; + + assert(bytecode.pending_word_len == 0, 'pending word len not empty'); + assert(bytecode.pending_word == 0, 'pending word not empty'); + let mut i: u32 = 0; + loop { + if i == byte_array.len() { + break; + } + assert(bytecode[i] == *byte_array[i], 'stored bytecode error'); + i += 1; + } +} + +#[test] +#[available_gas(20000000)] +fn test_store_bytecode_one_word_pending() { + 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(); + store_bytecode(evm_address, byte_array.span()); + + // Address at which the bytecode should be stored + let data_addr = compute_storage_base_address( + selector!("contract_account_bytecode"), array![evm_address.into()].span() + ); + let pending_word_addr = storage_base_address_from_felt252(data_addr.into() - 2_felt252); + let pending_word_len_addr = storage_base_address_from_felt252(data_addr.into() - 1_felt252); + + let pending_word = Store::::read(0, pending_word_addr).unwrap(); + let pending_word_len = Store::::read(0, pending_word_len_addr).unwrap(); + let list: List = Store::>::read(0, data_addr).unwrap(); + let bytecode: ByteArray = ByteArray { + data: list.array(), pending_word: pending_word, pending_word_len: pending_word_len + }; + + assert(bytecode.pending_word_len == 2, 'pending word len not two'); + assert(bytecode.pending_word == 0x2021, 'pending word not restituted'); + let mut i: u32 = 0; + loop { + if i == byte_array.len() { + break; + } + assert(bytecode[i] == *byte_array[i], 'stored bytecode error'); + i += 1; + } +} +//TODO add a test with huge amount of bytecode - using SNFoundry and loading data from txt + + diff --git a/crates/contracts/src/tests/utils.cairo b/crates/contracts/src/tests/utils.cairo index 7d6209166..78ce982c4 100644 --- a/crates/contracts/src/tests/utils.cairo +++ b/crates/contracts/src/tests/utils.cairo @@ -36,7 +36,7 @@ fn assert_no_events_left(address: ContractAddress) { } mod constants { - use starknet::{testing, contract_address_const, ContractAddress}; + use starknet::{EthAddress, testing, contract_address_const, ContractAddress}; fn ZERO() -> ContractAddress { contract_address_const::<0>() } @@ -49,6 +49,10 @@ mod constants { contract_address_const::<0xe1145>() } + fn EVM_ADDRESS() -> EthAddress { + 0xc0ffee.try_into().unwrap() + } + fn ETH_BANK() -> ContractAddress { contract_address_const::<0x777>() } diff --git a/crates/evm/src/context.cairo b/crates/evm/src/context.cairo index 5becd1439..d9c5f4b82 100644 --- a/crates/evm/src/context.cairo +++ b/crates/evm/src/context.cairo @@ -4,7 +4,7 @@ use evm::model::Event; use evm::stack::{Stack, StackTrait}; use starknet::get_caller_address; use starknet::{EthAddress, ContractAddress}; -use utils::helpers::{ArrayExtension, ArrayExtensionTrait}; +use utils::helpers::{ArrayExtension, ArrayExtTrait}; use utils::traits::{SpanDefault, EthAddressDefault, ContractAddressDefault}; #[derive(Drop, Default, Copy, PartialEq)] diff --git a/crates/evm/src/instructions/sha3.cairo b/crates/evm/src/instructions/sha3.cairo index 20ee5aa28..8a4f618c3 100644 --- a/crates/evm/src/instructions/sha3.cairo +++ b/crates/evm/src/instructions/sha3.cairo @@ -6,7 +6,7 @@ use evm::machine::Machine; use evm::memory::MemoryTrait; use evm::stack::StackTrait; use keccak::{cairo_keccak, u128_split}; -use utils::helpers::{ArrayExtensionTrait, U256Trait}; +use utils::helpers::{ArrayExtTrait, U256Trait}; #[generate_trait] impl Sha3Impl of Sha3Trait { diff --git a/crates/evm/src/memory.cairo b/crates/evm/src/memory.cairo index 5e564a765..32b0e2838 100644 --- a/crates/evm/src/memory.cairo +++ b/crates/evm/src/memory.cairo @@ -8,7 +8,7 @@ use utils::constants::{ POW_2_72, POW_2_80, POW_2_88, POW_2_96, POW_2_104, POW_2_112, POW_2_120, POW_256_16 }; use utils::{ - helpers, helpers::SpanExtensionTrait, helpers::ArrayExtensionTrait, math::Exponentiation, + helpers, helpers::SpanExtTrait, helpers::ArrayExtTrait, math::Exponentiation, math::WrappingExponentiation, math::Bitshift }; diff --git a/crates/evm/src/storage_journal.cairo b/crates/evm/src/storage_journal.cairo index a05d1a3ce..0137a9b95 100644 --- a/crates/evm/src/storage_journal.cairo +++ b/crates/evm/src/storage_journal.cairo @@ -1,6 +1,6 @@ use nullable::{match_nullable, FromNullableResult}; use starknet::{StorageBaseAddress, Store, storage_base_address_from_felt252}; -use utils::helpers::ArrayExtensionTrait; +use utils::helpers::ArrayExtTrait; 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. diff --git a/crates/evm/src/tests/test_instructions/test_environment_information.cairo b/crates/evm/src/tests/test_instructions/test_environment_information.cairo index 50aba05b4..3ced92655 100644 --- a/crates/evm/src/tests/test_instructions/test_environment_information.cairo +++ b/crates/evm/src/tests/test_instructions/test_environment_information.cairo @@ -18,8 +18,7 @@ use openzeppelin::token::erc20::interface::IERC20CamelDispatcherTrait; use starknet::{EthAddressIntoFelt252, contract_address_const, testing::set_contract_address}; use utils::helpers::{ - u256_to_bytes_array, load_word, ArrayExtension, ArrayExtensionTrait, SpanExtension, - SpanExtensionTrait + u256_to_bytes_array, load_word, ArrayExtension, ArrayExtTrait, SpanExtension, SpanExtTrait }; use utils::traits::{EthAddressIntoU256}; @@ -706,9 +705,9 @@ fn test_returndata_copy(dest_offset: u32, offset: u32, mut size: u32) { let result_span = u256_to_bytes_array(result).span(); if ((i + 1) * 32 > size) { - ArrayExtensionTrait::concat(ref results, result_span.slice(0, size - (i * 32))); + ArrayExtTrait::concat(ref results, result_span.slice(0, size - (i * 32))); } else { - ArrayExtensionTrait::concat(ref results, result_span); + ArrayExtTrait::concat(ref results, result_span); } i += 1; diff --git a/crates/evm/src/tests/test_memory.cairo b/crates/evm/src/tests/test_memory.cairo index 80ef1a430..61472aad9 100644 --- a/crates/evm/src/tests/test_memory.cairo +++ b/crates/evm/src/tests/test_memory.cairo @@ -1,9 +1,7 @@ use evm::memory::{MemoryTrait, InternalMemoryTrait, MemoryPrintTrait}; use integer::BoundedInt; use utils::constants::{POW_2_8, POW_2_56, POW_2_64, POW_2_120}; -use utils::{ - math::Exponentiation, math::WrappingExponentiation, helpers, helpers::SpanExtensionTrait -}; +use utils::{math::Exponentiation, math::WrappingExponentiation, helpers, helpers::SpanExtTrait}; mod internal { use evm::memory::{MemoryTrait, InternalMemoryTrait, MemoryPrintTrait}; diff --git a/crates/utils/src/helpers.cairo b/crates/utils/src/helpers.cairo index ccc8f4b0d..5153a7e16 100644 --- a/crates/utils/src/helpers.cairo +++ b/crates/utils/src/helpers.cairo @@ -335,8 +335,8 @@ fn pow2(pow: usize) -> u128 { /// Splits a u256 into `len` bytes, big-endian, and appends the result to `dst`. fn split_word(mut value: u256, mut len: usize, ref dst: Array) { let word_le = split_word_le(value, len); - let word_be = ArrayExtensionTrait::reverse(word_le.span()); - ArrayExtensionTrait::concat(ref dst, word_be.span()); + let word_be = ArrayExtTrait::reverse(word_le.span()); + ArrayExtTrait::concat(ref dst, word_be.span()); } fn split_u128_le(ref dest: Array, mut value: u128, mut len: usize) { @@ -442,7 +442,7 @@ fn u256_to_bytes_array(mut value: u256) -> Array { } #[generate_trait] -impl ArrayExtension, impl TDrop: Drop> of ArrayExtensionTrait { +impl ArrayExtension, impl TDrop: Drop> of ArrayExtTrait { // Concatenates two arrays by adding the elements of arr2 to arr1. fn concat(ref self: Array, mut arr2: Span) { loop { @@ -490,7 +490,7 @@ impl ArrayExtension, impl TDrop: Drop> of ArrayExtensi } #[generate_trait] -impl SpanExtension, +Drop> of SpanExtensionTrait { +impl SpanExtension, +Drop> of SpanExtTrait { // Returns true if the array contains an item. fn contains<+PartialEq>(mut self: Span, value: T) -> bool { loop { @@ -523,3 +523,19 @@ impl U256Impl of U256Trait { } } +#[generate_trait] +impl ByteArrayExt of ByteArrayExTrait { + fn from_bytes(mut bytes: Span) -> ByteArray { + //TODO(eni): optimize deserialization of Span to ByteArray; + // we can just deserialize bytes by chunks of 31, skipping pending_word + // checks + let mut arr: ByteArray = Default::default(); + loop { + match bytes.pop_front() { + Option::Some(byte) => { arr.append_byte(*byte); }, + Option::None => { break; } + } + }; + arr + } +} diff --git a/crates/utils/src/lib.cairo b/crates/utils/src/lib.cairo index 85f2ad5ac..e433e5d0c 100644 --- a/crates/utils/src/lib.cairo +++ b/crates/utils/src/lib.cairo @@ -8,6 +8,7 @@ mod eth_transaction; mod rlp; mod traits; mod i256; +mod storage; #[cfg(test)] mod tests; diff --git a/crates/utils/src/storage.cairo b/crates/utils/src/storage.cairo new file mode 100644 index 000000000..0368e48a0 --- /dev/null +++ b/crates/utils/src/storage.cairo @@ -0,0 +1,31 @@ +use starknet::{ + StorageBaseAddress, storage_base_address_from_felt252, Store, EthAddress, SyscallResult, + storage_write_syscall, storage_address_from_base, storage_read_syscall, + storage_address_from_base_and_offset +}; +use poseidon::PoseidonTrait; +use hash::{HashStateTrait, HashStateExTrait}; + + +/// Computes the StorageBaseAddress of a storage variable. The address is +/// computed by applying hashing the variable selector and the keys with +/// Poseidon. +/// # Arguments +/// * `selector` - The selector of the storage variable. +/// * `keys` - The keys of the storage variable. +/// # Returns +/// * The StorageBaseAddress of the storage variable, calculated as +/// the sequential hashes of the selector and the keys. +fn compute_storage_base_address(selector: felt252, mut keys: Span) -> StorageBaseAddress { + //TODO: if we want compatibility with LegacyMaps, we should use pedersen + // it might not be required. + let mut state = PoseidonTrait::new().update(selector); + let hash = loop { + match keys.pop_front() { + Option::Some(val) => { state = state.update(*val); }, + Option::None => { break state.finalize(); } + }; + }; + + storage_base_address_from_felt252(hash) +} diff --git a/crates/utils/src/tests/test_helpers.cairo b/crates/utils/src/tests/test_helpers.cairo index baf6eb5b0..bdc9e898e 100644 --- a/crates/utils/src/tests/test_helpers.cairo +++ b/crates/utils/src/tests/test_helpers.cairo @@ -1,7 +1,6 @@ use utils::helpers; -use utils::helpers::{ - SpanExtension, SpanExtensionTrait, ArrayExtension, ArrayExtensionTrait, U256Trait -}; +use utils::helpers::{SpanExtension, SpanExtTrait, ArrayExtension, ArrayExtTrait, U256Trait}; +use utils::helpers::{ByteArrayExTrait}; use debug::PrintTrait; #[test] @@ -201,3 +200,72 @@ fn test_split_u256_into_u64_little() { assert(high_l == 0x0000450000DEFA00, 'split mismatch'); assert(low_l == 0x00200400000000AD, 'split mismatch'); } + +#[test] +#[available_gas(20000000)] +fn test_pack_bytes_le_bytes31() { + let mut arr = array![0x11, 0x22, 0x33, 0x44]; + let res = ByteArrayExTrait::from_bytes(arr.span()); + + // Ensure that the result is complete and keeps the same order + let mut i = 0; + loop { + if i == arr.len() { + break; + }; + assert(*arr[i] == res[i], 'byte mismatch'); + i += 1; + }; +} + +#[test] +#[available_gas(20000000)] +fn test_pack_bytes_ge_bytes31() { + let mut arr = 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 res = ByteArrayExTrait::from_bytes(arr.span()); + + // Ensure that the result is complete and keeps the same order + let mut i = 0; + loop { + if i == arr.len() { + break; + }; + assert(*arr[i] == res[i], 'byte mismatch'); + i += 1; + }; +} diff --git a/crates/utils/src/tests/test_storage.cairo b/crates/utils/src/tests/test_storage.cairo new file mode 100644 index 000000000..5d558079e --- /dev/null +++ b/crates/utils/src/tests/test_storage.cairo @@ -0,0 +1,16 @@ +use utils::storage::compute_storage_base_address; +use starknet::{StorageBaseAddress, storage_base_address_from_felt252, storage_address_from_base}; + +#[test] +#[available_gas(20000000)] +fn test_compute_storage_base_address() { + let selector = selector!("my_storage_var"); + let keys = array![0x01, 0x02].span(); + + let base = compute_storage_base_address(selector, keys); + let addr = storage_address_from_base(base); + assert( + addr.into() == 0x07f99861f217719795b0dfa211100a10fc3c1cefaff03426ebedfc922e81bb15, + 'wrong address' + ); // hash calculated with starknet_crypto rs crate +} diff --git a/crates/utils/src/traits.cairo b/crates/utils/src/traits.cairo index 3fa59516c..eea4afc0b 100644 --- a/crates/utils/src/traits.cairo +++ b/crates/utils/src/traits.cairo @@ -1,6 +1,6 @@ use starknet::{ StorageBaseAddress, storage_address_from_base, storage_base_address_from_felt252, EthAddress, - ContractAddress + ContractAddress, Store, SyscallResult }; use utils::math::{Zero, One}; use evm::errors::{EVMError, TYPE_CONVERSION_ERROR}; @@ -96,3 +96,38 @@ impl U256TryIntoResultU32 of TryIntoResult { } } } + + +impl StoreBytes31 of Store { + fn read(address_domain: u32, base: StorageBaseAddress) -> SyscallResult { + Result::Ok( + Store::::read(address_domain, base)? + .try_into() + .expect('StoreBytes31 - non bytes31') + ) + } + #[inline(always)] + fn write(address_domain: u32, base: StorageBaseAddress, value: bytes31) -> SyscallResult<()> { + Store::::write(address_domain, base, value.into()) + } + #[inline(always)] + fn read_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8 + ) -> SyscallResult { + Result::Ok( + Store::::read_at_offset(address_domain, base, offset)? + .try_into() + .expect('StoreBytes31 - non bytes31') + ) + } + #[inline(always)] + fn write_at_offset( + address_domain: u32, base: StorageBaseAddress, offset: u8, value: bytes31 + ) -> SyscallResult<()> { + Store::::write_at_offset(address_domain, base, offset, value.into()) + } + #[inline(always)] + fn size() -> u8 { + 1_u8 + } +}