Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9986822
Refactor most opcodes and simplify stuff.
azteca1998 Oct 7, 2025
73b56db
Refactor system opcodes (except selfdestruct).
azteca1998 Oct 8, 2025
58e5dc6
Fix stuff.
azteca1998 Oct 8, 2025
391f730
Fix SSTORE gas.
azteca1998 Oct 8, 2025
7e46b1f
Make all handlers inline.
azteca1998 Oct 8, 2025
5d086d8
Fix stuff.
azteca1998 Oct 8, 2025
0ab8650
Fix stuff.
azteca1998 Oct 8, 2025
74effd2
Fix stuff.
azteca1998 Oct 8, 2025
7abf025
Fix stuff.
azteca1998 Oct 8, 2025
31c48f1
Fix stuff.
azteca1998 Oct 8, 2025
0aed2d7
Fix stuff.
azteca1998 Oct 8, 2025
e6593e4
Fix stuff.
azteca1998 Oct 8, 2025
481f4a7
Fix stuff.
azteca1998 Oct 8, 2025
b549281
Fix stuff.
azteca1998 Oct 8, 2025
eb83e91
Refactor last missing opcode.
azteca1998 Oct 8, 2025
350b3f6
Merge branch 'main' into refactor-opcode-handlers
azteca1998 Oct 9, 2025
f106360
Fix bug.
azteca1998 Oct 13, 2025
843a403
Fix EF tests's state runner.
azteca1998 Oct 13, 2025
db128fd
Merge branch 'main' into refactor-opcode-handlers
azteca1998 Oct 13, 2025
5da1598
Try to fix performance.
azteca1998 Oct 13, 2025
733d9a5
Merge branch 'main' into refactor-opcode-handlers
azteca1998 Nov 26, 2025
06d81c5
Merge branch 'main' into refactor-opcode-handlers
azteca1998 Jan 12, 2026
70b9380
Merge branch 'main' into refactor-opcode-handlers
azteca1998 Feb 11, 2026
0f6e584
perf(levm): remove Result<OpcodeResult, VMError> to avoid expensive m…
azteca1998 Feb 11, 2026
7a49e7c
fix(levm): defer eip7702 gas charging to after BAL recording in call …
azteca1998 Feb 11, 2026
d82e00e
Merge branch 'main' into refactor-opcode-handlers
azteca1998 Feb 27, 2026
edf4c8e
Update LEVM VM with optimizations lost in main merge.
azteca1998 Feb 27, 2026
df60db0
Fix lint issue.
azteca1998 Feb 27, 2026
d5ee807
Update `CHANGELOG.md`.
azteca1998 Feb 27, 2026
76a18b4
Fix lint issues.
azteca1998 Feb 27, 2026
1a94d39
Fix lint issues (again).
azteca1998 Feb 27, 2026
686fe81
Remove files that shouldn't be there.
azteca1998 Feb 27, 2026
15af0b7
Fix comments.
azteca1998 Feb 27, 2026
35606cf
Fix potential issues.
azteca1998 Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Perf

### 2026-02-27

- Refactor LEVM opcode handlers to avoid expensive matches [#4791](https://github.com/lambdaclass/ethrex/pull/4791)

### 2026-02-25

- Speed up snap sync validation with parallelism and deduplication [#6191](https://github.com/lambdaclass/ethrex/pull/6191)
Expand Down
8 changes: 5 additions & 3 deletions crates/blockchain/mempool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::{
error::MempoolError,
};
use ethrex_common::{
Address, H160, H256, U256,
Address, H160, H256,
types::{BlobsBundle, BlockHeader, ChainConfig, MempoolTransaction, Transaction, TxType},
};
use ethrex_storage::error::StoreError;
Expand Down Expand Up @@ -217,7 +217,9 @@ impl Mempool {
// Filter by blob gas fee
if is_blob_tx
&& let Some(blob_fee) = filter.blob_fee
&& tx.max_fee_per_blob_gas().is_none_or(|fee| fee < blob_fee)
&& tx
.max_fee_per_blob_gas()
.is_none_or(|fee| fee < blob_fee.into())
{
return false;
}
Expand Down Expand Up @@ -420,7 +422,7 @@ impl Mempool {
pub struct PendingTxFilter {
pub min_tip: Option<u64>,
pub base_fee: Option<u64>,
pub blob_fee: Option<U256>,
pub blob_fee: Option<u64>,
pub only_plain_txs: bool,
pub only_blob_txs: bool,
}
Expand Down
5 changes: 4 additions & 1 deletion crates/blockchain/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,10 +490,13 @@ impl Blockchain {
&self,
context: &mut PayloadBuildContext,
) -> Result<(TransactionQueue, TransactionQueue), ChainError> {
let blob_fee: u64 = context.base_fee_per_blob_gas.try_into().map_err(|_| {
ChainError::Custom("base_fee_per_blob_gas does not fit in u64".to_owned())
})?;
let tx_filter = PendingTxFilter {
/*TODO(https://github.com/lambdaclass/ethrex/issues/680): add tip filter */
base_fee: context.base_fee_per_gas(),
blob_fee: Some(context.base_fee_per_blob_gas),
blob_fee: Some(blob_fee),
..Default::default()
};
let plain_tx_filter = PendingTxFilter {
Expand Down
2 changes: 1 addition & 1 deletion crates/common/types/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ mod test {
#[test]
fn test_fake_exponential_overflow() {
// With u64 this overflows
assert!(fake_exponential(U256::from(57532635), U256::from(3145728), 3338477).is_ok());
assert!(fake_exponential(57532635.into(), 3145728.into(), 3338477).is_ok());
}

#[test]
Expand Down
13 changes: 8 additions & 5 deletions crates/l2/sequencer/l1_committer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1597,20 +1597,23 @@ async fn estimate_blob_gas(
// If the blob's market is in high demand, the equation may give a really big number.
// This function doesn't panic, it performs checked/saturating operations.
let blob_gas = fake_exponential(
U256::from(MIN_BASE_FEE_PER_BLOB_GAS),
U256::from(total_blob_gas),
MIN_BASE_FEE_PER_BLOB_GAS.into(),
total_blob_gas.into(),
BLOB_BASE_FEE_UPDATE_FRACTION,
)
.map_err(BlobEstimationError::FakeExponentialError)?;

let gas_with_headroom = (blob_gas * (100 + headroom)) / 100;

// Check if we have an overflow when we take the headroom into account.
let blob_gas = U256::from(arbitrary_base_blob_gas_price)
.checked_add(gas_with_headroom)
let gas_with_headroom_u64: u64 = gas_with_headroom
.try_into()
.map_err(|_| BlobEstimationError::OverflowError)?;
let blob_gas = arbitrary_base_blob_gas_price
.checked_add(gas_with_headroom_u64)
.ok_or(BlobEstimationError::OverflowError)?;

Ok(blob_gas)
Ok(blob_gas.into())
}

/// Regenerates state by re-applying blocks from the last known state root.
Expand Down
5 changes: 4 additions & 1 deletion crates/networking/rpc/eth/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ pub async fn get_all_block_rpc_receipts(
.unwrap_or_default(),
);
let base_fee_per_gas = header.base_fee_per_gas;
let blob_base_fee_u64: u64 = blob_base_fee
.try_into()
.map_err(|_| RpcErr::Internal("blob_base_fee does not fit in u64".to_owned()))?;
// Fetch receipt info from block
let block_info = RpcReceiptBlockInfo::from_block_header(header);
// Fetch receipt for each tx in the block and add block and tx info
Expand All @@ -360,7 +363,7 @@ pub async fn get_all_block_rpc_receipts(
tx.clone(),
index,
gas_used,
blob_base_fee,
blob_base_fee_u64,
base_fee_per_gas,
)?;
let receipt = RpcReceipt::new(
Expand Down
19 changes: 11 additions & 8 deletions crates/networking/rpc/eth/fee_market.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use ethrex_blockchain::payload::calc_gas_limit;
use ethrex_common::{
U256,
constants::GAS_PER_BLOB,
types::{
Block, BlockHeader, ELASTICITY_MULTIPLIER, Fork, ForkBlobSchedule, Transaction,
Expand Down Expand Up @@ -103,7 +102,7 @@ impl RpcHandler for FeeHistoryRequest {
let oldest_block = start_block;
let block_count = (end_block - start_block + 1) as usize;
let mut base_fee_per_gas = vec![0_u64; block_count + 1];
let mut base_fee_per_blob_gas = vec![U256::zero(); block_count + 1];
let mut base_fee_per_blob_gas = vec![0_u64; block_count + 1];
let mut gas_used_ratio = vec![0_f64; block_count];
let mut blob_gas_used_ratio = vec![0_f64; block_count];
let mut reward = Vec::<Vec<u64>>::with_capacity(block_count);
Expand Down Expand Up @@ -144,7 +143,9 @@ impl RpcHandler for FeeHistoryRequest {
);

base_fee_per_gas[idx] = header.base_fee_per_gas.unwrap_or_default();
base_fee_per_blob_gas[idx] = blob_base_fee;
base_fee_per_blob_gas[idx] = blob_base_fee
.try_into()
.map_err(|_| RpcErr::Internal("blob_base_fee does not fit in u64".to_owned()))?;
gas_used_ratio[idx] = header.gas_used as f64 / header.gas_limit as f64;
blob_gas_used_ratio[idx] = blob_gas_used_r;

Expand All @@ -155,7 +156,7 @@ impl RpcHandler for FeeHistoryRequest {
blob_schedule,
fork,
context.gas_ceil,
);
)?;
}
if !self.reward_percentiles.is_empty() {
reward.push(calculate_percentiles_for_block(
Expand All @@ -166,13 +167,12 @@ impl RpcHandler for FeeHistoryRequest {
}

let u64_to_hex_str = |x: u64| format!("0x{x:x}");
let u256_to_hex_str = |x: U256| format!("0x{x:x}");
let response = FeeHistoryResponse {
oldest_block: u64_to_hex_str(oldest_block),
base_fee_per_gas: base_fee_per_gas.into_iter().map(u64_to_hex_str).collect(),
base_fee_per_blob_gas: base_fee_per_blob_gas
.into_iter()
.map(u256_to_hex_str)
.map(u64_to_hex_str)
.collect(),
gas_used_ratio,
blob_gas_used_ratio,
Expand All @@ -191,7 +191,7 @@ fn project_next_block_base_fee_values(
schedule: ForkBlobSchedule,
fork: Fork,
gas_ceil: u64,
) -> (u64, U256) {
) -> Result<(u64, u64), RpcErr> {
// NOTE: Given that this client supports the Paris fork and later versions, we are sure that the next block
// will have the London update active, so the base fee calculation makes sense
// Geth performs a validation for this case:
Expand All @@ -208,7 +208,10 @@ fn project_next_block_base_fee_values(
let next_excess_blob_gas = calc_excess_blob_gas(header, schedule, fork);
let base_fee_per_blob =
calculate_base_fee_per_blob_gas(next_excess_blob_gas, schedule.base_fee_update_fraction);
(base_fee_per_gas, base_fee_per_blob)
let base_fee_per_blob_u64: u64 = base_fee_per_blob
.try_into()
.map_err(|_| RpcErr::Internal("base_fee_per_blob does not fit in u64".to_owned()))?;
Ok((base_fee_per_gas, base_fee_per_blob_u64))
}

async fn get_range(
Expand Down
8 changes: 4 additions & 4 deletions crates/networking/rpc/types/receipt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use ethrex_common::{
Address, Bloom, Bytes, H256, U256,
Address, Bloom, Bytes, H256,
constants::GAS_PER_BLOB,
evm::calculate_create_address,
serde_utils,
Expand Down Expand Up @@ -154,10 +154,10 @@ pub struct RpcReceiptTxInfo {
pub effective_gas_price: u64,
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::u256::hex_str_opt",
with = "serde_utils::u64::hex_str_opt",
default = "Option::default"
)]
pub blob_gas_price: Option<U256>,
pub blob_gas_price: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
with = "serde_utils::u64::hex_str_opt",
Expand All @@ -171,7 +171,7 @@ impl RpcReceiptTxInfo {
transaction: Transaction,
index: u64,
gas_used: u64,
block_blob_gas_price: U256,
block_blob_gas_price: u64,
base_fee_per_gas: Option<u64>,
) -> Result<Self, RpcErr> {
let nonce = transaction.nonce();
Expand Down
24 changes: 12 additions & 12 deletions crates/vm/backends/levm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,15 +456,15 @@ impl LEVM {
&vm_type,
)?;

let block_excess_blob_gas = block_header.excess_blob_gas.map(U256::from);
let block_excess_blob_gas = block_header.excess_blob_gas;
let config = EVMConfig::new_from_chain_config(&chain_config, block_header);
let env = Environment {
origin: tx_sender,
gas_limit: tx.gas_limit(),
config,
block_number: block_header.number.into(),
block_number: block_header.number,
coinbase: block_header.coinbase,
timestamp: block_header.timestamp.into(),
timestamp: block_header.timestamp,
prev_randao: Some(block_header.prev_randao),
slot_number: block_header
.slot_number
Expand All @@ -475,7 +475,7 @@ impl LEVM {
base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,
gas_price,
block_excess_blob_gas,
block_blob_gas_used: block_header.blob_gas_used.map(U256::from),
block_blob_gas_used: block_header.blob_gas_used,
tx_blob_hashes: tx.blob_versioned_hashes(),
tx_max_priority_fee_per_gas: tx.max_priority_fee().map(U256::from),
tx_max_fee_per_gas: tx.max_fee_per_gas().map(U256::from),
Expand Down Expand Up @@ -776,14 +776,14 @@ pub fn generic_system_contract_levm(
// EIPs 2935, 4788, 7002 and 7251 dictate that the system calls have a gas limit of 30 million and they do not use intrinsic gas.
// So we add the base cost that will be taken in the execution.
gas_limit: SYS_CALL_GAS_LIMIT + TX_BASE_COST,
block_number: block_header.number.into(),
block_number: block_header.number,
coinbase: block_header.coinbase,
timestamp: block_header.timestamp.into(),
timestamp: block_header.timestamp,
prev_randao: Some(block_header.prev_randao),
base_fee_per_gas: U256::zero(),
gas_price: U256::zero(),
block_excess_blob_gas: block_header.excess_blob_gas.map(U256::from),
block_blob_gas_used: block_header.blob_gas_used.map(U256::from),
block_excess_blob_gas: block_header.excess_blob_gas,
block_blob_gas_used: block_header.blob_gas_used,
block_gas_limit: i64::MAX as u64, // System calls, have no constraint on the block's gas limit.
config,
..Default::default()
Expand Down Expand Up @@ -965,7 +965,7 @@ fn env_from_generic(
let chain_config = db.store.get_chain_config()?;
let gas_price =
calculate_gas_price_for_generic(tx, header.base_fee_per_gas.unwrap_or(INITIAL_BASE_FEE));
let block_excess_blob_gas = header.excess_blob_gas.map(U256::from);
let block_excess_blob_gas = header.excess_blob_gas;
let config = EVMConfig::new_from_chain_config(&chain_config, header);

// Validate slot_number for Amsterdam+ blocks
Expand All @@ -991,17 +991,17 @@ fn env_from_generic(
.gas
.unwrap_or(get_max_allowed_gas_limit(header.gas_limit, config.fork)), // Ensure tx doesn't fail due to gas limit
config,
block_number: header.number.into(),
block_number: header.number,
coinbase: header.coinbase,
timestamp: header.timestamp.into(),
timestamp: header.timestamp,
prev_randao: Some(header.prev_randao),
slot_number,
chain_id: chain_config.chain_id.into(),
base_fee_per_gas: header.base_fee_per_gas.unwrap_or_default().into(),
base_blob_fee_per_gas: get_base_fee_per_blob_gas(block_excess_blob_gas, &config)?,
gas_price,
block_excess_blob_gas,
block_blob_gas_used: header.blob_gas_used.map(U256::from),
block_blob_gas_used: header.blob_gas_used,
tx_blob_hashes: tx.blob_versioned_hashes.clone(),
tx_max_priority_fee_per_gas: tx.max_priority_fee_per_gas.map(U256::from),
tx_max_fee_per_gas: tx.max_fee_per_gas.map(U256::from),
Expand Down
25 changes: 21 additions & 4 deletions crates/vm/levm/src/call_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ use crate::{
};
use bytes::Bytes;
use ethrex_common::types::block_access_list::BlockAccessListCheckpoint;
use ethrex_common::{Address, U256};
use ethrex_common::{H256, types::Code};
use std::{collections::HashMap, fmt, hint::assert_unchecked};
use ethrex_common::{Address, H256, U256, types::Code};
use std::{
collections::HashMap,
fmt,
hash::{Hash, Hasher},
hint::assert_unchecked,
};

/// [`u64`]s that make up a [`U256`]
const U64_PER_U256: usize = U256::MAX.0.len();
Expand Down Expand Up @@ -219,6 +223,16 @@ impl fmt::Debug for Stack {
}
}

impl Hash for Stack {
#[expect(
clippy::indexing_slicing,
reason = "offset is always within bounds of values"
)]
fn hash<H: Hasher>(&self, state: &mut H) {
self.values[self.offset..].hash(state);
}
}

#[derive(Debug)]
/// A call frame, or execution environment, is the context in which
/// the EVM is currently executing.
Expand Down Expand Up @@ -314,7 +328,10 @@ impl CallFrameBackup {
}

impl CallFrame {
#[allow(clippy::too_many_arguments)]
#[expect(
clippy::too_many_arguments,
reason = "inlined constructor, many args needed for performance"
)]
// Force inline, due to lot of arguments, inlining must be forced, and it is actually beneficial
// because passing so much data is costly. Verified with samply.
#[inline(always)]
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/levm/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub const VERSIONED_HASH_VERSION_KZG: u8 = 0x01;
pub const TARGET_BLOB_GAS_PER_BLOCK: u32 = 393216; // TARGET_BLOB_NUMBER_PER_BLOCK * GAS_PER_BLOB
pub const TARGET_BLOB_GAS_PER_BLOCK_PECTRA: u32 = 786432; // TARGET_BLOB_NUMBER_PER_BLOCK * GAS_PER_BLOB

pub const MIN_BASE_FEE_PER_BLOB_GAS: U256 = U256::one();
pub const MIN_BASE_FEE_PER_BLOB_GAS: u64 = 1;

// WARNING: Do _not_ use the BLOB_BASE_FEE_UPDATE_FRACTION_* family of
// constants as is. Use the `get_blob_base_fee_update_fraction_value`
Expand All @@ -69,7 +69,7 @@ pub const MAX_BLOB_COUNT_TX: usize = 6;
pub const VALID_BLOB_PREFIXES: [u8; 2] = [0x01, 0x02];

// Block constants
pub const LAST_AVAILABLE_BLOCK_LIMIT: U256 = U256([256, 0, 0, 0]);
pub const LAST_AVAILABLE_BLOCK_LIMIT: u64 = 256;

// EIP7702 - EOA Load Code
pub static SECP256K1_ORDER: LazyLock<U256> = LazyLock::new(||
Expand Down
10 changes: 5 additions & 5 deletions crates/vm/levm/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ pub struct Environment {
/// Gas limit of the Transaction
pub gas_limit: u64,
pub config: EVMConfig,
pub block_number: U256,
/// Coinbase is the block's beneficiary - the address that receives the block rewards (priority fees).
pub block_number: u64,
/// Coinbase is the block's beneficiary - the address that receives the block rewards and fees.
pub coinbase: Address,
pub timestamp: U256,
pub timestamp: u64,
pub prev_randao: Option<H256>,
pub difficulty: U256,
pub slot_number: U256,
pub chain_id: U256,
pub base_fee_per_gas: U256,
pub base_blob_fee_per_gas: U256,
pub gas_price: U256, // Effective gas price
pub block_excess_blob_gas: Option<U256>,
pub block_blob_gas_used: Option<U256>,
pub block_excess_blob_gas: Option<u64>,
pub block_blob_gas_used: Option<u64>,
pub tx_blob_hashes: Vec<H256>,
pub tx_max_priority_fee_per_gas: Option<U256>,
pub tx_max_fee_per_gas: Option<U256>,
Expand Down
1 change: 1 addition & 0 deletions crates/vm/levm/src/hooks/default_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ pub fn validate_max_fee_per_blob_gas(
}
.into());
}

Ok(())
}

Expand Down
2 changes: 1 addition & 1 deletion crates/vm/levm/src/hooks/l2_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ fn validate_sufficient_max_fee_per_gas_l2(
let total_fee = vm
.env
.base_fee_per_gas
.checked_add(U256::from(fee_config.operator_fee_per_gas))
.checked_add(fee_config.operator_fee_per_gas.into())
.ok_or(TxValidationError::InsufficientMaxFeePerGas)?;

if vm.env.tx_max_fee_per_gas.unwrap_or(vm.env.gas_price) < total_fee {
Expand Down
Loading
Loading