Skip to content

Commit 4a4e5c9

Browse files
authored
perf(l1): improve ecrecover by removing needless alloc and conversion (#5738)
**Motivation** Removed a vec alloc and a U256 conversion that could be avoided Tried to run the benchmarks but unrelated things improve and regress, so i think its a bit flaky, this PR only changed the precompile function so its hard to imagine how it affects that
1 parent be84ee6 commit 4a4e5c9

File tree

2 files changed

+37
-14
lines changed

2 files changed

+37
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Perf
44

5+
### 2026-01-13
6+
7+
- Improve ecrecover precompile by removing heap allocs and conversions [#5709](https://github.com/lambdaclass/ethrex/pull/5709)
8+
59
### 2026-01-12
610

711
- Refactor `ecpairing` using ark [#5792](https://github.com/lambdaclass/ethrex/pull/5792)

crates/vm/levm/src/precompiles.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,23 @@ pub(crate) fn fill_with_zeros(calldata: &Bytes, target_len: usize) -> Bytes {
375375
padded_calldata.into()
376376
}
377377

378+
#[expect(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
379+
#[cfg(all(
380+
not(feature = "sp1"),
381+
not(feature = "risc0"),
382+
not(feature = "zisk"),
383+
feature = "secp256k1"
384+
))]
385+
#[inline(always)]
386+
fn copy_segment(calldata: &Bytes, dst: &mut [u8], start: usize) {
387+
if start >= calldata.len() {
388+
return;
389+
}
390+
let end = (start + dst.len()).min(calldata.len());
391+
let src = &calldata[start..end];
392+
dst[..src.len()].copy_from_slice(src);
393+
}
394+
378395
#[cfg(all(
379396
not(feature = "sp1"),
380397
not(feature = "risc0"),
@@ -386,19 +403,25 @@ pub fn ecrecover(calldata: &Bytes, gas_remaining: &mut u64, _fork: Fork) -> Resu
386403

387404
increase_precompile_consumed_gas(ECRECOVER_COST, gas_remaining)?;
388405

389-
const INPUT_LEN: usize = 128;
390406
const WORD: usize = 32;
407+
const SIG_LEN: usize = 64;
391408

392-
let input = fill_with_zeros(calldata, INPUT_LEN);
409+
// Total input size = 128
410+
let mut raw_hash = [0u8; WORD];
411+
let mut raw_v = [0u8; WORD];
412+
let mut raw_sig = [0u8; SIG_LEN];
393413

394-
// len(raw_hash) == 32, len(raw_v) == 32, len(raw_sig) == 64
395-
let (raw_hash, tail) = input.split_at(WORD);
396-
let (raw_v, raw_sig) = tail.split_at(WORD);
414+
copy_segment(calldata, &mut raw_hash, 0);
415+
copy_segment(calldata, &mut raw_v, WORD);
416+
copy_segment(calldata, &mut raw_sig, WORD * 2);
397417

398418
// EVM expects v ∈ {27, 28}. Anything else is invalid → empty return.
399-
let recovery_id_byte = match u8::try_from(u256_from_big_endian(raw_v)) {
400-
Ok(27) => 0_i32,
401-
Ok(28) => 1_i32,
419+
if raw_v[..(WORD - 1)].iter().any(|&b| b != 0) {
420+
return Ok(Bytes::new());
421+
}
422+
let recovery_id_byte = match raw_v[WORD - 1] {
423+
27 => 0_i32,
424+
28 => 1_i32,
402425
_ => return Ok(Bytes::new()),
403426
};
404427

@@ -408,16 +431,12 @@ pub fn ecrecover(calldata: &Bytes, gas_remaining: &mut u64, _fork: Fork) -> Resu
408431
};
409432

410433
let Ok(recoverable_signature) =
411-
secp256k1::ecdsa::RecoverableSignature::from_compact(raw_sig, recovery_id)
434+
secp256k1::ecdsa::RecoverableSignature::from_compact(&raw_sig, recovery_id)
412435
else {
413436
return Ok(Bytes::new());
414437
};
415438

416-
let message = secp256k1::Message::from_digest(
417-
raw_hash
418-
.try_into()
419-
.map_err(|_err| InternalError::msg("Invalid message length for ecrecover"))?,
420-
);
439+
let message = secp256k1::Message::from_digest(raw_hash);
421440

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

0 commit comments

Comments
 (0)