diff --git a/crates/vm/levm/src/gas_cost.rs b/crates/vm/levm/src/gas_cost.rs index 5a4333dd2..3f188e9f3 100644 --- a/crates/vm/levm/src/gas_cost.rs +++ b/crates/vm/levm/src/gas_cost.rs @@ -187,6 +187,8 @@ pub const ECMUL_COST: u64 = 6000; pub const ECPAIRING_BASE_COST: u64 = 45000; pub const ECPAIRING_GROUP_COST: u64 = 34000; +pub const BLAKE2F_ROUND_COST: u64 = 1; + pub fn exp(exponent: U256) -> Result { let exponent_byte_size = (exponent .bits() diff --git a/crates/vm/levm/src/precompiles.rs b/crates/vm/levm/src/precompiles.rs index b37eaa5da..289b0e5ea 100644 --- a/crates/vm/levm/src/precompiles.rs +++ b/crates/vm/levm/src/precompiles.rs @@ -26,7 +26,9 @@ use sha3::Digest; use crate::{ call_frame::CallFrame, errors::{InternalError, PrecompileError, VMError}, - gas_cost::{self, ECADD_COST, ECMUL_COST, ECRECOVER_COST, MODEXP_STATIC_COST}, + gas_cost::{ + self, BLAKE2F_ROUND_COST, ECADD_COST, ECMUL_COST, ECRECOVER_COST, MODEXP_STATIC_COST, + }, }; pub const ECRECOVER_ADDRESS: H160 = H160([ @@ -83,6 +85,8 @@ pub const PRECOMPILES: [H160; 10] = [ POINT_EVALUATION_ADDRESS, ]; +pub const BLAKE2F_ELEMENT_SIZE: usize = 8; + pub fn is_precompile(callee_address: &Address) -> bool { PRECOMPILES.contains(callee_address) } @@ -678,12 +682,206 @@ fn update_pairing_result( Ok(()) } -fn blake2f( - _calldata: &Bytes, - _gas_for_call: u64, - _consumed_gas: &mut u64, +// Message word schedule permutations for each round are defined by SIGMA constant. +// Extracted from https://datatracker.ietf.org/doc/html/rfc7693#section-2.7 +pub const SIGMA: [[usize; 16]; 10] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], +]; + +// Initialization vector, used to initialize the work vector +// Extracted from https://datatracker.ietf.org/doc/html/rfc7693#appendix-C.2 +pub const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, +]; + +// Rotation constants, used in g +// Extracted from https://datatracker.ietf.org/doc/html/rfc7693#section-2.1 +const R1: u32 = 32; +const R2: u32 = 24; +const R3: u32 = 16; +const R4: u32 = 63; + +// The G primitive function mixes two input words, "x" and "y", into +// four words indexed by "a", "b", "c", and "d" in the working vector +// v[0..15]. The full modified vector is returned. +// Based on https://datatracker.ietf.org/doc/html/rfc7693#section-3.1 +#[allow(clippy::indexing_slicing)] +fn g(v: [u64; 16], a: usize, b: usize, c: usize, d: usize, x: u64, y: u64) -> [u64; 16] { + let mut ret = v; + ret[a] = v[a].wrapping_add(v[b]).wrapping_add(x); + ret[d] = (ret[d] ^ ret[a]).rotate_right(R1); + ret[c] = ret[c].wrapping_add(ret[d]); + ret[b] = (ret[b] ^ ret[c]).rotate_right(R2); + ret[a] = ret[a].wrapping_add(ret[b]).wrapping_add(y); + ret[d] = (ret[d] ^ ret[a]).rotate_right(R3); + ret[c] = ret[c].wrapping_add(ret[d]); + ret[b] = (ret[b] ^ ret[c]).rotate_right(R4); + + ret +} + +// Perform the permutations on the work vector +#[allow(clippy::indexing_slicing)] +fn word_permutation(rounds_to_permute: usize, v: [u64; 16], m: &[u64; 16]) -> [u64; 16] { + let mut ret = v; + + for i in 0..rounds_to_permute { + // Message word selection permutation for each round. + + let s: &[usize; 16] = &SIGMA[i % 10]; + + ret = g(ret, 0, 4, 8, 12, m[s[0]], m[s[1]]); + ret = g(ret, 1, 5, 9, 13, m[s[2]], m[s[3]]); + ret = g(ret, 2, 6, 10, 14, m[s[4]], m[s[5]]); + ret = g(ret, 3, 7, 11, 15, m[s[6]], m[s[7]]); + + ret = g(ret, 0, 5, 10, 15, m[s[8]], m[s[9]]); + ret = g(ret, 1, 6, 11, 12, m[s[10]], m[s[11]]); + ret = g(ret, 2, 7, 8, 13, m[s[12]], m[s[13]]); + ret = g(ret, 3, 4, 9, 14, m[s[14]], m[s[15]]); + } + + ret +} + +// Based on https://datatracker.ietf.org/doc/html/rfc7693#section-3.2 +fn blake2f_compress_f( + rounds: usize, + h: [u64; 8], + m: &[u64; 16], + t: &[u64; 2], + f: bool, +) -> Result<[u64; 8], InternalError> { + // Initialize local work vector v[0..15], takes first half from state and second half from IV. + let mut v: [u64; 16] = [0; 16]; + v[0..8].copy_from_slice(&h); + v[8..16].copy_from_slice(&IV); + + v[12] ^= t[0]; // Low word of the offset + v[13] ^= t[1]; // High word of the offset + + // If final block flag is true, invert all bits + if f { + v[14] = !v[14]; + } + + v = word_permutation(rounds, v, m); + + let mut output = [0; 8]; + + // XOR the two halves, put the results in the output slice + for (i, pos) in output.iter_mut().enumerate() { + *pos = h.get(i).ok_or(InternalError::SlicingError)? + ^ v.get(i).ok_or(InternalError::SlicingError)? + ^ v.get(i.overflowing_add(8).0) + .ok_or(InternalError::SlicingError)?; + } + + Ok(output) +} + +// Reads a part of the calldata and returns what is read as u64 or an error +fn read_bytes_from_offset(calldata: &Bytes, offset: usize, index: usize) -> Result { + let index_start = (index + .checked_mul(BLAKE2F_ELEMENT_SIZE) + .ok_or(PrecompileError::ParsingInputError)?) + .checked_add(offset) + .ok_or(PrecompileError::ParsingInputError)?; + let index_end = index_start + .checked_add(BLAKE2F_ELEMENT_SIZE) + .ok_or(PrecompileError::ParsingInputError)?; + + Ok(u64::from_le_bytes( + calldata + .get(index_start..index_end) + .ok_or(InternalError::SlicingError)? + .try_into() + .map_err(|_| PrecompileError::ParsingInputError)?, + )) +} + +type SliceArguments = ([u64; 8], [u64; 16], [u64; 2]); + +fn parse_slice_arguments(calldata: &Bytes) -> Result { + let mut h = [0; 8]; + for i in 0..8_usize { + let data_read = read_bytes_from_offset(calldata, 4, i)?; + + let read_slice = h.get_mut(i).ok_or(InternalError::SlicingError)?; + *read_slice = data_read; + } + + let mut m = [0; 16]; + for i in 0..16_usize { + let data_read = read_bytes_from_offset(calldata, 68, i)?; + + let read_slice = m.get_mut(i).ok_or(InternalError::SlicingError)?; + *read_slice = data_read; + } + + let mut t = [0; 2]; + for i in 0..2_usize { + let data_read = read_bytes_from_offset(calldata, 196, i)?; + + let read_slice = t.get_mut(i).ok_or(InternalError::SlicingError)?; + *read_slice = data_read; + } + + Ok((h, m, t)) +} + +pub fn blake2f( + calldata: &Bytes, + gas_for_call: u64, + consumed_gas: &mut u64, ) -> Result { - Ok(Bytes::new()) + if calldata.len() != 213 { + return Err(VMError::PrecompileError(PrecompileError::ParsingInputError)); + } + + let rounds: U256 = calldata + .get(0..4) + .ok_or(InternalError::SlicingError)? + .into(); + + let rounds: usize = rounds + .try_into() + .map_err(|_| PrecompileError::ParsingInputError)?; + + let gas_cost = + u64::try_from(rounds).map_err(|_| InternalError::ConversionError)? * BLAKE2F_ROUND_COST; + increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?; + + let (h, m, t) = parse_slice_arguments(calldata)?; + + let f = calldata.get(212).ok_or(InternalError::SlicingError)?; + if *f != 0 && *f != 1 { + return Err(VMError::PrecompileError(PrecompileError::ParsingInputError)); + } + let f = *f == 1; + + let result = blake2f_compress_f(rounds, h, &m, &t, f)?; + + // map the result to the output format (from a u64 slice to a u8 one) + let output: Vec = result.iter().flat_map(|num| num.to_le_bytes()).collect(); + + Ok(Bytes::from(output)) } fn point_evaluation( diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index e3b45c670..5d1d7a43f 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -9,13 +9,16 @@ use ethrex_levm::{ db::{cache, CacheDB, Db}, errors::{OutOfGasError, TxResult, VMError}, gas_cost::{ - self, ECADD_COST, ECMUL_COST, ECPAIRING_BASE_COST, ECPAIRING_GROUP_COST, ECRECOVER_COST, - IDENTITY_DYNAMIC_BASE, IDENTITY_STATIC_COST, MODEXP_STATIC_COST, RIPEMD_160_DYNAMIC_BASE, - RIPEMD_160_STATIC_COST, SHA2_256_DYNAMIC_BASE, SHA2_256_STATIC_COST, + self, BLAKE2F_ROUND_COST, ECADD_COST, ECMUL_COST, ECPAIRING_BASE_COST, + ECPAIRING_GROUP_COST, ECRECOVER_COST, IDENTITY_DYNAMIC_BASE, IDENTITY_STATIC_COST, + MODEXP_STATIC_COST, RIPEMD_160_DYNAMIC_BASE, RIPEMD_160_STATIC_COST, SHA2_256_DYNAMIC_BASE, + SHA2_256_STATIC_COST, }, memory, operations::Operation, - precompiles::{ecadd, ecmul, ecpairing, ecrecover, identity, modexp, ripemd_160, sha2_256}, + precompiles::{ + blake2f, ecadd, ecmul, ecpairing, ecrecover, identity, modexp, ripemd_160, sha2_256, + }, utils::{new_vm_with_ops, new_vm_with_ops_addr_bal_db, new_vm_with_ops_db, ops_to_bytecode}, vm::{word_to_address, Storage, VM}, Environment, @@ -4643,3 +4646,33 @@ fn ecpairing_test() { (ECPAIRING_BASE_COST + ECPAIRING_GROUP_COST * 2) ); } + +#[test] +fn blake2f_test() { + // Source: https://eips.ethereum.org/EIPS/eip-152#test-vector-5 + let calldata = hex::decode("0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = blake2f(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab92386edd4009923").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, 12 * BLAKE2F_ROUND_COST); +} + +#[test] +fn blake2f_test_2() { + // Inspired in tuple (23, 0, 0) of GeneralStateTests/stPreCompiledContracts/blake2B.json + let calldata = hex::decode("0000001048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b616162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8000000000000000300000000000000000000000000000001").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = blake2f(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("7df6f69476a03ae29e944814846460b058d1762fffe77f938ea723d1033de0d5bb1f8234bd73afaf955622fa2cdde95594577a8d53191908eb69b316a53c985b").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, 16 * BLAKE2F_ROUND_COST); +}