From 75f40833a9de34f4dbfa9b16314d3b8da1b386c7 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Fri, 31 Jul 2026 18:06:34 -0300 Subject: [PATCH 1/2] perf(guest): read the private input zero-copy via ef_io::read_input get_private_input() to_vec()'s the whole memory-mapped input before rkyv deserializes it; read_input hands rkyv a slice straight into the input region instead. Same bytes, same private-input commitment. Measured vs origin/main (same fixtures, deterministic): transfers_20 8,732,213 -> 8,692,490 (-39,723) erc20_20 10,328,222 -> 10,278,822 (-49,400) mixed_20 9,817,444 -> 9,768,492 (-48,952) Verified: test_prove_ethrex_empty_block (prove+verify) passes. --- executor/programs/rust/ethrex/src/main.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/executor/programs/rust/ethrex/src/main.rs b/executor/programs/rust/ethrex/src/main.rs index 30a39f4b5..4d42a804e 100644 --- a/executor/programs/rust/ethrex/src/main.rs +++ b/executor/programs/rust/ethrex/src/main.rs @@ -5,8 +5,16 @@ use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; use rkyv::rancor::Error; pub fn main() { - let input = lambda_vm_syscalls::syscalls::get_private_input(); - let input = rkyv::from_bytes::(&input).unwrap(); + // Zero-copy private input: `ef_io::read_input` returns a pointer+len into + // the memory-mapped input region (host pre-loads it before execution), so + // rkyv deserializes straight from the input. `get_private_input()` would + // `to_vec()` the whole input first — a full extra copy plus one large + // allocation (~52k cycles on a 20-tx block). + let mut input_ptr: *const u8 = core::ptr::null(); + let mut input_len: usize = 0; + unsafe { lambda_vm_syscalls::ef_io::read_input(&mut input_ptr, &mut input_len) }; + let input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + let input = rkyv::from_bytes::(input).unwrap(); // LambdaVM crypto provider, defined in the lambda_vm repo and injected here // (so crypto changes don't require an ethrex PR — see `crypto/ethrex-crypto`). // It accelerates trait-routed `keccak256` (via the keccak_permute precompile) From a6ca32e71c795cf9e8f38ccfd5473d097c6ea292 Mon Sep 17 00:00:00 2001 From: Mauro Toscano <12560266+MauroToscano@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:28:52 -0300 Subject: [PATCH 2/2] fix(guest): take the zero-copy input via the safe get_private_input_slice (#898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The zero-copy read is the right call, but it hand-rolls what `syscalls::get_private_input_slice` already does: borrow the mapped private-input region in place and hand back `&'static [u8]`, no copy and no allocation. `get_private_input` is that same call plus a `to_vec()`, so dropping to the slice is the whole win without the pointer plumbing. Three things that buys: - No raw pointers in guest code. `syscalls.rs` deliberately keeps the region layout and its one `unsafe` block in a single place — that is why `get_private_input_slice` exists. Re-reading the length prefix in the guest duplicates layout knowledge that has to stay in step with the executor. - Restores the length-prefix clamp. `get_private_input_slice` bounds the prefix by `MAX_PRIVATE_INPUT_SIZE`; `ef_io::read_input` returns it raw. The executor rejects oversized inputs, so honest runs are identical — but a forged prefix built a slice reaching past the region instead of a bounded one. - Drops a dependency on unspecified behavior. `ef_io::read_input` documents `buf_ptr` as unspecified when `buf_size == 0`, and the previous code fed it to `from_raw_parts` regardless. Harmless in practice (the implementation always writes it, and ethrex input is never empty), but not a contract to lean on. `bench_vs/lambda/recursion` already reads its blob this way. --- executor/programs/rust/ethrex/src/main.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/executor/programs/rust/ethrex/src/main.rs b/executor/programs/rust/ethrex/src/main.rs index 4d42a804e..8154978cf 100644 --- a/executor/programs/rust/ethrex/src/main.rs +++ b/executor/programs/rust/ethrex/src/main.rs @@ -5,15 +5,12 @@ use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; use rkyv::rancor::Error; pub fn main() { - // Zero-copy private input: `ef_io::read_input` returns a pointer+len into - // the memory-mapped input region (host pre-loads it before execution), so - // rkyv deserializes straight from the input. `get_private_input()` would - // `to_vec()` the whole input first — a full extra copy plus one large - // allocation (~52k cycles on a 20-tx block). - let mut input_ptr: *const u8 = core::ptr::null(); - let mut input_len: usize = 0; - unsafe { lambda_vm_syscalls::ef_io::read_input(&mut input_ptr, &mut input_len) }; - let input = unsafe { core::slice::from_raw_parts(input_ptr, input_len) }; + // Zero-copy private input: borrow the memory-mapped input region in place + // (the host pre-loads it before execution) so rkyv deserializes straight + // out of it. `get_private_input()` is this same slice plus a `to_vec()` — + // a full extra copy and one large allocation (~50k cycles on a 20-tx + // block). + let input = lambda_vm_syscalls::syscalls::get_private_input_slice(); let input = rkyv::from_bytes::(input).unwrap(); // LambdaVM crypto provider, defined in the lambda_vm repo and injected here // (so crypto changes don't require an ethrex PR — see `crypto/ethrex-crypto`).