Skip to content

Commit

Permalink
feat(levm): more precompiles implementation (#1522)
Browse files Browse the repository at this point in the history
**Motivation**

The goal is to implement some of the remaining precompiles.

**Description**

The precompiles implemented in this PR are `identity` and `modexp`.
There are around 100 tests not passing from folder
`eip198_modexp_precompile`, but those are not Cancun or Shangai tests,
but older.

---------

Co-authored-by: ilitteri <[email protected]>
  • Loading branch information
maximopalopoli and ilitteri authored Dec 23, 2024
1 parent 53c0829 commit 3465a1e
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 75 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/vm/levm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
82 changes: 37 additions & 45 deletions crates/vm/levm/src/gas_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<u64, VMError> {
Expand Down Expand Up @@ -823,67 +823,59 @@ pub fn identity(data_size: usize) -> Result<u64, VMError> {
}

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<u64, VMError> {
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<u64, VMError> {
Expand Down
170 changes: 143 additions & 27 deletions crates/vm/levm/src/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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<Bytes, VMError> {
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,
Expand All @@ -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)?;
Expand Down Expand Up @@ -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<Bytes, VMError> {
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<Bytes, VMError> {
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)?;

Expand All @@ -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<Bytes, VMError> {
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)?;

Expand All @@ -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<Bytes, VMError> {
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<Vec<u8>, 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<Bytes, VMError> {
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<Bytes, VMError> {
Expand Down
Loading

0 comments on commit 3465a1e

Please sign in to comment.