diff --git a/Cargo.lock b/Cargo.lock index 524fc8aee..a2417a7c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2191,6 +2191,7 @@ dependencies = [ "hex", "keccak-hash 0.11.0", "libsecp256k1", + "num-bigint 0.4.6", "ripemd", "serde", "serde_json", diff --git a/crates/vm/levm/Cargo.toml b/crates/vm/levm/Cargo.toml index b6494e5d5..cb4ad6571 100644 --- a/crates/vm/levm/Cargo.toml +++ b/crates/vm/levm/Cargo.toml @@ -18,6 +18,7 @@ thiserror = "2.0.3" libsecp256k1 = "0.7.1" sha2 = "0.10.8" ripemd = "0.1.3" +num-bigint = "0.4.5" [dev-dependencies] hex = "0.4.3" diff --git a/crates/vm/levm/src/gas_cost.rs b/crates/vm/levm/src/gas_cost.rs index be2ec78d2..7faadcae0 100644 --- a/crates/vm/levm/src/gas_cost.rs +++ b/crates/vm/levm/src/gas_cost.rs @@ -7,6 +7,7 @@ use crate::{ use bytes::Bytes; /// Contains the gas costs of the EVM instructions use ethrex_core::U256; +use num_bigint::BigUint; // Opcodes cost pub const STOP: u64 = 0; @@ -176,8 +177,7 @@ pub const RIPEMD_160_DYNAMIC_BASE: u64 = 120; pub const IDENTITY_STATIC_COST: u64 = 15; pub const IDENTITY_DYNAMIC_BASE: u64 = 3; -pub const MODEXP_STATIC_COST: u64 = 0; -pub const MODEXP_DYNAMIC_BASE: u64 = 200; +pub const MODEXP_STATIC_COST: u64 = 200; pub const MODEXP_DYNAMIC_QUOTIENT: u64 = 3; pub fn exp(exponent: U256) -> Result { @@ -823,67 +823,59 @@ pub fn identity(data_size: usize) -> Result { } pub fn modexp( - exponent: U256, - base_size: u64, - exponent_size: u64, - modulus_size: u64, + exponent: &BigUint, + base_size: usize, + exponent_size: usize, + modulus_size: usize, ) -> Result { + let base_size: u64 = base_size + .try_into() + .map_err(|_| PrecompileError::ParsingInputError)?; + let exponent_size: u64 = exponent_size + .try_into() + .map_err(|_| PrecompileError::ParsingInputError)?; + let modulus_size: u64 = modulus_size + .try_into() + .map_err(|_| PrecompileError::ParsingInputError)?; + let max_length = base_size.max(modulus_size); let words = (max_length .checked_add(7) .ok_or(OutOfGasError::GasCostOverflow)?) - / WORD_SIZE_IN_BYTES_U64; + .checked_div(8) + .ok_or(InternalError::DivisionError)?; + let multiplication_complexity = words.checked_pow(2).ok_or(OutOfGasError::GasCostOverflow)?; - let mut iteration_count: u64 = 0; - if exponent_size <= WORD_SIZE_IN_BYTES_U64 && exponent.is_zero() { - iteration_count = 0; - } else if exponent_size <= WORD_SIZE_IN_BYTES_U64 { - iteration_count = exponent + let iteration_count = if exponent_size <= 32 && *exponent != BigUint::ZERO { + exponent .bits() .checked_sub(1) .ok_or(InternalError::ArithmeticOperationUnderflow)? - .try_into() - .map_err(|_| InternalError::ConversionError)?; - } else if exponent_size > WORD_SIZE_IN_BYTES_U64 { - iteration_count = 8u64 - .checked_mul( - exponent_size - .checked_sub(WORD_SIZE_IN_BYTES_U64) - .ok_or(InternalError::ArithmeticOperationUnderflow)?, - ) - .ok_or(InternalError::ArithmeticOperationOverflow)? - .checked_add( - (exponent - & (2usize - .checked_pow(256) - .ok_or(InternalError::ArithmeticOperationOverflow)?) - .checked_sub(1) - .ok_or(InternalError::ArithmeticOperationOverflow)? - .into()) - .bits() - .checked_sub(1) - .ok_or(InternalError::ArithmeticOperationUnderflow)? - .try_into() - .map_err(|_| InternalError::ConversionError)?, - ) - .ok_or(InternalError::ArithmeticOperationOverflow)?; - } - + } else if exponent_size > 32 { + let extra_size = (exponent_size + .checked_sub(32) + .ok_or(InternalError::ArithmeticOperationUnderflow)?) + .checked_mul(8) + .ok_or(OutOfGasError::GasCostOverflow)?; + extra_size + .checked_add(exponent.bits().max(1)) + .ok_or(OutOfGasError::GasCostOverflow)? + .checked_sub(1) + .ok_or(InternalError::ArithmeticOperationUnderflow)? + } else { + 0 + }; let calculate_iteration_count = iteration_count.max(1); - let static_gas = MODEXP_STATIC_COST; - - let dynamic_gas = MODEXP_DYNAMIC_BASE.max( + let cost = MODEXP_STATIC_COST.max( multiplication_complexity .checked_mul(calculate_iteration_count) .ok_or(OutOfGasError::GasCostOverflow)? / MODEXP_DYNAMIC_QUOTIENT, ); - Ok(static_gas - .checked_add(dynamic_gas) - .ok_or(OutOfGasError::GasCostOverflow)?) + Ok(cost) } fn precompile(data_size: usize, static_cost: u64, dynamic_base: u64) -> Result { diff --git a/crates/vm/levm/src/precompiles.rs b/crates/vm/levm/src/precompiles.rs index d793cb741..d7148d355 100644 --- a/crates/vm/levm/src/precompiles.rs +++ b/crates/vm/levm/src/precompiles.rs @@ -2,12 +2,13 @@ use bytes::Bytes; use ethrex_core::{Address, H160, U256}; use keccak_hash::keccak256; use libsecp256k1::{self, Message, RecoveryId, Signature}; +use num_bigint::BigUint; use sha3::Digest; use crate::{ call_frame::CallFrame, - errors::{InternalError, PrecompileError, VMError}, - gas_cost::{ripemd_160 as ripemd_160_cost, sha2_256 as sha2_256_cost, ECRECOVER_COST}, + errors::{InternalError, OutOfGasError, PrecompileError, VMError}, + gas_cost::{self, ECRECOVER_COST, MODEXP_STATIC_COST}, }; pub const ECRECOVER_ADDRESS: H160 = H160([ @@ -118,21 +119,19 @@ fn increase_precompile_consumed_gas( /// When slice length is less than 128, the rest is filled with zeros. If slice length is /// more than 128 the excess bytes are discarded. -fn fill_with_zeros(slice: &[u8]) -> Result<[u8; 128], VMError> { - let mut result = [0; 128]; - - let n = slice.len().min(128); - - let trimmed_slice: &[u8] = slice.get(..n).unwrap_or_default(); - - for i in 0..n { - let byte: &mut u8 = result.get_mut(i).ok_or(InternalError::SlicingError)?; - *byte = *trimmed_slice.get(i).ok_or(InternalError::SlicingError)?; +fn fill_with_zeros(calldata: &Bytes, target_len: usize) -> Result { + let mut padded_calldata = calldata.to_vec(); + if padded_calldata.len() < target_len { + let size_diff = target_len + .checked_sub(padded_calldata.len()) + .ok_or(InternalError::ArithmeticOperationUnderflow)?; + padded_calldata.extend(vec![0u8; size_diff]); } - - Ok(result) + Ok(padded_calldata.into()) } +/// ECDSA (Elliptic curve digital signature algorithm) public key recovery function. +/// Given a hash, a Signature and a recovery Id, returns the public key recovered by secp256k1 pub fn ecrecover( calldata: &Bytes, gas_for_call: u64, @@ -143,7 +142,7 @@ pub fn ecrecover( increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?; // If calldata does not reach the required length, we should fill the rest with zeros - let calldata = fill_with_zeros(calldata)?; + let calldata = fill_with_zeros(calldata, 128)?; // Parse the input elements, first as a slice of bytes and then as an specific type of the crate let hash = calldata.get(0..32).ok_or(InternalError::SlicingError)?; @@ -191,20 +190,25 @@ pub fn ecrecover( Ok(Bytes::from(output.to_vec())) } -fn identity( - _calldata: &Bytes, - _gas_for_call: u64, - _consumed_gas: &mut u64, +pub fn identity( + calldata: &Bytes, + gas_for_call: u64, + consumed_gas: &mut u64, ) -> Result { - Ok(Bytes::new()) + let gas_cost = gas_cost::identity(calldata.len())?; + + increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?; + + Ok(calldata.clone()) } +/// Returns the calldata hashed by sha2-256 algorithm pub fn sha2_256( calldata: &Bytes, gas_for_call: u64, consumed_gas: &mut u64, ) -> Result { - let gas_cost = sha2_256_cost(calldata.len())?; + let gas_cost = gas_cost::sha2_256(calldata.len())?; increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?; @@ -213,12 +217,13 @@ pub fn sha2_256( Ok(Bytes::from(result)) } +/// Returns the calldata hashed by ripemd-160 algorithm, padded by zeros at left pub fn ripemd_160( calldata: &Bytes, gas_for_call: u64, consumed_gas: &mut u64, ) -> Result { - let gas_cost = ripemd_160_cost(calldata.len())?; + let gas_cost = gas_cost::ripemd_160(calldata.len())?; increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?; @@ -232,12 +237,123 @@ pub fn ripemd_160( Ok(Bytes::from(output)) } -fn modexp( - _calldata: &Bytes, - _gas_for_call: u64, - _consumed_gas: &mut u64, +pub fn modexp( + calldata: &Bytes, + gas_for_call: u64, + consumed_gas: &mut u64, ) -> Result { - Ok(Bytes::new()) + // If calldata does not reach the required length, we should fill the rest with zeros + let calldata = fill_with_zeros(calldata, 96)?; + + let b_size: U256 = calldata + .get(0..32) + .ok_or(PrecompileError::ParsingInputError)? + .into(); + + let e_size: U256 = calldata + .get(32..64) + .ok_or(PrecompileError::ParsingInputError)? + .into(); + + let m_size: U256 = calldata + .get(64..96) + .ok_or(PrecompileError::ParsingInputError)? + .into(); + + if b_size == U256::zero() && m_size == U256::zero() { + *consumed_gas = consumed_gas + .checked_add(MODEXP_STATIC_COST) + .ok_or(OutOfGasError::ConsumedGasOverflow)?; + return Ok(Bytes::new()); + } + + // Because on some cases conversions to usize exploded before the check of the zero value could be done + let b_size = usize::try_from(b_size).map_err(|_| PrecompileError::ParsingInputError)?; + let e_size = usize::try_from(e_size).map_err(|_| PrecompileError::ParsingInputError)?; + let m_size = usize::try_from(m_size).map_err(|_| PrecompileError::ParsingInputError)?; + + let base_limit = b_size + .checked_add(96) + .ok_or(InternalError::ArithmeticOperationOverflow)?; + + let exponent_limit = e_size + .checked_add(base_limit) + .ok_or(InternalError::ArithmeticOperationOverflow)?; + + // The reason I use unwrap_or_default is to cover the case where calldata does not reach the required + // length, so then we should fill the rest with zeros. The same is done in modulus parsing + let b = get_slice_or_default(&calldata, 96, base_limit, b_size)?; + let base = BigUint::from_bytes_be(&b); + + let e = get_slice_or_default(&calldata, base_limit, exponent_limit, e_size)?; + let exponent = BigUint::from_bytes_be(&e); + + let m = match calldata.get(exponent_limit..) { + Some(m) => { + let m_extended = fill_with_zeros(&Bytes::from(m.to_vec()), m_size)?; + m_extended.get(..m_size).unwrap_or_default().to_vec() + } + None => Default::default(), + }; + let modulus = BigUint::from_bytes_be(&m); + + let gas_cost = gas_cost::modexp(&exponent, b_size, e_size, m_size)?; + increase_precompile_consumed_gas(gas_for_call, gas_cost, consumed_gas)?; + + let result = mod_exp(base, exponent, modulus); + + let res_bytes = result.to_bytes_be(); + let res_bytes = increase_left_pad(&Bytes::from(res_bytes), m_size)?; + + Ok(res_bytes.slice(..m_size)) +} + +fn get_slice_or_default( + calldata: &Bytes, + lower_limit: usize, + upper_limit: usize, + size_to_expand: usize, +) -> Result, VMError> { + match calldata.get(lower_limit..upper_limit) { + Some(e) => { + let e_extended = fill_with_zeros(&Bytes::from(e.to_vec()), size_to_expand)?; + Ok(e_extended + .get(..size_to_expand) + .unwrap_or_default() + .to_vec()) + } + None => Ok(Default::default()), + } +} + +/// I allow this clippy alert because in the code modulus could never be +/// zero because that case is covered in the if above that line +#[allow(clippy::arithmetic_side_effects)] +fn mod_exp(base: BigUint, exponent: BigUint, modulus: BigUint) -> BigUint { + if modulus == BigUint::ZERO { + BigUint::ZERO + } else if exponent == BigUint::ZERO { + BigUint::from(1_u8) % modulus + } else { + base.modpow(&exponent, &modulus) + } +} + +pub fn increase_left_pad(result: &Bytes, m_size: usize) -> Result { + let mut padded_result = vec![0u8; m_size]; + if result.len() < m_size { + let size_diff = m_size + .checked_sub(result.len()) + .ok_or(InternalError::ArithmeticOperationUnderflow)?; + padded_result + .get_mut(size_diff..) + .ok_or(InternalError::SlicingError)? + .copy_from_slice(result); + + Ok(padded_result.into()) + } else { + Ok(result.clone()) + } } fn ecadd(_calldata: &Bytes, _gas_for_call: u64, _consumed_gas: &mut u64) -> Result { diff --git a/crates/vm/levm/tests/tests.rs b/crates/vm/levm/tests/tests.rs index 9d279e8b8..fe4b6fa5d 100644 --- a/crates/vm/levm/tests/tests.rs +++ b/crates/vm/levm/tests/tests.rs @@ -9,12 +9,13 @@ use ethrex_levm::{ db::{cache, CacheDB, Db}, errors::{OutOfGasError, TxResult, VMError}, gas_cost::{ - self, ECRECOVER_COST, RIPEMD_160_DYNAMIC_BASE, RIPEMD_160_STATIC_COST, - SHA2_256_DYNAMIC_BASE, SHA2_256_STATIC_COST, + self, 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::{ecrecover, ripemd_160, sha2_256}, + precompiles::{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, @@ -4534,3 +4535,48 @@ fn ripemd_160_test() { (RIPEMD_160_STATIC_COST + RIPEMD_160_DYNAMIC_BASE) ); } + +#[test] +fn identity_test() { + let calldata = hex::decode("ff").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = identity(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("ff").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, IDENTITY_STATIC_COST + IDENTITY_DYNAMIC_BASE); +} + +#[test] +fn modexp_test() { + let calldata = hex::decode("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000108090a").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = modexp(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from(hex::decode("08").unwrap()); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, MODEXP_STATIC_COST); +} + +#[test] +fn modexp_test_2() { + // This tests that in case of the sizes read first are bigger than the calldata len then the calldata is filled with zeros + let calldata = hex::decode("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002003ffff80").unwrap(); + let calldata = Bytes::from(calldata); + + let mut consumed_gas = 0; + let result = modexp(&calldata, 10000, &mut consumed_gas).unwrap(); + + let expected_result = Bytes::from( + hex::decode("3b01b01ac41f2d6e917c6d6a221ce793802469026d9ab7578fa2e79e4da6aaab").unwrap(), + ); + + assert_eq!(result, expected_result); + assert_eq!(consumed_gas, MODEXP_STATIC_COST); +}