Skip to content

Commit

Permalink
fix(levm): pre_merge_EFTest (#1663)
Browse files Browse the repository at this point in the history
**Motivation**

<!-- Why does this pull request exist? What are its goals? -->
This pull request exists to implement and refine the modular
exponentiation (modexp) precompile functionality as specified in
EIP-198. The goal is to ensure compatibility with Ethereum Foundation
(EF) tests across various forks and pass pre-merge validation for the
functionality.
**Description**
To implement EIP-198, the following changes were made:
• EIP-198: Added support for the ModExp precompile to pass tests from
the eip198_modexp_precompile test directory. Current progress:140/144
(97.22%).
• EIP-2028: Adjusted calldata gas cost, reducing the gas per non-zero
byte from 68 to 16 while keeping the gas cost of zero bytes unchanged.
This change was necessary to align with the requirements of the
precompile implementation.
• EIP-2929: Updated gas cost for CALL opcode to 700 to address
compatibility issues for Istanbul tests. This hardcoded adjustment was
required to ensure the precompile functions correctly in forks such as
Istanbul.

To monitor test progress, run the following command:
```Bash
cargo test --release -p ef_tests-levm --test ef_tests_levm -- --tests modexp.json  
```
<!-- A clear and concise general description of the changes this PR
introduces -->

---------

Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com>
  • Loading branch information
LucasUTNFRD and ilitteri authored Jan 9, 2025
1 parent 6c3701a commit ae00775
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 67 deletions.
182 changes: 139 additions & 43 deletions crates/vm/levm/src/gas_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ pub const EXTCODECOPY_WARM_DYNAMIC: u64 = DEFAULT_WARM_DYNAMIC;
pub const CALL_STATIC: u64 = DEFAULT_STATIC;
pub const CALL_COLD_DYNAMIC: u64 = DEFAULT_COLD_DYNAMIC;
pub const CALL_WARM_DYNAMIC: u64 = DEFAULT_WARM_DYNAMIC;
pub const CALL_PRE_BERLIN: u64 = 700;
pub const CALL_POSITIVE_VALUE: u64 = 9000;
pub const CALL_POSITIVE_VALUE_STIPEND: u64 = 2300;
pub const CALL_TO_EMPTY_ACCOUNT: u64 = 25000;
Expand Down Expand Up @@ -158,6 +159,7 @@ pub const CREATE_BASE_COST: u64 = 32000;
// Calldata costs
pub const CALLDATA_COST_ZERO_BYTE: u64 = 4;
pub const CALLDATA_COST_NON_ZERO_BYTE: u64 = 16;
pub const CALLDATA_COST_NON_ZERO_BYTE_PRE_ISTANBUL: u64 = 68;

// Blob gas costs
pub const BLOB_GAS_PER_BLOB: u64 = 131072;
Expand All @@ -182,6 +184,8 @@ pub const MODEXP_STATIC_COST: u64 = 200;
pub const MODEXP_DYNAMIC_BASE: u64 = 200;
pub const MODEXP_DYNAMIC_QUOTIENT: u64 = 3;

pub const MODEXP_DYNAMIC_QUOTIENT_PRE_BERLIN: u64 = 20;

pub const ECADD_COST: u64 = 150;
pub const ECMUL_COST: u64 = 6000;

Expand Down Expand Up @@ -524,19 +528,26 @@ pub fn selfdestruct(
Ok(gas_cost)
}

pub fn tx_calldata(calldata: &Bytes) -> Result<u64, OutOfGasError> {
pub fn tx_calldata(calldata: &Bytes, spec_id: SpecId) -> Result<u64, OutOfGasError> {
// This cost applies both for call and create
// 4 gas for each zero byte in the transaction data 16 gas for each non-zero byte in the transaction.
let mut calldata_cost: u64 = 0;
for byte in calldata {
if *byte != 0 {
calldata_cost = calldata_cost
.checked_add(CALLDATA_COST_NON_ZERO_BYTE)
.ok_or(OutOfGasError::GasUsedOverflow)?;
calldata_cost = if *byte != 0 {
if spec_id >= SpecId::ISTANBUL {
calldata_cost
.checked_add(CALLDATA_COST_NON_ZERO_BYTE)
.ok_or(OutOfGasError::GasUsedOverflow)?
} else {
// EIP-2028
calldata_cost
.checked_add(CALLDATA_COST_NON_ZERO_BYTE_PRE_ISTANBUL)
.ok_or(OutOfGasError::GasUsedOverflow)?
}
} else {
calldata_cost = calldata_cost
calldata_cost
.checked_add(CALLDATA_COST_ZERO_BYTE)
.ok_or(OutOfGasError::GasUsedOverflow)?;
.ok_or(OutOfGasError::GasUsedOverflow)?
}
}
Ok(calldata_cost)
Expand Down Expand Up @@ -629,6 +640,7 @@ pub fn extcodehash(address_was_cold: bool) -> Result<u64, VMError> {
)
}

#[allow(clippy::too_many_arguments)]
pub fn call(
new_memory_size: usize,
current_memory_size: usize,
Expand All @@ -637,14 +649,20 @@ pub fn call(
value_to_transfer: U256,
gas_from_stack: U256,
gas_left: u64,
spec_id: SpecId,
) -> Result<(u64, u64), VMError> {
let memory_expansion_cost = memory::expansion_cost(new_memory_size, current_memory_size)?;

let address_access_cost = address_access_cost(
address_was_cold,
CALL_STATIC,
CALL_COLD_DYNAMIC,
CALL_WARM_DYNAMIC,
if spec_id >= SpecId::BERLIN {
CALL_WARM_DYNAMIC
} else {
//https://eips.ethereum.org/EIPS/eip-2929
CALL_PRE_BERLIN
},
)?;
let positive_value_cost = if !value_to_transfer.is_zero() {
CALL_POSITIVE_VALUE
Expand Down Expand Up @@ -803,11 +821,120 @@ pub fn identity(data_size: usize) -> Result<u64, VMError> {
precompile(data_size, IDENTITY_STATIC_COST, IDENTITY_DYNAMIC_BASE)
}

//https://eips.ethereum.org/EIPS/eip-2565
pub fn modexp_eip2565(
max_length: u64,
exponent_first_32_bytes: &BigUint,
exponent_size: u64,
) -> Result<u64, VMError> {
let words = (max_length
.checked_add(7)
.ok_or(OutOfGasError::GasCostOverflow)?)
.checked_div(8)
.ok_or(InternalError::DivisionError)?;
let multiplication_complexity = words.checked_pow(2).ok_or(OutOfGasError::GasCostOverflow)?;

let calculate_iteration_count =
if exponent_size <= 32 && *exponent_first_32_bytes != BigUint::ZERO {
exponent_first_32_bytes
.bits()
.checked_sub(1)
.ok_or(InternalError::ArithmeticOperationUnderflow)?
} 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_first_32_bytes.bits().max(1))
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_sub(1)
.ok_or(InternalError::ArithmeticOperationUnderflow)?
} else {
0
}
.max(1);

let cost = MODEXP_STATIC_COST.max(
multiplication_complexity
.checked_mul(calculate_iteration_count)
.ok_or(OutOfGasError::GasCostOverflow)?
/ MODEXP_DYNAMIC_QUOTIENT,
);
Ok(cost)
}

//https://eips.ethereum.org/EIPS/eip-198
pub fn modexp_eip198(
max_length: u64,
exponent_first_32_bytes: &BigUint,
exponent_size: u64,
) -> Result<u64, VMError> {
let multiplication_complexity = if max_length <= 64 {
max_length
.checked_pow(2)
.ok_or(OutOfGasError::GasCostOverflow)?
} else if max_length <= 1024 {
max_length
.checked_pow(2)
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_div(4)
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_add(
max_length
.checked_mul(96)
.ok_or(OutOfGasError::GasCostOverflow)?,
)
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_sub(3072)
.ok_or(OutOfGasError::GasCostOverflow)?
} else {
max_length
.checked_pow(2)
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_div(16)
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_add(
max_length
.checked_mul(480)
.ok_or(OutOfGasError::GasCostOverflow)?,
)
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_sub(199680)
.ok_or(OutOfGasError::GasCostOverflow)?
};

let calculate_iteration_count = if exponent_size < 32 {
exponent_first_32_bytes.bits().saturating_sub(1)
} else {
let extra_size = (exponent_size
.checked_sub(32)
.ok_or(InternalError::ArithmeticOperationUnderflow)?)
.checked_mul(8)
.ok_or(OutOfGasError::GasCostOverflow)?;

let bits_part = exponent_first_32_bytes.bits().saturating_sub(1);

extra_size
.checked_add(bits_part)
.ok_or(OutOfGasError::GasCostOverflow)?
}
.max(1);

let cost = multiplication_complexity
.checked_mul(calculate_iteration_count)
.ok_or(OutOfGasError::GasCostOverflow)?
/ MODEXP_DYNAMIC_QUOTIENT_PRE_BERLIN;
Ok(cost)
}

pub fn modexp(
exponent_first_32_bytes: &BigUint,
base_size: usize,
exponent_size: usize,
modulus_size: usize,
spec_id: SpecId,
) -> Result<u64, VMError> {
let base_size: u64 = base_size
.try_into()
Expand All @@ -820,43 +947,12 @@ pub fn modexp(
.map_err(|_| PrecompileError::ParsingInputError)?;

let max_length = base_size.max(modulus_size);
let words = (max_length
.checked_add(7)
.ok_or(OutOfGasError::GasCostOverflow)?)
.checked_div(8)
.ok_or(InternalError::DivisionError)?;

let multiplication_complexity = words.checked_pow(2).ok_or(OutOfGasError::GasCostOverflow)?;

let iteration_count = if exponent_size <= 32 && *exponent_first_32_bytes != BigUint::ZERO {
exponent_first_32_bytes
.bits()
.checked_sub(1)
.ok_or(InternalError::ArithmeticOperationUnderflow)?
} 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_first_32_bytes.bits().max(1))
.ok_or(OutOfGasError::GasCostOverflow)?
.checked_sub(1)
.ok_or(InternalError::ArithmeticOperationUnderflow)?
if spec_id >= SpecId::BERLIN {
modexp_eip2565(max_length, exponent_first_32_bytes, exponent_size)
} else {
0
};
let calculate_iteration_count = iteration_count.max(1);

let cost = MODEXP_STATIC_COST.max(
multiplication_complexity
.checked_mul(calculate_iteration_count)
.ok_or(OutOfGasError::GasCostOverflow)?
/ MODEXP_DYNAMIC_QUOTIENT,
);

Ok(cost)
modexp_eip198(max_length, exponent_first_32_bytes, exponent_size)
}
}

fn precompile(data_size: usize, static_cost: u64, dynamic_base: u64) -> Result<u64, VMError> {
Expand Down
1 change: 1 addition & 0 deletions crates/vm/levm/src/opcode_handlers/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl VM {
value_to_transfer,
gas,
gas_left,
self.env.spec_id,
)?;
self.increase_consumed_gas(current_call_frame, cost)?;

Expand Down
53 changes: 34 additions & 19 deletions crates/vm/levm/src/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ pub fn is_precompile(callee_address: &Address, spec_id: SpecId) -> bool {
PRECOMPILES.contains(callee_address)
}

pub fn execute_precompile(current_call_frame: &mut CallFrame) -> Result<Bytes, VMError> {
pub fn execute_precompile(
current_call_frame: &mut CallFrame,
spec_id: SpecId,
) -> Result<Bytes, VMError> {
let callee_address = current_call_frame.code_address;
let calldata = current_call_frame.calldata.clone();
let gas_for_call = current_call_frame
Expand All @@ -123,7 +126,9 @@ pub fn execute_precompile(current_call_frame: &mut CallFrame) -> Result<Bytes, V
address if address == RIPEMD_160_ADDRESS => {
ripemd_160(&calldata, gas_for_call, consumed_gas)?
}
address if address == MODEXP_ADDRESS => modexp(&calldata, gas_for_call, consumed_gas)?,
address if address == MODEXP_ADDRESS => {
modexp(&calldata, gas_for_call, consumed_gas, spec_id)?
}
address if address == ECADD_ADDRESS => ecadd(&calldata, gas_for_call, consumed_gas)?,
address if address == ECMUL_ADDRESS => ecmul(&calldata, gas_for_call, consumed_gas)?,
address if address == ECPAIRING_ADDRESS => {
Expand Down Expand Up @@ -277,73 +282,83 @@ pub fn modexp(
calldata: &Bytes,
gas_for_call: u64,
consumed_gas: &mut u64,
spec_id: SpecId,
) -> Result<Bytes, VMError> {
// 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::from_big_endian(
let base_size = U256::from_big_endian(
calldata
.get(0..32)
.ok_or(PrecompileError::ParsingInputError)?,
);

let e_size = U256::from_big_endian(
let exponent_size = U256::from_big_endian(
calldata
.get(32..64)
.ok_or(PrecompileError::ParsingInputError)?,
);

let m_size = U256::from_big_endian(
let modulus_size = U256::from_big_endian(
calldata
.get(64..96)
.ok_or(PrecompileError::ParsingInputError)?,
);

if b_size == U256::zero() && m_size == U256::zero() {
if base_size == U256::zero() && modulus_size == U256::zero() {
increase_precompile_consumed_gas(gas_for_call, MODEXP_STATIC_COST, consumed_gas)?;
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_size = usize::try_from(base_size).map_err(|_| PrecompileError::ParsingInputError)?;
let exponent_size =
usize::try_from(exponent_size).map_err(|_| PrecompileError::ParsingInputError)?;
let modulus_size =
usize::try_from(modulus_size).map_err(|_| PrecompileError::ParsingInputError)?;

let base_limit = b_size
let base_limit = base_size
.checked_add(96)
.ok_or(InternalError::ArithmeticOperationOverflow)?;

let exponent_limit = e_size
let exponent_limit = exponent_size
.checked_add(base_limit)
.ok_or(InternalError::ArithmeticOperationOverflow)?;

let modulus_limit = m_size
let modulus_limit = modulus_size
.checked_add(exponent_limit)
.ok_or(InternalError::ArithmeticOperationOverflow)?;

let b = get_slice_or_default(&calldata, 96, base_limit, b_size)?;
let b = get_slice_or_default(&calldata, 96, base_limit, base_size)?;
let base = BigUint::from_bytes_be(&b);

let e = get_slice_or_default(&calldata, base_limit, exponent_limit, e_size)?;
let e = get_slice_or_default(&calldata, base_limit, exponent_limit, exponent_size)?;
let exponent = BigUint::from_bytes_be(&e);

let m = get_slice_or_default(&calldata, exponent_limit, modulus_limit, m_size)?;
let m = get_slice_or_default(&calldata, exponent_limit, modulus_limit, modulus_size)?;
let modulus = BigUint::from_bytes_be(&m);

// First 32 bytes of exponent or exponent if e_size < 32
let bytes_to_take = 32.min(e_size);
let bytes_to_take = 32.min(exponent_size);
// Use of unwrap_or_default because if e == 0 get_slice_or_default returns an empty vec
let exp_first_32 = BigUint::from_bytes_be(e.get(0..bytes_to_take).unwrap_or_default());

let gas_cost = gas_cost::modexp(&exp_first_32, b_size, e_size, m_size)?;
let gas_cost = gas_cost::modexp(
&exp_first_32,
base_size,
exponent_size,
modulus_size,
spec_id,
)?;

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)?;
let res_bytes = increase_left_pad(&Bytes::from(res_bytes), modulus_size)?;

Ok(res_bytes.slice(..m_size))
Ok(res_bytes.slice(..modulus_size))
}

/// This function returns the slice between the lower and upper limit of the calldata (as a vector),
Expand Down
Loading

0 comments on commit ae00775

Please sign in to comment.