Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
80 changes: 16 additions & 64 deletions Cargo.lock

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

3 changes: 0 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,3 @@ tower-http = { version = "0.6.2", features = ["cors"] }
indexmap = { version = "2.11.4" }

rocksdb = "0.24.0"

[patch.crates-io]
secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", tag = "patch-0.30.0-sp1-5.0.0" }
4 changes: 3 additions & 1 deletion crates/l2/prover/src/guest_program/src/sp1/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ guest_program = { path = "../../" }
ethrex-common = { path = "../../../../../../common", default-features = false }
ethrex-storage = { path = "../../../../../../storage", default-features = false }
ethrex-rlp = { path = "../../../../../../common/rlp" }
ethrex-vm = { path = "../../../../../../vm", default-features = false }
ethrex-vm = { path = "../../../../../../vm", default-features = false, features = [
"sp1",
] }
ethrex-blockchain = { path = "../../../../../../blockchain", default-features = false }
ethrex-l2-common = { path = "../../../../../common", default-features = false }

Expand Down
4 changes: 4 additions & 0 deletions crates/vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ path = "./lib.rs"

[features]
default = []

c-kzg = ["ethrex-levm/c-kzg", "ethrex-common/c-kzg"]

sp1 = ["ethrex-levm/sp1"]

debug = ["ethrex-levm/debug"]

[lints.clippy]
Expand Down
11 changes: 10 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 @@ -47,8 +51,13 @@ spinoff = "0.8.0"

[features]
default = []

c-kzg = ["ethrex-common/c-kzg"]

sp1 = []

ethereum_foundation_tests = []

debug = []

[lints.rust]
Expand Down
72 changes: 68 additions & 4 deletions crates/vm/levm/src/precompiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ use bls12_381::{
};
use bytes::{Buf, Bytes};
use ethrex_common::H160;
use ethrex_common::utils::{keccak, u256_from_big_endian_const};
use ethrex_common::utils::u256_from_big_endian_const;
use ethrex_common::{
Address, H256, U256, serde_utils::bool, types::Fork, types::Fork::*,
utils::u256_from_big_endian,
};
use ethrex_crypto::{blake2f::blake2b_f, kzg::verify_kzg_proof};
use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};
use k256::elliptic_curve::Field;
use lambdaworks_math::{
elliptic_curve::{
Expand Down Expand Up @@ -61,8 +60,8 @@ use crate::{
gas_cost::{
self, BLAKE2F_ROUND_COST, BLS12_381_G1_K_DISCOUNT, BLS12_381_G1ADD_COST,
BLS12_381_G2_K_DISCOUNT, BLS12_381_G2ADD_COST, BLS12_381_MAP_FP_TO_G1_COST,
BLS12_381_MAP_FP2_TO_G2_COST, ECADD_COST, ECMUL_COST, ECRECOVER_COST, G1_MUL_COST,
G2_MUL_COST, POINT_EVALUATION_COST,
BLS12_381_MAP_FP2_TO_G2_COST, ECADD_COST, ECMUL_COST, G1_MUL_COST, G2_MUL_COST,
POINT_EVALUATION_COST,
},
};
use lambdaworks_math::elliptic_curve::short_weierstrass::curves::bls12_381::curve::{
Expand Down Expand Up @@ -383,6 +382,65 @@ pub(crate) fn fill_with_zeros(calldata: &Bytes, target_len: usize) -> Bytes {
padded_calldata.into()
}

#[cfg(not(feature = "sp1"))]
pub fn ecrecover(calldata: &Bytes, gas_remaining: &mut u64, _fork: Fork) -> Result<Bytes, VMError> {
use sha3::Keccak256;

use crate::gas_cost::ECRECOVER_COST;

increase_precompile_consumed_gas(ECRECOVER_COST, gas_remaining)?;

const INPUT_LEN: usize = 128;
const WORD: usize = 32;

let input = fill_with_zeros(calldata, INPUT_LEN);

// len(raw_hash) == 32, len(raw_v) == 32, len(raw_sig) == 64
let (raw_hash, tail) = input.split_at(WORD);
let (raw_v, raw_sig) = tail.split_at(WORD);

// EVM expects v ∈ {27, 28}. Anything else is invalid → empty return.
let recovery_id_byte = match u8::try_from(u256_from_big_endian(raw_v)) {
Ok(27) => 0_i32,
Ok(28) => 1_i32,
_ => return Ok(Bytes::new()),
};

// Recovery id from the adjusted byte.
let Ok(recovery_id) = secp256k1::ecdsa::RecoveryId::try_from(recovery_id_byte) else {
return Ok(Bytes::new());
};

let Ok(recoverable_signature) =
secp256k1::ecdsa::RecoverableSignature::from_compact(raw_sig, recovery_id)
else {
return Ok(Bytes::new());
};
Comment on lines +414 to +418
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

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

The ECRECOVER precompile must enforce EIP-2 (Homestead) to reject high-s signatures; otherwise malleable signatures will be accepted. After constructing the recoverable signature, convert to a standard signature and reject if normalization would change s: let mut standard_sig = recoverable_signature.to_standard(); if standard_sig.normalize_s() { return Ok(Bytes::new()); }.

Copilot uses AI. Check for mistakes.

let message = secp256k1::Message::from_digest(
raw_hash
.try_into()
.map_err(|_err| InternalError::msg("Invalid message length for ecrecover"))?,
);

let Ok(public_key) = recoverable_signature.recover(&message) else {
return Ok(Bytes::new());
};

// We need to take the 64 bytes from the public key (discarding the first pos of the slice)
let public_key_hash = Keccak256::digest(&public_key.serialize_uncompressed()[1..]);
Copy link

Copilot AI Oct 17, 2025

Choose a reason for hiding this comment

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

Keccak256::digest is a trait method from sha3::Digest and requires the Digest trait to be in scope; otherwise this won't compile. Add use sha3::Digest; alongside the Keccak256 import in this cfg block.

Copilot uses AI. Check for mistakes.

// Address is the last 20 bytes of the hash.
#[expect(clippy::indexing_slicing)]
let recovered_address_bytes = &public_key_hash[12..];

let mut out = [0u8; 32];

out[12..32].copy_from_slice(recovered_address_bytes);

Ok(Bytes::copy_from_slice(&out))
}

/// ## ECRECOVER precompile.
/// Elliptic curve digital signature algorithm (ECDSA) public key recovery function.
///
Expand All @@ -392,7 +450,13 @@ pub(crate) fn fill_with_zeros(calldata: &Bytes, target_len: usize) -> Bytes {
/// [64..128): r||s (64 bytes)
///
/// Returns the recovered address.
#[cfg(feature = "sp1")]
pub fn ecrecover(calldata: &Bytes, gas_remaining: &mut u64, _fork: Fork) -> Result<Bytes, VMError> {
use ethrex_common::utils::keccak;
use k256::ecdsa::{RecoveryId, Signature, VerifyingKey};

use crate::gas_cost::ECRECOVER_COST;

increase_precompile_consumed_gas(ECRECOVER_COST, gas_remaining)?;

const INPUT_LEN: usize = 128;
Expand Down
Loading