Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Perf

### 2025-10-17

- Replaces incremental iteration with a one-time precompute method that scans the entire bytecode, building a `BitVec<u8, Msb0>` where bits mark valid `JUMPDEST` positions, skipping `PUSH1..PUSH32` data bytes.
- Updates `is_blacklisted` to O(1) bit lookup.

### 2025-10-14

- Improve get_closest_nodes p2p performance [#4838](https://github.com/lambdaclass/ethrex/pull/4838)
Expand Down
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/l2/prover/src/guest_program/src/risc0/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/l2/prover/src/guest_program/src/sp1/Cargo.lock

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

7 changes: 6 additions & 1 deletion crates/vm/levm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ sha3 = "0.10.8"
datatest-stable = "0.2.9"
walkdir = "2.5.0"
secp256k1.workspace = true
p256 = { version = "0.13.2", features = ["ecdsa", "arithmetic", "expose-field"] }
p256 = { version = "0.13.2", features = [
"ecdsa",
"arithmetic",
"expose-field",
] }
sha2 = "0.10.8"
ripemd = "0.1.3"
malachite = "0.6.1"
Expand All @@ -41,6 +45,7 @@ k256 = "0.13.4"
# We directly import SP1's patch because the API for the patch is different from the original subtrate-bn crate
substrate-bn = { git = "https://github.com/sp1-patches/bn", tag = "patch-0.6.0-sp1-5.0.0", optional = true }

bitvec = { version = "1.0.1", features = ["alloc"] }

[dev-dependencies]
hex.workspace = true
Expand Down
78 changes: 38 additions & 40 deletions crates/vm/levm/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ use crate::{
vm::{Substate, VM},
};
use ExceptionalHalt::OutOfGas;
use bytes::{Bytes, buf::IntoIter};
use bitvec::{bitvec, order::Msb0, vec::BitVec};
use bytes::Bytes;
use ethrex_common::{
Address, H256, U256,
evm::calculate_create_address,
Expand All @@ -29,7 +30,7 @@ use secp256k1::{
ecdsa::{RecoverableSignature, RecoveryId},
};
use sha3::{Digest, Keccak256};
use std::{collections::HashMap, iter::Enumerate};
use std::collections::HashMap;
pub type Storage = HashMap<U256, H256>;

// ================== Address related functions ======================
Expand Down Expand Up @@ -79,59 +80,56 @@ pub fn calculate_create2_address(
/// offsets of bytes `0x5B` (`JUMPDEST`) within push constants.
#[derive(Debug)]
pub struct JumpTargetFilter {
/// The list of invalid jump target offsets.
filter: Vec<usize>,
/// The last processed offset, plus one.
offset: usize,

/// Program bytecode iterator.
iter: Enumerate<IntoIter<Bytes>>,
/// Number of bytes remaining to process from the last push instruction.
partial: usize,
bytecode: Bytes,
jumpdests: Option<BitVec<u8, Msb0>>,
}

impl JumpTargetFilter {
/// Create an empty `JumpTargetFilter`.
pub fn new(bytecode: Bytes) -> Self {
Self {
filter: Vec::new(),
offset: 0,

iter: bytecode.into_iter().enumerate(),
partial: 0,
bytecode,
jumpdests: None,
}
}

/// Check whether a target jump address is blacklisted or not.
///
/// This method may potentially grow the filter if the requested address is out of range.
/// Builds the jumpdest table on the first call, and caches it for future calls.
#[expect(
clippy::as_conversions,
clippy::arithmetic_side_effects,
clippy::indexing_slicing
)]
pub fn is_blacklisted(&mut self, address: usize) -> bool {
if let Some(delta) = address.checked_sub(self.offset) {
// It is not realistic to expect a bytecode offset to overflow an `usize`.
#[expect(clippy::arithmetic_side_effects)]
for (offset, value) in (&mut self.iter).take(delta + 1) {
match self.partial.checked_sub(1) {
None => {
// Neither the `as` conversions nor the subtraction can fail here.
#[expect(clippy::as_conversions)]
if (Opcode::PUSH1..=Opcode::PUSH32).contains(&Opcode::from(value)) {
self.partial = value as usize - Opcode::PUSH0 as usize;
}
}
Some(partial) => {
self.partial = partial;

#[expect(clippy::as_conversions)]
if value == Opcode::JUMPDEST as u8 {
self.filter.push(offset);
}
match self.jumpdests {
// Already built the jumpdest table, just check it
Some(ref jumpdests) => address >= jumpdests.len() || !jumpdests[address],
// First time we are called, need to build the jumpdest table
None => {
let code = &self.bytecode;
let len = code.len();
let mut jumpdests = bitvec![u8, Msb0; 0; len]; // All false, size = len
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could try using the default usize instead of u8 here, as the documentation states:

If you are only using this crate to discard the seven wasted bits per bool in a collection of bools, and are not too concerned about the in-memory representation, then you should use the default type argument of usize. This is because most processors work best when moving an entire usize between memory and the processor itself, and using a smaller type may cause it to slow down. Additionally, processor instructions are typically optimized for the whole register, and the processor might need to do additional clearing work for narrower types.

Also, Lsb0 might be a better default here, according to the docs:

The default ordering is Lsb0, as it typically produces shorter object code than Msb0 does.


let mut i = 0;
while i < len {
let opcode = Opcode::from(code[i]);
if opcode == Opcode::JUMPDEST {
jumpdests.set(i, true);
} else if (Opcode::PUSH1..=Opcode::PUSH32).contains(&opcode) {
// PUSH1 (0x60) to PUSH32 (0x7f): skip 1 to 32 bytes
let skip = opcode as usize - Opcode::PUSH0 as usize;
i += skip; // Advance past data bytes
}
i += 1;
}
}

self.filter.last() == Some(&address)
} else {
self.filter.binary_search(&address).is_ok()
let is_blacklisted = address >= jumpdests.len() || !jumpdests[address];

self.jumpdests = Some(jumpdests);

is_blacklisted
}
}
}
}
Expand Down
Loading