From 597d753700f197ca147a1171ad2790de13d701ef Mon Sep 17 00:00:00 2001 From: Quentash <100387965+Quentash@users.noreply.github.com> Date: Mon, 25 Sep 2023 05:24:07 +0200 Subject: [PATCH] feat: implement 0x20 - SHA3 Opcode (#281) * testing herodotus fct * preclean * cleaned code and tests * merge main * forgot to change gas_limit back to 1k * review correction * added helpers * soluce2 + check on memory load * typos * added internal sha3 tests * added helpers tests * wrong error message * used mod internal instead of trait * functions and comments rework * bug correction due to merge * removed unnecessary computation * quick fix review * corrected tests names * docs: added fn docs * made compute_words more verbose * Made helper fcts on u256trait * modified comments * Ensured memory lenght * added memory size verification to tests --------- Co-authored-by: msaug --- crates/evm/src/instructions.cairo | 1 + crates/evm/src/instructions/sha3.cairo | 153 ++++++- crates/evm/src/tests/test_instructions.cairo | 1 + .../tests/test_instructions/test_sha3.cairo | 409 ++++++++++++++++++ crates/utils/src/helpers.cairo | 19 + crates/utils/src/tests/test_helpers.cairo | 26 +- 6 files changed, 603 insertions(+), 6 deletions(-) create mode 100644 crates/evm/src/tests/test_instructions/test_sha3.cairo diff --git a/crates/evm/src/instructions.cairo b/crates/evm/src/instructions.cairo index d29e24eee..ef51bb420 100644 --- a/crates/evm/src/instructions.cairo +++ b/crates/evm/src/instructions.cairo @@ -23,6 +23,7 @@ mod push_operations; use push_operations::PushOperationsTrait; mod sha3; +use sha3::Sha3Trait; mod stop_and_arithmetic_operations; use stop_and_arithmetic_operations::StopAndArithmeticOperationsTrait; diff --git a/crates/evm/src/instructions/sha3.cairo b/crates/evm/src/instructions/sha3.cairo index 9077c244c..5302368d5 100644 --- a/crates/evm/src/instructions/sha3.cairo +++ b/crates/evm/src/instructions/sha3.cairo @@ -1,9 +1,152 @@ //! SHA3. // Internal imports -use evm::context::{ExecutionContextTrait, ExecutionContext}; +use evm::context::{ExecutionContext, ExecutionContextTrait, BoxDynamicExecutionContextDestruct}; +use evm::stack::StackTrait; +use evm::memory::MemoryTrait; +use evm::errors::EVMError; +use evm::helpers::U256IntoResultU32; +use keccak::{cairo_keccak, u128_split}; +use utils::helpers::{ArrayExtensionTrait, U256Trait}; -/// SHA3 operation. -/// Hashes n bytes in memory at a given offset in memory. -/// # Specification: https://www.evm.codes/#20?fork=shanghai -fn exec_sha3(ref context: ExecutionContext) {} +use array::ArrayTrait; + +#[generate_trait] +impl Sha3Impl of Sha3Trait { + /// SHA3 operation : Hashes n bytes in memory at a given offset in memory + /// and push the hash result to the stack. + /// + /// # Inputs + /// * `offset` - The offset in memory where to read the datas + /// * `size` - The amount of bytes to read + /// + /// # Specification: https://www.evm.codes/#20?fork=shanghai + fn exec_sha3(ref self: ExecutionContext) -> Result<(), EVMError> { + let offset: usize = self.stack.pop_usize()?; + let mut size: usize = self.stack.pop_usize()?; + + let mut to_hash: Array = Default::default(); + + let (nb_words, nb_zeroes) = internal::compute_memory_words_amount( + size, offset, self.memory.bytes_len + ); + let mut last_input_offset = internal::fill_array_with_memory_words( + ref self, ref to_hash, offset, nb_words + ); + // Fill array to hash with zeroes for bytes out of memory bound + // which is faster than reading them from memory + to_hash.append_n(0, 4 * nb_zeroes); + + // For cases where the size of bytes to hash isn't a multiple of 8, + // prepare the last bytes to hash into last_input instead of appending + // it to to_hash. + let last_input: u64 = if (size % 32 != 0) { + let loaded = self.memory.load(last_input_offset); + internal::prepare_last_input(ref to_hash, loaded, size % 32) + } else { + 0 + }; + // Properly set the memory length in case we skipped reading zeroes + self.memory.ensure_length(size + offset); + + let mut hash = cairo_keccak(ref to_hash, last_input, size % 8); + self.stack.push(hash.reverse_endianness()) + } +} + + +mod internal { + use evm::context::{ExecutionContext, ExecutionContextTrait, BoxDynamicExecutionContextDestruct}; + use evm::stack::StackTrait; + use evm::memory::MemoryTrait; + use utils::helpers::U256Trait; + + /// Computes how many words are read from the memory + /// and how many words must be filled with zeroes + /// given a target size, a memory offset and the length of the memory. + /// + /// # Arguments + /// + /// * `size` - The amount of bytes to hash + /// * `offset` - Offset in memory + /// * `mem_len` - Size of the memory + /// Returns : (nb_words, nb_zeroes) + fn compute_memory_words_amount(size: u32, offset: u32, mem_len: u32) -> (u32, u32) { + // Bytes to hash are less than a word size + if size < 32 { + return (0, 0); + } + // Bytes out of memory bound are zeroes + if offset > mem_len { + return (0, size / 32); + } + // The only word to read from memory is less than 32 bytes + if mem_len - offset < 32 { + return (1, (size / 32) - 1); + } + + let bytes_to_read = cmp::min(mem_len - offset, size); + let nb_words = bytes_to_read / 32; + (nb_words, (size / 32) - nb_words) + } + + /// Fills the `to_hash` array with little endian u64s + /// by splitting words read from the memory and + /// returns the next offset to read from. + /// + /// # Arguments + /// + /// * `self` - The context in which the memory is read + /// * `to_hash` - A reference to the array to fill + /// * `offset` - Offset in memory to start reading from + /// * `amount` - The amount of words to read from memory + /// Return the new offset + fn fill_array_with_memory_words( + ref self: ExecutionContext, ref to_hash: Array, mut offset: u32, mut amount: u32 + ) -> u32 { + loop { + if amount == 0 { + break; + } + let loaded = self.memory.load(offset); + let ((high_h, low_h), (high_l, low_l)) = loaded.split_into_u64_le(); + to_hash.append(low_h); + to_hash.append(high_h); + to_hash.append(low_l); + to_hash.append(high_l); + + offset += 32; + amount -= 1; + }; + offset + } + + /// Fills the `to_hash` array with the n-1 remaining little endian u64 + /// depending on size from a word and returns + /// the u64 containing the last 8 bytes word to hash. + /// + /// # Arguments + /// + /// * `to_hash` - A reference to the array to fill + /// * `value` - The word to split in u64 words + /// * `size` - The amount of bytes still required to hash + /// Returns the last u64 word that isn't 8 Bytes long. + fn prepare_last_input(ref to_hash: Array, value: u256, size: u32) -> u64 { + let ((high_h, low_h), (high_l, low_l)) = value.split_into_u64_le(); + if size < 8 { + return low_h; + } else if size < 16 { + to_hash.append(low_h); + return high_h; + } else if size < 24 { + to_hash.append(low_h); + to_hash.append(high_h); + return low_l; + } else { + to_hash.append(low_h); + to_hash.append(high_h); + to_hash.append(low_l); + return high_l; + } + } +} diff --git a/crates/evm/src/tests/test_instructions.cairo b/crates/evm/src/tests/test_instructions.cairo index 3e53835c5..9c3c5b109 100644 --- a/crates/evm/src/tests/test_instructions.cairo +++ b/crates/evm/src/tests/test_instructions.cairo @@ -7,3 +7,4 @@ mod test_push_operations; mod test_memory_operations; mod test_exchange_operations; mod test_system_operations; +mod test_sha3; diff --git a/crates/evm/src/tests/test_instructions/test_sha3.cairo b/crates/evm/src/tests/test_instructions/test_sha3.cairo new file mode 100644 index 000000000..044ad9031 --- /dev/null +++ b/crates/evm/src/tests/test_instructions/test_sha3.cairo @@ -0,0 +1,409 @@ +use evm::instructions::Sha3Trait; +use evm::instructions::sha3::internal; +use evm::tests::test_utils::setup_execution_context; +use evm::context::{ExecutionContext, ExecutionContextTrait, BoxDynamicExecutionContextDestruct}; +use evm::memory::{InternalMemoryTrait, MemoryTrait}; +use evm::stack::StackTrait; +use option::OptionTrait; +use evm::errors::{EVMError, TYPE_CONVERSION_ERROR}; + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_0_offset_0() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(0x00); + ctx.stack.push(0x00); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + + assert( + result == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, 'wrong result' + ); + assert(ctx.memory.bytes_len == 32, 'wrong memory size'); +} + + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_5_offset_4() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(0x05); + ctx.stack.push(0x04); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec, 'wrong result' + ); + assert(ctx.memory.bytes_len == 64, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_10_offset_10() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(10); + ctx.stack.push(10); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x6bd2dd6bd408cbee33429358bf24fdc64612fbf8b1b4db604518f40ffd34b607, 'wrong result' + ); + assert(ctx.memory.bytes_len == 64, 'wrong memory size'); +} + +#[test] +#[available_gas(1000000000000000)] +fn test_exec_sha3_size_0xFFFFF_offset_1000() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(0xFFFFF); + ctx.stack.push(1000); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0xbe6f1b42b34644f918560a07f959d23e532dea5338e4b9f63db0caeb608018fa, 'wrong result' + ); + assert(ctx.memory.bytes_len == (((0xFFFFF + 1000) + 31) / 32) * 32, 'wrong memory size'); +} + +#[test] +#[available_gas(1000000000000000)] +fn test_exec_sha3_size_1000000_offset_2() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(1000000); + ctx.stack.push(2); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x4aa461ae9513f3b03ae397740ade979809dd02ae2c14e101b32842fbee21f0a, 'wrong result' + ); + assert(ctx.memory.bytes_len == (((1000000 + 2) + 31) / 32) * 32, 'wrong memory size'); +} + +#[test] +#[available_gas(1000000000000000)] +fn test_exec_sha3_size_1000000_offset_23() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(1000000); + ctx.stack.push(2); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x4aa461ae9513f3b03ae397740ade979809dd02ae2c14e101b32842fbee21f0a, 'wrong result' + ); + assert(ctx.memory.bytes_len == (((1000000 + 23) + 31) / 32) * 32, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_1_offset_2048() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(1); + ctx.stack.push(2048); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a, 'wrong result' + ); + assert(ctx.memory.bytes_len == (((2048 + 1) + 31) / 32) * 32, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_0_offset_1024() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(0); + ctx.stack.push(1024); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, 'wrong result' + ); + assert(ctx.memory.bytes_len == 1024, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_32_offset_2016() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(32); + ctx.stack.push(2016); + + ctx.memory.store(0xFFFFFFFF00000000000000000000000000000000000000000000000000000000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563, 'wrong result' + ); + assert(ctx.memory.bytes_len == (((2016 + 32) + 31) / 32) * 32, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_32_offset_0() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(32); + ctx.stack.push(0); + + ctx.memory.store(0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x567d6b045256961aee949d6bb4d5f814c5b42e6b8bb49a833e8e89fbcddee86c, 'wrong result' + ); + assert(ctx.memory.bytes_len == 32, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_31_offset_0() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(31); + ctx.stack.push(0); + + ctx.memory.store(0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x4b13f212816c02cc818ba4802e81a4ac1904d2c920fe8d8cf3e4f05233a57d2e, 'wrong result' + ); + assert(ctx.memory.bytes_len == 32, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000)] +fn test_exec_sha3_size_33_offset_0() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(33); + ctx.stack.push(0); + + ctx.memory.store(0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000, 0); + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0xa6fa3edfabbe64b6ce26120b21ac9b8191005115d5e7e03fa58ec9cc74c0f2f4, 'wrong result' + ); + assert(ctx.memory.bytes_len == 64, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000000)] +fn test_exec_sha3_size_0x0C80_offset_0() { + // Given + let mut ctx = setup_execution_context(); + + ctx.stack.push(0x0C80); + ctx.stack.push(0x00); + + let mut mem_dst: u32 = 0; + loop { + if mem_dst > 0x0C80 { + break; + } + ctx + .memory + .store(0xFAFAFAFA00000000000000000000000000000000000000000000000000000000, mem_dst); + mem_dst += 0x20; + }; + + // When + ctx.exec_sha3(); + + // Then + let result = ctx.stack.peek().unwrap(); + assert( + result == 0x2022ae07f3a362b08ac0a4bcb785c830cb5c368dc0ce6972249c6abbc68a5291, 'wrong result' + ); + assert(ctx.memory.bytes_len == 0x0C80 + 32, 'wrong memory size'); +} + +#[test] +#[available_gas(20000000000)] +fn test_internal_fill_array_with_memory_words() { + // Given + let mut ctx = setup_execution_context(); + let mut to_hash: Array = Default::default(); + + ctx.memory.store(0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000, 0); + let mut size = 32; + let mut offset = 0; + + // When + let (words_from_mem, _) = internal::compute_memory_words_amount( + size, offset, ctx.memory.bytes_len + ); + internal::fill_array_with_memory_words(ref ctx, ref to_hash, offset, words_from_mem); + + // Then + assert(to_hash.len() == 4, 'wrong array length'); + assert((*to_hash[0]) == 0xE5000000FFFFFFFA, 'wrong array value'); + assert((*to_hash[1]) == 0xDE00000077000000, 'wrong array value'); + assert((*to_hash[2]) == 0x00200400000000AD, 'wrong array value'); + assert((*to_hash[3]) == 0x0000450000DEFA00, 'wrong array value'); +} + +#[test] +#[available_gas(20000000000)] +fn test_internal_fill_array_with_memory_words_size_33() { + // Given + let mut ctx = setup_execution_context(); + let mut to_hash: Array = Default::default(); + + ctx.memory.store(0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000, 0); + let mut size = 33; + let mut offset = 0; + + // When + let (words_from_mem, _) = internal::compute_memory_words_amount( + size, offset, ctx.memory.bytes_len + ); + internal::fill_array_with_memory_words(ref ctx, ref to_hash, offset, words_from_mem); + + // Then + assert(to_hash.len() == 4, 'wrong array length'); + assert((*to_hash[0]) == 0xE5000000FFFFFFFA, 'wrong array value'); + assert((*to_hash[1]) == 0xDE00000077000000, 'wrong array value'); + assert((*to_hash[2]) == 0x00200400000000AD, 'wrong array value'); + assert((*to_hash[3]) == 0x0000450000DEFA00, 'wrong array value'); +} + +#[test] +#[available_gas(20000000000)] +fn test_internal_fill_array_with_last_inputs_size_5() { + // Given + let mut to_hash: Array = Default::default(); + let value: u256 = 0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000; + let size = 5; + + // When + let result = internal::prepare_last_input(ref to_hash, value, size); + + // Then + assert(result == 0xE5000000FFFFFFFA, 'wrong result'); + assert(to_hash.len() == 0, 'wrong result'); +} + +#[test] +#[available_gas(20000000000)] +fn test_internal_fill_array_with_last_inputs_size_20() { + // Given + let mut to_hash: Array = Default::default(); + let value: u256 = 0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000; + let size = 20; + + // When + let result = internal::prepare_last_input(ref to_hash, value, size); + + // Then + assert(result == 0x00200400000000AD, 'wrong result'); + assert(to_hash.len() == 2, 'wrong result'); + assert((*to_hash[0]) == 0xE5000000FFFFFFFA, 'wrong array value'); + assert((*to_hash[1]) == 0xDE00000077000000, 'wrong array value'); +} + +#[test] +#[available_gas(20000000000)] +fn test_internal_fill_array_with_last_inputs_size_50() { + // Given + let mut to_hash: Array = Default::default(); + let value: u256 = 0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000; + let size = 50; + + // When + let result = internal::prepare_last_input(ref to_hash, value, size); + + // Then + assert(result == 0x0000450000DEFA00, 'wrong result'); + assert(to_hash.len() == 3, 'wrong result'); + assert((*to_hash[0]) == 0xE5000000FFFFFFFA, 'wrong array value'); + assert((*to_hash[1]) == 0xDE00000077000000, 'wrong array value'); + assert((*to_hash[2]) == 0x00200400000000AD, 'wrong array value'); +} diff --git a/crates/utils/src/helpers.cairo b/crates/utils/src/helpers.cairo index 307c852a8..73161f9fe 100644 --- a/crates/utils/src/helpers.cairo +++ b/crates/utils/src/helpers.cairo @@ -7,6 +7,7 @@ use utils::constants::{ POW_256_11_U256, POW_256_12_U256, POW_256_13_U256, POW_256_14_U256, POW_256_15_U256, POW_256_16_U256, }; +use keccak::u128_split; /// Ceils a number of bits to the next word (32 bytes) @@ -258,3 +259,21 @@ impl SpanExtension of SpanExtensionTrait { } } + +#[generate_trait] +impl U256Impl of U256Trait { + /// Splits an u256 into 4 little endian u64. + /// Returns ((high_high, high_low),(low_high, low_low)) + fn split_into_u64_le(self: u256) -> ((u64, u64), (u64, u64)) { + let low_le = integer::u128_byte_reverse(self.low); + let high_le = integer::u128_byte_reverse(self.high); + (u128_split(high_le), u128_split(low_le)) + } + + /// Reverse the endianness of an u256 + fn reverse_endianness(self: u256) -> u256 { + let new_low = integer::u128_byte_reverse(self.high); + let new_high = integer::u128_byte_reverse(self.low); + u256 { low: new_low, high: new_high } + } +} diff --git a/crates/utils/src/tests/test_helpers.cairo b/crates/utils/src/tests/test_helpers.cairo index da8386e45..0ca01e212 100644 --- a/crates/utils/src/tests/test_helpers.cairo +++ b/crates/utils/src/tests/test_helpers.cairo @@ -1,5 +1,7 @@ use utils::helpers; -use utils::helpers::{SpanExtension, SpanExtensionTrait, ArrayExtension, ArrayExtensionTrait}; +use utils::helpers::{ + SpanExtension, SpanExtensionTrait, ArrayExtension, ArrayExtensionTrait, U256Trait +}; use debug::PrintTrait; #[test] @@ -180,3 +182,25 @@ fn test_append_n() { // Then assert(original == array![1, 2, 3, 4, 9, 9, 9], 'append_n failed'); } + +#[test] +#[available_gas(2000000000)] +fn test_reverse_bytes_u256() { + let value: u256 = 0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000; + let res = value.reverse_endianness(); + assert( + res == 0x0000450000DEFA0000200400000000ADDE00000077000000E5000000FFFFFFFA, + 'reverse mismatch' + ); +} + +#[test] +#[available_gas(2000000000)] +fn test_split_u256_into_u64_little() { + let value: u256 = 0xFAFFFFFF000000E500000077000000DEAD0000000004200000FADE0000450000; + let ((high_h, low_h), (high_l, low_l)) = value.split_into_u64_le(); + assert(high_h == 0xDE00000077000000, 'split mismatch'); + assert(low_h == 0xE5000000FFFFFFFA, 'split mismatch'); + assert(high_l == 0x0000450000DEFA00, 'split mismatch'); + assert(low_l == 0x00200400000000AD, 'split mismatch'); +}