From cfdbb692d747cefb9b0463281085cea7d812f3be Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 19 Aug 2026 16:14:41 -0300 Subject: [PATCH 1/4] feat(hints): replace the hint ecall with a private-input hint arena MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The non-constraining hint ecall (and its prover HINT table) is replaced by untrusted 32-byte hint slots appended to the private-input region: [u32 len][data][pad8][u32 hint_count][u32 pad][32-byte slots] Guests consume slots positionally (syscalls: hint_count/hint_slot/ next_hint/request_hint) and must still verify each hint in-circuit, falling back to software on failure or arena exhaustion. A lying host can only force fallbacks, never change the result. When hints are not known beforehand, request_hint appends (hint_id, input) to a request log above the private-input window; the host reads it back (Memory::hint_requests / ExecutionResult::hint_requests), answers with compute_hint, and re-runs with a complete arena (executor::collect_hints — the two-pass flow). - executor: encode_private_input_region (single source of truth for the wire format), hints threaded through Executor::new, ecall dispatch and HINT_* error variants removed - prover: HINT table and its trace-builder/CPU plumbing removed; hints threaded through prove_*/count_*/Traces::from_* - ethrex-crypto: field_inv/scalar_inv/decompress_r read from the arena (verify + software fallback kept) - cli: --hints on execute/prove/count-elements, execute --record-hints for the recording pass - guests: hint_arena (slot API) and ecrecover_hints (N recoveries); two-pass measurement drivers on the executor and prover (continuation) sides Measured on 30 ecrecovers: 6.41M guest cycles with software fallback vs 866k with arena hints (7.40x); continuation proof of the hinted run verifies end-to-end (14 epochs @ 2^16). --- bin/cli/src/main.rs | 148 ++- crypto/ethrex-crypto/src/lib.rs | 84 +- crypto/ethrex-crypto/src/tests/hint_tests.rs | 20 +- .../.cargo/config.toml | 0 .../programs/rust/ecrecover_hints/Cargo.lock | 1011 +++++++++++++++++ .../programs/rust/ecrecover_hints/Cargo.toml | 16 + .../programs/rust/ecrecover_hints/src/main.rs | 38 + .../.cargo/config.toml | 0 .../rust/{hint_min => hint_arena}/Cargo.lock | 96 +- .../{hint_multi => hint_arena}/Cargo.toml | 2 +- executor/programs/rust/hint_arena/src/main.rs | 35 + executor/programs/rust/hint_min/Cargo.toml | 9 - executor/programs/rust/hint_min/src/main.rs | 31 - executor/programs/rust/hint_multi/Cargo.lock | 331 ------ executor/programs/rust/hint_multi/src/main.rs | 43 - executor/src/flamegraph.rs | 4 +- executor/src/main.rs | 2 +- executor/src/tests/hint_tests.rs | 196 ---- executor/src/tests/mod.rs | 1 - executor/src/vm/execution.rs | 40 +- executor/src/vm/instruction/execution.rs | 93 +- executor/src/vm/memory.rs | 277 ++++- executor/tests/asm.rs | 12 +- executor/tests/hint_arena_ecrecover.rs | 121 ++ executor/tests/rust.rs | 2 +- prover/benches/bench_continuation.rs | 6 +- prover/src/continuation.rs | 101 +- prover/src/lib.rs | 41 +- prover/src/tables/cpu.rs | 8 - prover/src/tables/hint.rs | 373 ------ prover/src/tables/mod.rs | 1 - prover/src/tables/page.rs | 25 +- prover/src/tables/trace_builder.rs | 226 +--- prover/src/test_utils.rs | 20 +- .../tests/constraint_program_device_tests.rs | 1 - prover/src/tests/constraint_program_tests.rs | 1 - prover/src/tests/constraint_set_tests_b.rs | 16 - .../tests/count_table_lengths_drift_tests.rs | 33 +- prover/src/tests/hint_tests.rs | 171 --- prover/src/tests/mod.rs | 2 - prover/src/tests/ood_window_ir_tests.rs | 1 - prover/src/tests/page_offset_forgery_poc.rs | 8 +- prover/src/tests/prove_elfs_tests.rs | 481 ++------ prover/src/tests/recursion_smoke_test.rs | 9 +- .../src/tests/recursion_soundness_gap_poc.rs | 5 +- prover/src/tests/trace_builder_tests.rs | 28 +- prover/src/tests/trace_test_helpers.rs | 2 + prover/tests/calibration.rs | 4 +- prover/tests/ecrecover_hints_continuation.rs | 61 + prover/tests/gpu_constraint_interp_real.rs | 1 - syscalls/src/syscalls.rs | 193 +++- 51 files changed, 2267 insertions(+), 2163 deletions(-) rename executor/programs/rust/{hint_min => ecrecover_hints}/.cargo/config.toml (100%) create mode 100644 executor/programs/rust/ecrecover_hints/Cargo.lock create mode 100644 executor/programs/rust/ecrecover_hints/Cargo.toml create mode 100644 executor/programs/rust/ecrecover_hints/src/main.rs rename executor/programs/rust/{hint_multi => hint_arena}/.cargo/config.toml (100%) rename executor/programs/rust/{hint_min => hint_arena}/Cargo.lock (72%) rename executor/programs/rust/{hint_multi => hint_arena}/Cargo.toml (86%) create mode 100644 executor/programs/rust/hint_arena/src/main.rs delete mode 100644 executor/programs/rust/hint_min/Cargo.toml delete mode 100644 executor/programs/rust/hint_min/src/main.rs delete mode 100644 executor/programs/rust/hint_multi/Cargo.lock delete mode 100644 executor/programs/rust/hint_multi/src/main.rs delete mode 100644 executor/src/tests/hint_tests.rs create mode 100644 executor/tests/hint_arena_ecrecover.rs delete mode 100644 prover/src/tables/hint.rs delete mode 100644 prover/src/tests/hint_tests.rs create mode 100644 prover/tests/ecrecover_hints_continuation.rs diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index a04e920db..64607f356 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -147,6 +147,23 @@ enum Commands { /// data). #[arg(long)] cycles: bool, + + /// Path to a hint arena file (concatenated 32-byte slots) appended to + /// the private-input region; produce one with --record-hints. The + /// flamegraph path always runs hint-free (conservative profiles). + #[arg(long, value_hint = ValueHint::FilePath, conflicts_with = "flamegraph")] + hints: Option, + + /// Recording pass of the two-pass hint flow: run once with an empty + /// hint arena, answer the guest's logged hint requests host-side, and + /// write the resulting arena (concatenated 32-byte slots) to this file + /// for use with --hints on the second, provable run. + #[arg( + long, + value_hint = ValueHint::FilePath, + conflicts_with_all = ["flamegraph", "cycle_budget", "hints", "cycles"] + )] + record_hints: Option, }, /// Generate a proof for an ELF program @@ -193,6 +210,11 @@ enum Commands { long_help = "Continuation epoch size as log2(cycles); e.g. 20 means 1,048,576 cycles.\n\nDefault when omitted: 20. Values below 18 are rejected for the CLI because tiny epochs are dominated by fixed overhead. Indicative ethrex 10-transfer distinct-account peak heap from a local sweep: 19 ~= 6.9 GB, 20 ~= 9.5 GB, 21 ~= 15.8 GB, 22 ~= 26.8 GB. Higher values reduce epoch count, continuation bundle size, and fixed per-epoch overhead, but increase peak memory. For a new workload, try the highest value your machine can run without swapping." )] epoch_size_log2: Option, + + /// Path to a hint arena file (concatenated 32-byte slots) appended to + /// the private-input region; produce one with `execute --record-hints`. + #[arg(long, value_hint = ValueHint::FilePath)] + hints: Option, }, /// Verify a proof bundle @@ -227,6 +249,11 @@ enum Commands { /// Path to the private input file #[arg(long, value_hint = ValueHint::FilePath)] private_input: Option, + + /// Path to a hint arena file (concatenated 32-byte slots) appended to + /// the private-input region; produce one with `execute --record-hints`. + #[arg(long, value_hint = ValueHint::FilePath)] + hints: Option, }, } @@ -243,6 +270,8 @@ fn main() -> ExitCode { flamegraph_checkpoint_cycles, cycle_budget, cycles, + hints, + record_hints, } => cmd_execute( elf, private_input, @@ -253,6 +282,8 @@ fn main() -> ExitCode { }, cycle_budget, cycles, + hints, + record_hints, ), Commands::Prove { elf, @@ -264,6 +295,7 @@ fn main() -> ExitCode { elements, continuations, epoch_size_log2, + hints, } => { if continuations { cmd_prove_continuation( @@ -274,9 +306,10 @@ fn main() -> ExitCode { blowup, time, cycles, + hints, ) } else { - cmd_prove(elf, output, private_input, blowup, time, cycles, elements) + cmd_prove(elf, output, private_input, blowup, time, cycles, elements, hints) } } Commands::Verify { @@ -292,7 +325,11 @@ fn main() -> ExitCode { cmd_verify(proof, elf, blowup, time) } } - Commands::CountElements { elf, private_input } => cmd_count_elements(elf, private_input), + Commands::CountElements { + elf, + private_input, + hints, + } => cmd_count_elements(elf, private_input, hints), } } @@ -306,10 +343,37 @@ fn read_private_input(path: Option<&PathBuf>) -> Result, String> { } } -fn count_cycles(elf_data: &[u8], private_inputs: &[u8]) -> Result { +/// Read a hint arena file: concatenated 32-byte slots, in the guest's request +/// order (see `executor::vm::memory::encode_private_input_region`). +fn read_hints(path: Option<&PathBuf>) -> Result, String> { + match path { + Some(path) => { + eprintln!("Reading hint arena file..."); + let bytes = + std::fs::read(path).map_err(|e| format!("Failed to read hint arena file: {e}"))?; + if bytes.len() % 32 != 0 { + return Err(format!( + "Hint arena file must be whole 32-byte slots, got {} bytes", + bytes.len() + )); + } + Ok(bytes + .chunks_exact(32) + .map(|slot| slot.try_into().expect("32-byte chunk")) + .collect()) + } + None => Ok(vec![]), + } +} + +fn count_cycles( + elf_data: &[u8], + private_inputs: &[u8], + hints: &[[u8; 32]], +) -> Result { let program = Elf::load(elf_data).map_err(|e| format!("Failed to load ELF for cycle count: {e:?}"))?; - let executor = Executor::new(&program, private_inputs.to_vec()) + let executor = Executor::new(&program, private_inputs.to_vec(), hints) .map_err(|e| format!("Failed to create executor for cycle count: {e:?}"))?; executor .run() @@ -383,6 +447,8 @@ fn cmd_execute( flamegraph: FlamegraphCliOptions, cycle_budget: Option, cycles: bool, + hints_path: Option, + record_hints_path: Option, ) -> ExitCode { let elf_data = match std::fs::read(&elf_path) { Ok(data) => data, @@ -408,6 +474,36 @@ fn cmd_execute( } }; + // Recording pass of the two-pass hint flow: run hint-free, answer the + // guest's logged requests host-side, and write the arena for --hints runs. + if let Some(record_path) = record_hints_path { + let hints = match executor::vm::execution::collect_hints(&program, private_inputs) { + Ok(hints) => hints, + Err(e) => { + eprintln!("Hint recording run failed: {e:?}"); + return ExitCode::FAILURE; + } + }; + let mut bytes = Vec::with_capacity(32 * hints.len()); + for slot in &hints { + bytes.extend_from_slice(slot); + } + if let Err(e) = std::fs::write(&record_path, &bytes) { + eprintln!("Failed to write hint arena file: {e}"); + return ExitCode::FAILURE; + } + eprintln!("Recorded {} hint slots to {:?}", hints.len(), record_path); + return ExitCode::SUCCESS; + } + + let hints = match read_hints(hints_path.as_ref()) { + Ok(hints) => hints, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + }; + // Accelerator invocation counts, tallied only in the plain streaming path // below (the flamegraph path drives execution inside the executor and does // not expose per-log data). `None` means "not counted", so the accel lines @@ -469,7 +565,7 @@ fn cmd_execute( total_cycles } else { - let mut executor = match Executor::new(&program, private_inputs) { + let mut executor = match Executor::new(&program, private_inputs, &hints) { Ok(e) => e, Err(e) => { eprintln!("Failed to create executor: {:?}", e); @@ -550,6 +646,7 @@ fn cmd_prove( time: bool, cycles: bool, elements: bool, + hints_path: Option, ) -> ExitCode { eprintln!("Reading ELF file..."); let elf_data = match std::fs::read(&elf_path) { @@ -568,11 +665,19 @@ fn cmd_prove( } }; + let hints = match read_hints(hints_path.as_ref()) { + Ok(hints) => hints, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + }; + // Pre-pass: execute once outside the timer to count dynamic instructions. // Mirrors SP1's cycle-count pass so both provers report the same kind of // number without inflating the measured proving time. let cycle_count = if cycles { - match count_cycles(&elf_data, &private_inputs) { + match count_cycles(&elf_data, &private_inputs, &hints) { Ok(count) => Some(count), Err(e) => { eprintln!("{e}"); @@ -585,7 +690,7 @@ fn cmd_prove( // Pre-pass: build traces and count field elements without running the proof. let element_count = if elements { - match prover::count_elements(&elf_data, &private_inputs) { + match prover::count_elements(&elf_data, &private_inputs, &hints) { Ok(counts) => Some(counts), Err(e) => { eprintln!("Failed to count elements: {:?}", e); @@ -620,6 +725,7 @@ fn cmd_prove( let proof = prover::prove_with_options_and_inputs( &elf_data, &private_inputs, + &hints, &opts, &Default::default(), ); @@ -740,6 +846,7 @@ fn cmd_prove_continuation( blowup: u8, time: bool, cycles: bool, + hints_path: Option, ) -> ExitCode { eprintln!("Reading ELF file..."); let elf_data = match std::fs::read(&elf_path) { @@ -758,8 +865,16 @@ fn cmd_prove_continuation( } }; + let hints = match read_hints(hints_path.as_ref()) { + Ok(hints) => hints, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + }; + let cycle_count = if cycles { - match count_cycles(&elf_data, &private_inputs) { + match count_cycles(&elf_data, &private_inputs, &hints) { Ok(count) => Some(count), Err(e) => { eprintln!("{e}"); @@ -800,6 +915,7 @@ fn cmd_prove_continuation( let bundle = match prover::continuation::prove_continuation( &elf_data, &private_inputs, + &hints, epoch_size_log2, &opts, ) { @@ -916,7 +1032,11 @@ fn cmd_verify_continuation( } } -fn cmd_count_elements(elf_path: PathBuf, private_input_path: Option) -> ExitCode { +fn cmd_count_elements( + elf_path: PathBuf, + private_input_path: Option, + hints_path: Option, +) -> ExitCode { let elf_data = match std::fs::read(&elf_path) { Ok(data) => data, Err(e) => { @@ -933,7 +1053,15 @@ fn cmd_count_elements(elf_path: PathBuf, private_input_path: Option) -> } }; - match prover::count_elements(&elf_data, &private_inputs) { + let hints = match read_hints(hints_path.as_ref()) { + Ok(hints) => hints, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + }; + + match prover::count_elements(&elf_data, &private_inputs, &hints) { Ok((main, aux)) => { println!("Elements: {}", main); println!("Aux elements (EF-cols): {}", aux); diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index ec36b0831..4a3206b82 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -64,38 +64,36 @@ impl Crypto for LambdaVmEcsmCrypto { // ── ECDSA secp256k1 recovery via the ECSM precompile ──────────────────────── -/// Obtain a 32-byte big-endian hint for `x_be` via the executor `hint` ecall -/// (the host computes the modular inverse / sqrt; the value is provable via the -/// prover's HINT table). The result is UNTRUSTED — the ecall adds no correctness -/// constraint, so every caller MUST verify it in-guest (`x·inv == 1`, `y² == x³+7`) -/// AND recompute in software on any verification failure. The hint is only ever -/// allowed to save work, never to change the answer: because the prover chooses the -/// bytes, an unverified-or-rejected-outright hint would let it steer a caller's -/// accept/reject outcome (e.g. force a valid signature to look invalid). See -/// [`scalar_inv`] / [`decompress_r`] for the fallback that closes that hole. +/// Fetch a hint for `(hint_id, x_be)` from the private-input hint arena +/// (positional — one slot per request). On an exhausted arena the request is +/// appended to the guest's hint request log (the recording pass of the +/// two-pass hint flow) and zeros are returned, which fail the caller's +/// in-guest verify and trigger its software fallback. +/// +/// The hint is UNTRUSTED — the prover chooses the arena bytes, so every caller +/// MUST verify it in-guest (`x·inv == 1`, `y² == x³+7`) AND recompute in +/// software on any verification failure. The hint is only ever allowed to save +/// work, never to change the answer: an unverified-or-rejected-outright hint +/// would let the prover steer a caller's accept/reject outcome (e.g. force a +/// valid signature to look invalid). See [`scalar_inv`] / [`decompress_r`] for +/// the fallback that closes that hole. #[cfg(target_arch = "riscv64")] fn get_hint(hint_id: usize, x_be: &[u8; 32]) -> [u8; 32] { - // 8-byte-aligned output buffer so the HINT table's four 8-byte writes land on the - // aligned memory path (MEMW_A) instead of the general MEMW path. An `[u8; 32]` on - // the stack is only 1-aligned, which forces the four writes onto the unaligned - // path and inflates the trace. - #[repr(C, align(8))] - struct Aligned32([u8; 32]); - let mut out = Aligned32([0u8; 32]); - lambda_vm_syscalls::syscalls::hint(hint_id, &mut out.0, x_be); - out.0 + lambda_vm_syscalls::syscalls::request_hint(hint_id, x_be).unwrap_or([0u8; 32]) } /// Scalar-field inverse `x⁻¹ mod n`. /// -/// On riscv64 the inverse is first requested from the untrusted `hint` ecall and -/// verified in-guest (`x·inv == 1`); **on any verification failure it is recomputed -/// in software.** `x⁻¹` exists for every `x` this is called with — the only caller, -/// `ecsm_ecrecover`, guarantees `r ≠ 0` before calling — so a failed verify can only -/// mean the host lied, and the software value is authoritative. This is what keeps -/// the result independent of the prover-chosen hint: a bad hint makes the guest do -/// more work, it can never change the answer, so it cannot turn a valid signature -/// into a recovery failure. Off-target (host) it inverts in software directly. +/// On riscv64 the inverse is read from the untrusted private-input **hint +/// arena** (positional — one slot per request) and verified in-guest +/// (`x·inv == 1`); **on any verification failure or an exhausted arena it is +/// recomputed in software.** `x⁻¹` exists for every `x` this is called with — +/// the only caller, `ecsm_ecrecover`, guarantees `r ≠ 0` before calling — so a +/// failed verify can only mean the host lied, and the software value is +/// authoritative. This is what keeps the result independent of the +/// prover-chosen hint: a bad hint makes the guest do more work, it can never +/// change the answer, so it cannot turn a valid signature into a recovery +/// failure. Off-target (host) it inverts in software directly. fn scalar_inv(x: &Scalar) -> Option { #[cfg(target_arch = "riscv64")] { @@ -133,16 +131,16 @@ where /// Decompress R from its x-coordinate + parity. /// -/// On riscv64 the square root `y = sqrt(x³+7)` is first requested from the untrusted -/// `hint` ecall and verified in-guest (`y² == x³+7`), with parity selection; **on any -/// verification failure the point is recomputed with the software -/// `AffinePoint::decompress`.** Unlike the inverse, a failure here is *not* -/// necessarily a lying host: a genuine non-residue (an invalid signature) has no -/// root and must legitimately yield `None`. So the fallback is the authoritative -/// software decompress, which returns `Some` for a residue and `None` for a -/// non-residue regardless of the prover-chosen hint — the hint can only save work, -/// never steer the accept/reject outcome. Off-target it uses the software -/// decompress directly. +/// On riscv64 the square root `y = sqrt(x³+7)` is read from the untrusted +/// private-input **hint arena** and verified in-guest (`y² == x³+7`), with +/// parity selection; **on any verification failure or an exhausted arena the +/// point is recomputed with the software `AffinePoint::decompress`.** Unlike +/// the inverse, a failure here is *not* necessarily a lying host: a genuine +/// non-residue (an invalid signature) has no root and must legitimately yield +/// `None`. So the fallback is the authoritative software decompress, which +/// returns `Some` for a residue and `None` for a non-residue regardless of the +/// prover-chosen hint — the hint can only save work, never steer the +/// accept/reject outcome. Off-target it uses the software decompress directly. fn decompress_r(r_bytes: &FieldBytes, y_is_odd: bool) -> Option { #[cfg(target_arch = "riscv64")] { @@ -337,12 +335,14 @@ fn ecsm_oracle(x: &FieldElement, k: &Scalar) -> Option { /// Base-field inverse `x⁻¹ mod p`. /// -/// On riscv64 the inverse is first requested from the untrusted `hint` ecall and -/// verified in-guest (`x·inv == 1`); **on any verification failure it is recomputed -/// in software.** A bad hint can only cost the guest extra work, never change the -/// answer — it cannot steer a caller's accept/reject outcome. Off-target it inverts -/// in software directly. Returns `None` only for a genuinely non-invertible input -/// (`x = 0`), which the callers' degeneracy guards already exclude. +/// On riscv64 the inverse is read from the untrusted private-input **hint +/// arena** (positional — one slot per request) and verified in-guest +/// (`x·inv == 1`); **on any verification failure or an exhausted arena it is +/// recomputed in software.** A bad or missing hint can only cost the guest +/// extra work, never change the answer — it cannot steer a caller's +/// accept/reject outcome. Off-target it inverts in software directly. Returns +/// `None` only for a genuinely non-invertible input (`x = 0`), which the +/// callers' degeneracy guards already exclude. #[cfg(any(target_arch = "riscv64", test))] fn field_inv(x: &FieldElement) -> Option { #[cfg(target_arch = "riscv64")] diff --git a/crypto/ethrex-crypto/src/tests/hint_tests.rs b/crypto/ethrex-crypto/src/tests/hint_tests.rs index ace59f208..6bfb413e5 100644 --- a/crypto/ethrex-crypto/src/tests/hint_tests.rs +++ b/crypto/ethrex-crypto/src/tests/hint_tests.rs @@ -1,16 +1,16 @@ //! Host tests for the untrusted-hint verify-then-fallback paths (`scalar_inv`, //! `field_inv`, `decompress_r`). //! -//! The guest asks the (untrusted, prover-chosen) `hint` ecall for a modular -//! inverse / square root, then verifies it in-circuit. These tests inject the -//! oracle directly — an *honest* oracle (matching the executor's `compute_hint`) -//! and a *lying* one — and assert the software fallback makes the result identical -//! either way. That is the property the whole hint design rests on: because the -//! prover chooses the hinted bytes and the ecall adds no correctness constraint, a -//! bad hint must only be able to make the guest do more work, never change its -//! accept/reject outcome. On the guest this code is `cfg(target_arch = "riscv64")`; -//! the `test` gate on `*_with_oracle` is what lets CI compile and exercise it on -//! the host. +//! The guest reads each modular inverse / square root from the untrusted, +//! prover-chosen private-input **hint arena**, then verifies it in-circuit. +//! These tests inject the oracle directly — an *honest* oracle (matching the +//! executor's `compute_hint`) and a *lying* one — and assert the software +//! fallback makes the result identical either way. That is the property the +//! whole hint design rests on: because the prover chooses the arena bytes and +//! they are unconstrained, a bad hint must only be able to make the guest do +//! more work, never change its accept/reject outcome. On the guest this code +//! is `cfg(target_arch = "riscv64")`; the `test` gate on `*_with_oracle` is +//! what lets CI compile and exercise it on the host. use crate::*; diff --git a/executor/programs/rust/hint_min/.cargo/config.toml b/executor/programs/rust/ecrecover_hints/.cargo/config.toml similarity index 100% rename from executor/programs/rust/hint_min/.cargo/config.toml rename to executor/programs/rust/ecrecover_hints/.cargo/config.toml diff --git a/executor/programs/rust/ecrecover_hints/Cargo.lock b/executor/programs/rust/ecrecover_hints/Cargo.lock new file mode 100644 index 000000000..56d252d8c --- /dev/null +++ b/executor/programs/rust/ecrecover_hints/Cargo.lock @@ -0,0 +1,1011 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown", + "itertools", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "arrayvec", + "digest", + "educe", + "itertools", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff", + "ark-serialize", + "ark-std", + "educe", + "fnv", + "hashbrown", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "arrayvec", + "digest", + "num-bigint", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.7", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-affine-constructors#78cad0378b17fc3157b83f514be192bf46edf9a1" +dependencies = [ + "digest", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ecrecover_hints" +version = "0.1.0" +dependencies = [ + "ethrex-crypto", + "lambda-vm-ethrex-crypto", + "lambda-vm-syscalls", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "enum-ordinalize" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89dd01549b09589510cf0647475075d12071456586d70f5c75c98ae2a5537677" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65863d15a4ce2888bd2f0f543cc963d3879c3a022c8ee43f6141d479a3ac815" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "ethereum-types" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab15ed80916029f878e0267c3a9f92b67df55e79af370bf66199059ae2b4ee3" +dependencies = [ + "fixed-hash", + "primitive-types", + "uint", +] + +[[package]] +name = "ethrex-crypto" +version = "13.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "bls12_381", + "ethereum-types", + "ff", + "hex-literal", + "k256", + "num-bigint", + "p256", + "ripemd", + "sha2", + "thiserror 2.0.20", + "tiny-keccak", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "lambda-vm-ethrex-crypto" +version = "0.1.0" +dependencies = [ + "ethrex-crypto", + "k256", + "lambda-vm-syscalls", +] + +[[package]] +name = "lambda-vm-syscalls" +version = "0.1.0" +dependencies = [ + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "rand 0.9.5", + "riscv", + "thiserror 1.0.69", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15600a7d856470b7d278b3fe0e311fe28c2526348549f8ef2ff7db3299c87f5" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uint" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9227a75a5a540a464c832ad4a4195dbdbecd8787610a56262721fde6f04f90" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/executor/programs/rust/ecrecover_hints/Cargo.toml b/executor/programs/rust/ecrecover_hints/Cargo.toml new file mode 100644 index 000000000..c4980d5cf --- /dev/null +++ b/executor/programs/rust/ecrecover_hints/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] + +[profile.release] +lto = "thin" + +[package] +name = "ecrecover_hints" +version = "0.1.0" +edition = "2024" + +[dependencies] +lambda-vm-syscalls = { path = "../../../../syscalls" } +lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } +# The `Crypto` trait the guest calls through. Same rev + default-features=false +# as lambda-vm-ethrex-crypto's own dep, so feature unification adds nothing. +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-crypto", default-features = false } diff --git a/executor/programs/rust/ecrecover_hints/src/main.rs b/executor/programs/rust/ecrecover_hints/src/main.rs new file mode 100644 index 000000000..2dc016044 --- /dev/null +++ b/executor/programs/rust/ecrecover_hints/src/main.rs @@ -0,0 +1,38 @@ +//! ecrecover measurement guest for the hint arena: N secp256k1 recoveries via +//! the LambdaVM crypto provider, whose inverses/sqrts come from the +//! private-input hint arena (no hint ecall). +//! +//! Private input layout: `[u32 LE count]` then `count` records of +//! `sig(64) || recid(1) || msg(32)`. The recovered addresses are XOR-folded and +//! committed. Hint consumption is positional: per recovery the guest requests +//! sqrt (decompress), the batched field inverse (lincomb), and the scalar +//! inverse, in that order — the host's arena must follow the same order. + +use ethrex_crypto::Crypto; +use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; +use lambda_vm_syscalls as syscalls; + +pub fn main() { + let input = syscalls::syscalls::get_private_input(); + assert!(input.len() >= 4, "input too short for count"); + let count = u32::from_le_bytes(input[0..4].try_into().unwrap()) as usize; + assert_eq!(input.len(), 4 + count * 97, "input length mismatch"); + + let crypto = LambdaVmEcsmCrypto; + let mut acc = [0u8; 32]; + let mut off = 4; + for _ in 0..count { + let sig: &[u8; 64] = input[off..off + 64].try_into().unwrap(); + let recid = input[off + 64]; + let msg: &[u8; 32] = input[off + 65..off + 97].try_into().unwrap(); + let addr = crypto + .secp256k1_ecrecover(sig, recid, msg) + .expect("ecrecover failed"); + for i in 0..32 { + acc[i] ^= addr[i]; + } + off += 97; + } + + syscalls::syscalls::commit(&acc); +} diff --git a/executor/programs/rust/hint_multi/.cargo/config.toml b/executor/programs/rust/hint_arena/.cargo/config.toml similarity index 100% rename from executor/programs/rust/hint_multi/.cargo/config.toml rename to executor/programs/rust/hint_arena/.cargo/config.toml diff --git a/executor/programs/rust/hint_min/Cargo.lock b/executor/programs/rust/hint_arena/Cargo.lock similarity index 72% rename from executor/programs/rust/hint_min/Cargo.lock rename to executor/programs/rust/hint_arena/Cargo.lock index cc02eff98..3b6eb2fd1 100644 --- a/executor/programs/rust/hint_min/Cargo.lock +++ b/executor/programs/rust/hint_arena/Cargo.lock @@ -2,42 +2,18 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "const-default" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" - [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "embedded-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" -dependencies = [ - "const-default", - "critical-section", - "linked_list_allocator", - "rlsf", -] - [[package]] name = "embedded-hal" version = "1.0.0" @@ -68,7 +44,7 @@ dependencies = [ ] [[package]] -name = "hint_min" +name = "hint_arena" version = "0.1.0" dependencies = [ "lambda-vm-syscalls", @@ -78,7 +54,6 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ - "embedded-alloc", "getrandom 0.2.17", "getrandom 0.3.4", "lazy_static", @@ -99,12 +74,6 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "linked_list_allocator" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" - [[package]] name = "paste" version = "1.0.15" @@ -194,7 +163,7 @@ checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -203,49 +172,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" -[[package]] -name = "rlsf" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" -dependencies = [ - "cfg-if", - "const-default", - "libc", - "rustversion", - "svgbobdoc", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "svgbobdoc" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" -dependencies = [ - "base64", - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-width", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.119" @@ -274,7 +200,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -283,12 +209,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -312,20 +232,20 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] diff --git a/executor/programs/rust/hint_multi/Cargo.toml b/executor/programs/rust/hint_arena/Cargo.toml similarity index 86% rename from executor/programs/rust/hint_multi/Cargo.toml rename to executor/programs/rust/hint_arena/Cargo.toml index faacdb38e..3d361e1b9 100644 --- a/executor/programs/rust/hint_multi/Cargo.toml +++ b/executor/programs/rust/hint_arena/Cargo.toml @@ -1,7 +1,7 @@ [workspace] [package] -name = "hint_multi" +name = "hint_arena" version = "0.1.0" edition = "2024" diff --git a/executor/programs/rust/hint_arena/src/main.rs b/executor/programs/rust/hint_arena/src/main.rs new file mode 100644 index 000000000..8583f9cb2 --- /dev/null +++ b/executor/programs/rust/hint_arena/src/main.rs @@ -0,0 +1,35 @@ +//! Hint-arena guest: reads THREE 32-byte hints from the private-input hint +//! arena (no ecall — ordinary aligned loads from the memory-mapped region), +//! XOR-accumulates them, and commits the accumulator. +//! +//! Arena counterpart to `hint_multi`: same three logical hints (one per +//! selector's worth of values), but the bytes arrive as prover-chosen +//! private-input page data instead of executor-written ecall outputs, so the +//! proof needs no HINT table rows at all — the guest's reads are ordinary +//! MEMR loads chained to the private-input pages. + +use lambda_vm_syscalls as syscalls; + +pub fn main() { + let mut acc = [0u8; 32]; + + assert_eq!( + syscalls::syscalls::hint_count(), + 3, + "the host must supply exactly three hint slots" + ); + + for _ in 0..3 { + let hint = syscalls::syscalls::next_hint().expect("arena exhausted"); + for i in 0..32 { + acc[i] ^= hint[i]; + } + } + + // Positional consumption is one slot per request: a fourth request runs + // past the end and must yield None (the caller's cue to fall back to + // software), never a desynced stream. + assert!(syscalls::syscalls::next_hint().is_none()); + + syscalls::syscalls::commit(&acc); +} diff --git a/executor/programs/rust/hint_min/Cargo.toml b/executor/programs/rust/hint_min/Cargo.toml deleted file mode 100644 index 4bfe4614f..000000000 --- a/executor/programs/rust/hint_min/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[workspace] - -[package] -name = "hint_min" -version = "0.1.0" -edition = "2024" - -[dependencies] -lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/programs/rust/hint_min/src/main.rs b/executor/programs/rust/hint_min/src/main.rs deleted file mode 100644 index 833a01b8a..000000000 --- a/executor/programs/rust/hint_min/src/main.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Minimal P0 guest for the Hint prover table: one `hint` ecall (field inverse of -//! a small value) + commit the result. No in-guest verify — this exercises exactly -//! the Hint table's bus surface (Ecall receive, the register read binding `out_addr` -//! to `a2`, four 8-byte MEMW writes and the output range checks; the input read is -//! deliberately not modelled) so we can get prove→verify to balance before scaling -//! to ethrex. -//! -//! Buffers are 8-byte aligned so the writes land in the aligned MEMW table — the same -//! choice the ethrex call site makes (`get_hint` in `crypto/ethrex-crypto` wraps its -//! output in an `align(8)` buffer). Alignment is a preference rather than a -//! requirement — `classify_memw` routes unaligned accesses to the general MEMW table. - -use lambda_vm_syscalls as syscalls; - -#[repr(align(8))] -struct Aligned32([u8; 32]); - -pub fn main() { - // input = 3 (big-endian), a valid invertible field element. - let mut x = Aligned32([0u8; 32]); - x.0[31] = 3; - let mut inv = Aligned32([0u8; 32]); - - syscalls::syscalls::hint( - syscalls::syscalls::HINT_FIELD_INV, - &mut inv.0, - &x.0, - ); - - syscalls::syscalls::commit(&inv.0); -} diff --git a/executor/programs/rust/hint_multi/Cargo.lock b/executor/programs/rust/hint_multi/Cargo.lock deleted file mode 100644 index 9803c875a..000000000 --- a/executor/programs/rust/hint_multi/Cargo.lock +++ /dev/null @@ -1,331 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "const-default" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "embedded-alloc" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" -dependencies = [ - "const-default", - "critical-section", - "linked_list_allocator", - "rlsf", -] - -[[package]] -name = "embedded-hal" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", -] - -[[package]] -name = "hint_multi" -version = "0.1.0" -dependencies = [ - "lambda-vm-syscalls", -] - -[[package]] -name = "lambda-vm-syscalls" -version = "0.1.0" -dependencies = [ - "embedded-alloc", - "getrandom 0.2.17", - "getrandom 0.3.4", - "lazy_static", - "rand", - "riscv", - "thiserror", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "linked_list_allocator" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "riscv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" -dependencies = [ - "critical-section", - "embedded-hal", - "paste", - "riscv-macros", - "riscv-pac", -] - -[[package]] -name = "riscv-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "riscv-pac" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" - -[[package]] -name = "rlsf" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" -dependencies = [ - "cfg-if", - "const-default", - "libc", - "rustversion", - "svgbobdoc", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "svgbobdoc" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" -dependencies = [ - "base64", - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-width", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] diff --git a/executor/programs/rust/hint_multi/src/main.rs b/executor/programs/rust/hint_multi/src/main.rs deleted file mode 100644 index 2a03a644d..000000000 --- a/executor/programs/rust/hint_multi/src/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Multi-hint P0/P2 guest for the Hint prover table: THREE `hint` ecalls, one per -//! selector, each result read back with ordinary `LOAD`s (XOR-accumulated) and the -//! accumulator committed. -//! -//! Complements `hint_min` (one hint, read back via `commit`): this exercises the -//! parts the ethrex consumer relies on that a single-call guest does not — -//! **multiple real HINT rows** (padded to a power of two), **all three selectors** -//! (`HINT_FIELD_INV` / `HINT_SCALAR_INV` / `HINT_FIELD_SQRT`, so the AIR's -//! `selector < 3` range-check is exercised at every accepted value rather than only -//! at 0) and **read-back of the hinted output via normal `LOAD` instructions** -//! (whose MEMW reads must chain to the HINT table's writes). Buffers are 8-byte -//! aligned so the writes land in the aligned MEMW table. - -use lambda_vm_syscalls as syscalls; - -#[repr(align(8))] -struct Aligned32([u8; 32]); - -pub fn main() { - let mut acc = Aligned32([0u8; 32]); - - // One call per selector. 4 is a quadratic residue mod p, so the sqrt hint has a - // real root rather than the zeros `compute_hint` returns on a numeric failure. - for (hint_id, seed) in [ - (syscalls::syscalls::HINT_FIELD_INV, 3u8), - (syscalls::syscalls::HINT_SCALAR_INV, 5u8), - (syscalls::syscalls::HINT_FIELD_SQRT, 4u8), - ] { - let mut x = Aligned32([0u8; 32]); - x.0[31] = seed; - let mut out = Aligned32([0u8; 32]); - - syscalls::syscalls::hint(hint_id, &mut out.0, &x.0); - - // Read the hinted output back via ordinary loads and fold it in, so the - // MEMW reads of `out` must chain to the HINT table's writes. - for i in 0..32 { - acc.0[i] ^= out.0[i]; - } - } - - syscalls::syscalls::commit(&acc.0); -} diff --git a/executor/src/flamegraph.rs b/executor/src/flamegraph.rs index 2abf14942..11e5d5b38 100644 --- a/executor/src/flamegraph.rs +++ b/executor/src/flamegraph.rs @@ -353,7 +353,9 @@ pub fn run_with_flamegraph( ) -> (FlamegraphGenerator, Result) { let symbols = SymbolTable::parse(elf_bytes); let mut generator = FlamegraphGenerator::new(symbols, program.entry_point); - let mut executor = match Executor::new(program, private_inputs) { + // Profiling entry point: no hint arena (pass `&[]`); hint-consuming programs + // fall back to software paths here, which only makes profiles conservative. + let mut executor = match Executor::new(program, private_inputs, &[]) { Ok(executor) => executor, Err(e) => return (generator, Err(e.into())), }; diff --git a/executor/src/main.rs b/executor/src/main.rs index 283085fd1..dd69715df 100644 --- a/executor/src/main.rs +++ b/executor/src/main.rs @@ -9,7 +9,7 @@ fn main() -> Result<(), ExecutorError> { let elf_data = std::fs::read("./program_artifacts/rust/ethrex.elf").unwrap(); let inputs = fs::read("tests/ethrex_simple_tx.bin").unwrap(); let program = Elf::load(&elf_data).unwrap(); - let executor = Executor::new(&program, inputs)?; + let executor = Executor::new(&program, inputs, &[])?; executor.run()?; Ok(()) } diff --git a/executor/src/tests/hint_tests.rs b/executor/src/tests/hint_tests.rs deleted file mode 100644 index 2ed8c096c..000000000 --- a/executor/src/tests/hint_tests.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! Tests for the non-constraining `Hint` syscall. - -use crate::vm::instruction::decoding::Instruction; -use crate::vm::instruction::execution::{ - ExecutionError, HINT_FIELD_INV, HINT_FIELD_SQRT, HINT_SCALAR_INV, HINT_SYSCALL_NUMBER, - compute_hint, -}; -use crate::vm::memory::Memory; -use crate::vm::registers::Registers; - -fn write_u256(memory: &mut Memory, addr: u64, bytes: &[u8; 32]) { - for i in 0..4 { - let mut dw = [0u8; 8]; - dw.copy_from_slice(&bytes[i * 8..i * 8 + 8]); - memory - .store_doubleword(addr + (i as u64) * 8, u64::from_le_bytes(dw)) - .unwrap(); - } -} - -fn read_u256(memory: &Memory, addr: u64) -> [u8; 32] { - let mut out = [0u8; 32]; - for i in 0..4 { - let dw = memory.load_doubleword(addr + (i as u64) * 8).unwrap(); - out[i * 8..i * 8 + 8].copy_from_slice(&dw.to_le_bytes()); - } - out -} - -/// Runs one `Hint` ecall with the given operand addresses, returning the 32 bytes -/// written at `out_addr`. -fn run_hint_at( - hint_id: u64, - in_addr: u64, - out_addr: u64, - input: &[u8; 32], -) -> Result<[u8; 32], ExecutionError> { - let mut memory = Memory::default(); - let mut registers = Registers::default(); - let mut pc = 0u64; - - write_u256(&mut memory, in_addr, input); - registers.write(17, HINT_SYSCALL_NUMBER).unwrap(); - registers.write(10, hint_id).unwrap(); - registers.write(11, in_addr).unwrap(); - registers.write(12, out_addr).unwrap(); - Instruction::EcallEbreak.run(&mut pc, &mut registers, &mut memory)?; - Ok(read_u256(&memory, out_addr)) -} - -/// The base-field inverse hint round-trips through guest memory, big-endian in and -/// out, and matches `compute_hint` (the value the prover recomputes). -#[test] -fn hint_syscall_writes_the_field_inverse() { - let mut input = [0u8; 32]; - input[31] = 3; // 3, big-endian - - let out = run_hint_at(HINT_FIELD_INV, 0x1000, 0x2000, &input).expect("hint must run"); - assert_eq!(out, compute_hint(HINT_FIELD_INV, &input)); - - // 3 · 3⁻¹ ≡ 1 (mod p) — the same check the guest performs on the untrusted value. - let three: k256::FieldElement = - Option::from(k256::FieldElement::from_bytes(&input.into())).unwrap(); - let inv: k256::FieldElement = - Option::from(k256::FieldElement::from_bytes(&out.into())).unwrap(); - assert_eq!( - (three * inv).to_bytes(), - k256::FieldElement::ONE.to_bytes(), - "hinted inverse must satisfy x·inv == 1" - ); -} - -/// Both operands must keep their 32-byte range inside the lower address limb: the -/// HINT table sends the output writes as `[out_addr_lo + 8i, out_addr_hi]`, which -/// cannot represent a carry into the high limb, so a straddling operand would make -/// the trace unprovable. The executor rejects it upfront instead. -#[test] -fn hint_syscall_rejects_address_overflow() { - let input = [0u8; 32]; - // Last accessed byte is at +31, so the first rejected base is 2^32 - 31. - for (in_addr, out_addr) in [ - (0x1000, 0xFFFF_FFE8), - (0xFFFF_FFE8, 0x2000), - (0x1000, 0xFFFF_FFE1), - (0xFFFF_FFE1, 0x2000), - (0x1000, 0xFFFF_FFFF), - ] { - let err = run_hint_at(HINT_FIELD_INV, in_addr, out_addr, &input) - .expect_err("straddling operand must be rejected"); - assert!( - matches!(err, ExecutionError::HintAddressOverflow), - "expected address overflow for in={in_addr:#x}, out={out_addr:#x}, got {err:?}" - ); - } -} - -/// The boundary case: an operand ending exactly on the last byte of the limb is -/// still representable and must be accepted. -#[test] -fn hint_syscall_accepts_operand_ending_at_the_limb_boundary() { - let input = [0u8; 32]; - // 2^32 - 32: last byte lands at 2^32 - 1, the largest in-limb address. - run_hint_at(HINT_FIELD_INV, 0x1000, 0xFFFF_FFE0, &input) - .expect("operand ending at the limb boundary must run"); - run_hint_at(HINT_FIELD_INV, 0xFFFF_FFE0, 0x2000, &input) - .expect("operand ending at the limb boundary must run"); -} - -/// The scalar-field inverse hint (mod n) round-trips through guest memory and -/// satisfies `x·inv == 1 (mod n)` — the check the guest performs on the untrusted -/// value. Used by production ecrecover (`r⁻¹`). -#[test] -fn hint_syscall_writes_the_scalar_inverse() { - use k256::elliptic_curve::PrimeField; - - let mut input = [0u8; 32]; - input[31] = 3; // 3, big-endian - - let out = run_hint_at(HINT_SCALAR_INV, 0x1000, 0x2000, &input).expect("hint must run"); - assert_eq!(out, compute_hint(HINT_SCALAR_INV, &input)); - - let three: k256::Scalar = Option::from(k256::Scalar::from_repr(input.into())).unwrap(); - let inv: k256::Scalar = Option::from(k256::Scalar::from_repr(out.into())).unwrap(); - assert_eq!( - (three * inv).to_bytes(), - k256::Scalar::ONE.to_bytes(), - "hinted scalar inverse must satisfy x·inv == 1 (mod n)" - ); -} - -/// The base-field sqrt hint (mod p) round-trips and satisfies `y² == rhs (mod p)`. -/// Used by production ecrecover (decompressing R). `4 = 2²` is a residue. -#[test] -fn hint_syscall_writes_the_field_sqrt() { - let mut input = [0u8; 32]; - input[31] = 4; // rhs = 4, big-endian - - let out = run_hint_at(HINT_FIELD_SQRT, 0x1000, 0x2000, &input).expect("hint must run"); - assert_eq!(out, compute_hint(HINT_FIELD_SQRT, &input)); - - let rhs: k256::FieldElement = - Option::from(k256::FieldElement::from_bytes(&input.into())).unwrap(); - let y: k256::FieldElement = Option::from(k256::FieldElement::from_bytes(&out.into())).unwrap(); - assert_eq!( - y.square().to_bytes(), - rhs.to_bytes(), - "hinted sqrt must satisfy y² == rhs (mod p)" - ); -} - -/// An unknown `hint_id` is rejected up front. Silently writing zeros would be -/// indistinguishable from a legitimate numeric failure and — because the guest reads -/// the value back — could let a prover-chosen selector steer a caller's accept/reject -/// outcome. The executor traps so a guest bug surfaces loudly. `HINT_FIELD_SQRT = 2` -/// is the last known selector, so 3 is the first unknown one. -#[test] -fn hint_syscall_rejects_an_unknown_selector() { - let mut input = [0u8; 32]; - input[31] = 3; - for bad in [3u64, 100, u64::MAX] { - let err = run_hint_at(bad, 0x1000, 0x2000, &input).expect_err("unknown selector must trap"); - assert!( - matches!(err, ExecutionError::HintUnknownSelector(id) if id == bad), - "expected HintUnknownSelector({bad}), got {err:?}" - ); - } -} - -/// The guest's `lambda-vm-syscalls` crate re-declares the selectors as `usize`, -/// linked to the `u64` copies here only by a comment. A divergence is **silent**: -/// the ecall would trap on an unknown selector, or — worse for the selectors that -/// stay in range — hand back the wrong function's answer, which the guest's -/// verify-then-fallback swallows as "the host lied" and quietly recomputes in -/// software. Nothing fails; the guest just runs ~2000× slower for the right result. -/// This test is the only thing that would notice. -/// -/// `is_valid_hint_selector`'s const-assert pins the AIR's range-check to this crate's -/// accepted set, but nothing ties the *guest's* copy of the selectors to it — that is -/// a third declaration, in a crate the workspace excludes, and this is what binds it. -/// -/// The syscall number itself is not asserted here: the guest's copy is -/// `#[cfg(target_arch = "riscv64")]` and private, so it does not exist in a host -/// build. It is covered indirectly — a wrong number makes every `hint` guest fail -/// to prove, which `test_prove_hint_min_rust_guest` catches loudly. -#[cfg(test)] -mod guest_constant_sync { - use super::{HINT_FIELD_INV, HINT_FIELD_SQRT, HINT_SCALAR_INV}; - use lambda_vm_syscalls::syscalls as guest; - - #[test] - fn hint_selectors_match_the_guest() { - assert_eq!(guest::HINT_FIELD_INV as u64, HINT_FIELD_INV); - assert_eq!(guest::HINT_SCALAR_INV as u64, HINT_SCALAR_INV); - assert_eq!(guest::HINT_FIELD_SQRT as u64, HINT_FIELD_SQRT); - } -} diff --git a/executor/src/tests/mod.rs b/executor/src/tests/mod.rs index 244447b22..456607433 100644 --- a/executor/src/tests/mod.rs +++ b/executor/src/tests/mod.rs @@ -1,5 +1,4 @@ pub mod ecsm_tests; pub mod flamegraph_tests; -pub mod hint_tests; pub mod keccak_tests; pub mod memory_tests; diff --git a/executor/src/vm/execution.rs b/executor/src/vm/execution.rs index dc0660178..24bb85101 100644 --- a/executor/src/vm/execution.rs +++ b/executor/src/vm/execution.rs @@ -25,6 +25,10 @@ pub struct ExecutionResult { /// Predecoded instructions map (pc -> instruction) /// Use this to look up instructions by their PC from the logs pub instructions: U64HashMap, + /// `(hint_id, input)` pairs the guest appended to the hint request log + /// (arena exhausted), in request order — the recording pass of the two-pass + /// hint flow. Empty when the arena covered every request. + pub hint_requests: Vec<(u64, [u8; 32])>, } /// Size of each log chunk - balances memory usage vs callback overhead @@ -51,9 +55,13 @@ pub struct Executor { } impl Executor { - pub fn new(program: &Elf, private_inputs: Vec) -> Result { + pub fn new( + program: &Elf, + private_inputs: Vec, + hints: &[[u8; 32]], + ) -> Result { let mut memory = Memory::default(); - memory.store_private_inputs(private_inputs)?; + memory.store_private_inputs(private_inputs, hints)?; let instructions = InstructionCache::new(&program.data)?; load_program(&program.data, &mut memory)?; @@ -163,6 +171,7 @@ impl Executor { return_values: self.get_return_values()?, logs, instructions: self.instructions.into_instruction_map(), + hint_requests: self.memory.hint_requests()?, }) } @@ -190,6 +199,33 @@ impl Executor { } } +/// Record the guest's hint requests and answer them — the host half of the +/// two-pass hint flow for guests whose hints are not known before the run. +/// +/// The program runs once with an empty hint arena: every +/// `syscalls::syscalls::request_hint` misses, is appended to the guest's +/// request log, and the guest recomputes in software (correct, just slower). +/// Each logged `(hint_id, input)` is then answered with +/// [`crate::vm::instruction::execution::compute_hint`], and the answers — in +/// request order — are returned as the hint arena for the second, provable +/// pass: pass them as `hints` to [`Executor::new`] (or the prover's `prove_*` +/// functions) together with the same program and private input. Both passes +/// produce the same committed output; the arena only changes how cheaply the +/// guest gets there. A guest that requests nothing yields an empty arena. +pub fn collect_hints( + program: &Elf, + private_inputs: Vec, +) -> Result, ExecutorError> { + let result = Executor::new(program, private_inputs, &[])?.run()?; + Ok(result + .hint_requests + .iter() + .map(|(hint_id, input)| { + crate::vm::instruction::execution::compute_hint(*hint_id, input) + }) + .collect()) +} + fn load_program(segments: &[crate::elf::Segment], memory: &mut Memory) -> Result<(), MemoryError> { for segment in segments { for (i, inst) in segment.values.iter().enumerate() { diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index 592af95e8..806095b4d 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -16,9 +16,6 @@ pub enum SyscallNumbers { Halt = 93, // Placeholder discriminant. The actual syscall value is ECSM_SYSCALL_NUMBER. Ecsm = 94, - // Placeholder discriminant. The actual syscall value is HINT_SYSCALL_NUMBER. - // Non-constraining hint (host computes modular inverse/sqrt, guest verifies). - Hint = 95, } /// Syscall number for KeccakPermute (u64::MAX - 1 = 0xFFFF_FFFF_FFFF_FFFE). @@ -34,37 +31,24 @@ const KECCAK_STATE_BYTES: u64 = 25 * 8; /// bus as `[lo32, hi32] = [2^32 - 11, 2^32 - 1]`. pub const ECSM_SYSCALL_NUMBER: u64 = u64::MAX - 10; -/// Syscall number for the non-constraining `Hint` ecall. -/// -/// The host computes a modular inverse or square root and writes it back to the -/// guest, which MUST verify it (e.g. `x·inv == 1`) and recompute in software on a -/// verification failure. The ecall adds no in-circuit correctness constraint of its -/// own — it lets the guest replace an expensive computation with a cheap check, -/// without letting the (prover-chosen) hinted value change the guest's result. -pub const HINT_SYSCALL_NUMBER: u64 = u64::MAX - 30; - -/// Hint operation selector passed in `a0`. +/// Hint operation selector passed to [`compute_hint`]. These are also the +/// request-log `hint_id`s the guest writes with `syscalls::syscalls::request_hint`. pub const HINT_FIELD_INV: u64 = 0; // secp256k1 base-field inverse (mod p) pub const HINT_SCALAR_INV: u64 = 1; // secp256k1 scalar-field inverse (mod n) pub const HINT_FIELD_SQRT: u64 = 2; // secp256k1 base-field square root -/// One past the largest valid hint selector. The prover's HINT table range-checks -/// `a0 < HINT_SELECTOR_BOUND` on the ALU bus to accept exactly the set -/// [`is_valid_hint_selector`] accepts, so both live here rather than being restated -/// independently in the AIR. +/// One past the largest valid hint selector. Kept in parity with the request-log +/// `hint_id` constants in the `syscalls` crate (`HINT_FIELD_INV` / `HINT_SCALAR_INV` +/// / `HINT_FIELD_SQRT`). pub const HINT_SELECTOR_BOUND: u64 = 3; -/// Whether `hint_id` names a hint [`compute_hint`] can produce. The ecall rejects -/// anything else up front with [`ExecutionError::HintUnknownSelector`]. +/// Whether `hint_id` names a hint [`compute_hint`] can produce. pub const fn is_valid_hint_selector(hint_id: u64) -> bool { matches!(hint_id, HINT_FIELD_INV | HINT_SCALAR_INV | HINT_FIELD_SQRT) } -// The AIR's range-check and the executor's accepted set must denote the same set: every -// selector below the bound is valid, and the bound itself is not. Appending a selector -// without moving the bound (or vice versa) fails to compile here, instead of making the -// HINT table assert `LT(selector, bound) = 1` against an LT row the builder emits as 0 — -// an unbalanced ALU bus with no algebraic pointer to the cause. +// Every selector below the bound is valid, and the bound itself is not. Appending a +// selector without moving the bound (or vice versa) fails to compile here. const _: () = { let mut id = 0; while id < HINT_SELECTOR_BOUND { @@ -88,7 +72,6 @@ impl TryFrom for SyscallNumbers { 93 => Ok(SyscallNumbers::Halt), v if v == KECCAK_SYSCALL_NUMBER => Ok(SyscallNumbers::KeccakPermute), v if v == ECSM_SYSCALL_NUMBER => Ok(SyscallNumbers::Ecsm), - v if v == HINT_SYSCALL_NUMBER => Ok(SyscallNumbers::Hint), _ => Err(()), } } @@ -112,8 +95,7 @@ impl SyscallNumbers { SyscallNumbers::Print | SyscallNumbers::Panic | SyscallNumbers::Commit - | SyscallNumbers::Halt - | SyscallNumbers::Hint => None, + | SyscallNumbers::Halt => None, } } } @@ -140,20 +122,17 @@ fn store_u256_le(memory: &mut Memory, addr: u64, bytes: &[u8; 32]) -> Result<(), /// Compute a non-constraining hint (modular inverse / sqrt) with the same k256 /// arithmetic the guest verifies against. Input/output are 32-byte big-endian, -/// k256's own serialization — unlike the ECSM ABI, which is little-endian because -/// its chip consumes little-endian limbs. The HINT table only copies these bytes -/// into memory writes, so the order is free to match the consumers. +/// k256's own serialization. This is the host-side answer to a request logged by +/// the guest's `syscalls::syscalls::request_hint`: the host runs it over +/// [`crate::vm::memory::Memory::hint_requests`] and appends the results to the +/// hint arena in the private-input region for a second pass. /// /// On a numeric failure (non-canonical input, no inverse/sqrt) returns zeros. This /// is NOT a loud failure and must not be treated as one: the guest's in-circuit /// verify rejects the value and recomputes it in software (see the `ethrex-crypto` /// crate), so a zero/garbage hint only costs the guest extra work — it can never -/// change the guest's result. An *unknown* `hint_id` never reaches here: the ecall -/// dispatch rejects it up front with [`ExecutionError::HintUnknownSelector`], so the -/// `_` arm below is defensive only. -/// -/// `pub` so the prover's `collect_hint_ops` can reproduce the exact output value -/// the executor wrote to guest memory (the value is not carried in the CPU log). +/// change the guest's result. The `_` arm for an unknown `hint_id` is defensive +/// only: the arena is untrusted input, and the guest verifies regardless. pub fn compute_hint(hint_id: u64, in_be: &[u8; 32]) -> [u8; 32] { use k256::elliptic_curve::PrimeField; let mut fb = k256::FieldBytes::default(); @@ -189,7 +168,7 @@ pub fn compute_hint(hint_id: u64, in_be: &[u8; 32]) -> [u8; 32] { /// `(addr mod 2^32) + max_offset < 2^32`. Tables that send an address to the memory /// bus as a `[lo32, hi32]` pair with the per-access offset added to `lo32` alone /// cannot represent a carry into `hi32`, so an operand straddling the limb boundary -/// makes the trace unprovable. Used by the ECSM and Hint ecalls. +/// makes the trace unprovable. Used by the ECSM ecall. fn addr_limb_ok(addr: u64, max_offset: u64) -> bool { (addr % LOW_LIMB) + max_offset < LOW_LIMB } @@ -550,42 +529,6 @@ impl Instruction { src2_val = addr_xg; dst_val = addr_k; } - SyscallNumbers::Hint => { - // Non-constraining hint: host computes a modular inverse/sqrt - // and writes it to the guest, which verifies it (and falls back - // to software on failure). a0 = hint_id, a1 = input addr - // (32-byte BE), a2 = output addr. The `_le` helpers only move - // bytes in address order, which is what a raw big-endian buffer - // needs. - let hint_id = registers.read(10)?; - let in_addr = registers.read(11)?; - let out_addr = registers.read(12)?; - // Reject an unrecognized selector up front: an unknown `hint_id` - // would otherwise silently produce a zero output (see - // `compute_hint`), indistinguishable from a legitimate numeric - // failure. Fail loudly instead so a guest bug surfaces here. - if !is_valid_hint_selector(hint_id) { - return Err(ExecutionError::HintUnknownSelector(hint_id)); - } - // Both operands are bounded so their 32-byte ranges cannot cross the - // 2^32 limb boundary, and the HINT table range-checks both low limbs - // against the same bound (`HINT_ADDR_LIMB_BOUND`) so the AIR accepts - // exactly what this rejects. The memory bus does not do that job on - // its own: it bounds `out_addr` only to 2^32 - 25, because the write - // bases are `out_addr_lo + 8i` and MEMW's carry columns resolve the - // bytes past the largest base. `in_addr` is not on the bus at all - // (the input read is not modeled). Bounding both also keeps - // `load_u256_le`/`store_u256_le` from overflowing their address - // arithmetic. - if !addr_limb_ok(in_addr, 31) || !addr_limb_ok(out_addr, 31) { - return Err(ExecutionError::HintAddressOverflow); - } - let input = load_u256_le(memory, in_addr)?; - let output = compute_hint(hint_id, &input); - store_u256_le(memory, out_addr, &output)?; - src2_val = in_addr; - dst_val = out_addr; - } SyscallNumbers::Halt => { // halt return Ok(Log { @@ -766,10 +709,6 @@ pub enum ExecutionError { EcsmAddressOverflow, #[error("ECSM xG and k operand ranges overlap")] EcsmOperandOverlap, - #[error("Hint address range overflows the lower 32-bit limb")] - HintAddressOverflow, - #[error("Unknown hint selector: {0}")] - HintUnknownSelector(u64), #[error("ECSM scalar multiplication error: {0}")] Ecsm(#[from] ecsm::EcsmError), } diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs index e1a269a01..705d20922 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -47,7 +47,8 @@ pub const MAX_PUBLIC_OUTPUT_TOTAL_SIZE: u64 = 1024 * 1024; pub const MAX_PRIVATE_INPUT_SIZE: u64 = 512 * 1024 * 1024; /// Fixed high address where private input is mapped. Guest programs can read /// directly from this address (ZisK-style memory-mapped input). -/// Layout: 4-byte LE length prefix at `PRIVATE_INPUT_START_INDEX`, then data at +4. +/// Layout: `[u32 LE main_len][main data][zero-pad to 8][u32 LE hint_count][u32 zero pad]` +/// then `hint_count` 32-byte hint slots (see [`encode_private_input_region`]). /// Must match `PRIVATE_INPUT_START` in `syscalls/src/syscalls.rs`. pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; /// Size in bytes of the private input's wire-format length prefix (the `u32` LE @@ -55,6 +56,72 @@ pub const PRIVATE_INPUT_START_INDEX: u64 = 0xFF000000; /// data follows at `+ PRIVATE_INPUT_LENGTH_PREFIX_BYTES`). Single source of truth /// for every page-span computation over the private-input region. pub const PRIVATE_INPUT_LENGTH_PREFIX_BYTES: usize = size_of::(); +/// Size in bytes of one hint slot in the hint arena. +pub const HINT_SLOT_BYTES: u64 = 32; +/// Size in bytes of the hint-arena header (`[u32 LE hint_count][u32 zero pad]`), +/// always present once the region is written at all. +pub const HINT_ARENA_HEADER_BYTES: u64 = 8; + +/// Start of the hint request log: just past the reserved private-input window, +/// rounded up to 8 (`0xFF000000 + 4 + 512 MiB + 4` = `0x11F000008`). The guest +/// appends `(hint_id, input)` entries here when the hint arena is exhausted — +/// the recording pass of the two-pass hint flow. The host reads them back with +/// [`Memory::hint_requests`]. Must match `HINT_LOG_START` in +/// `syscalls/src/syscalls.rs`. +pub const HINT_LOG_START_INDEX: u64 = 0xFF000000 + + PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64 + + MAX_PRIVATE_INPUT_SIZE + + 4; +/// Size in bytes of the request-log header (`[u32 LE count][u32 zero pad]`). +pub const HINT_LOG_HEADER_BYTES: u64 = 8; +/// Size in bytes of one request-log entry (`[u64 LE hint_id][32-byte input]`). +pub const HINT_LOG_ENTRY_BYTES: u64 = 40; + +/// Byte offset from `PRIVATE_INPUT_START_INDEX` at which the hint-arena header +/// (count word) sits, for a given main-input length: the 4-byte length prefix +/// plus the main data, padded with zeros up to the next 8-byte boundary. +pub const fn hint_arena_header_offset(main_len: u64) -> u64 { + (PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64 + main_len + 7) & !7 +} + +/// Canonical encoder for the private-input region: +/// `[len][data][pad8][count][pad][slots]`. Single source of truth for the wire +/// format; the prover's trace builder must call this instead of re-encoding. +/// +/// The count word and its pad are ALWAYS written (even when `hints` is empty), +/// so the layout is uniform and old guests reading past their data see a zero +/// count. The whole region must fit the reserved window: +/// `align8(4 + main_len) + 8 + 32 * hint_count <= 4 + MAX_PRIVATE_INPUT_SIZE`, +/// which keeps the verifier's `max_private_input_pages()` bound valid. +pub fn encode_private_input_region( + inputs: &[u8], + hints: &[[u8; 32]], +) -> Result, MemoryError> { + let main_len = u32::try_from(inputs.len()).map_err(|_| MemoryError::PrivateInputSizeExceeded)?; + let header_offset = hint_arena_header_offset(inputs.len() as u64); + let hints_bytes = (hints.len() as u64) + .checked_mul(HINT_SLOT_BYTES) + .ok_or(MemoryError::PrivateInputSizeExceeded)?; + let total = header_offset + .checked_add(HINT_ARENA_HEADER_BYTES) + .and_then(|t| t.checked_add(hints_bytes)) + .ok_or(MemoryError::PrivateInputSizeExceeded)?; + if total > PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64 + MAX_PRIVATE_INPUT_SIZE { + return Err(MemoryError::PrivateInputSizeExceeded); + } + + let mut region = Vec::with_capacity(total as usize); + region.extend_from_slice(&main_len.to_le_bytes()); + region.extend_from_slice(inputs); + region.resize(header_offset as usize, 0); + region.extend_from_slice(&(hints.len() as u32).to_le_bytes()); + region.extend_from_slice(&[0u8; 4]); + for hint in hints { + region.extend_from_slice(hint); + } + debug_assert_eq!(region.len() as u64, total); + Ok(region) +} #[derive(Default, Debug, Clone)] pub struct Memory { @@ -222,23 +289,45 @@ impl Memory { Ok(self.public_output.clone()) } - /// Pre-loads private input bytes at `PRIVATE_INPUT_START_INDEX` as a - /// 4-byte LE length prefix followed by the raw data. The guest reads these - /// bytes directly via normal RISC-V loads (ZisK-style memory-mapped input). - pub fn store_private_inputs(&mut self, inputs: Vec) -> Result<(), MemoryError> { - if inputs.is_empty() { - return Ok(()); + /// Read the hint request log the guest appended via `request_hint` (the + /// recording pass of the two-pass hint flow): `(hint_id, input)` entries in + /// request order. Empty when the arena covered every request. + pub fn hint_requests(&self) -> Result, MemoryError> { + let count = self.load_word(HINT_LOG_START_INDEX)? as usize; + let mut out = Vec::with_capacity(count); + for i in 0..count { + let entry = HINT_LOG_START_INDEX + + HINT_LOG_HEADER_BYTES + + i as u64 * HINT_LOG_ENTRY_BYTES; + let hint_id = self.load_doubleword(entry)?; + let bytes = self.load_bytes(entry + 8, 32)?; + let mut input = [0u8; 32]; + input.copy_from_slice(&bytes); + out.push((hint_id, input)); } - if inputs.len() as u64 > MAX_PRIVATE_INPUT_SIZE { - return Err(MemoryError::PrivateInputSizeExceeded); + Ok(out) + } + + /// Pre-loads private input bytes at `PRIVATE_INPUT_START_INDEX` in the + /// canonical wire format ([`encode_private_input_region`]): a 4-byte LE + /// length prefix, the main data zero-padded to an 8-byte boundary, then the + /// always-present hint-arena header (`[u32 LE hint_count][u32 zero pad]`) + /// followed by `hint_count` 32-byte hint slots. The guest reads these bytes + /// directly via normal RISC-V loads (ZisK-style memory-mapped input). + /// + /// With no main input AND no hints nothing is written at all (the region + /// reads back as all zeros, including a zero hint count) so a no-input + /// program keeps zero private-input pages. + pub fn store_private_inputs( + &mut self, + inputs: Vec, + hints: &[[u8; 32]], + ) -> Result<(), MemoryError> { + if inputs.is_empty() && hints.is_empty() { + return Ok(()); } - let len_u32 = - u32::try_from(inputs.len()).map_err(|_| MemoryError::PrivateInputSizeExceeded)?; - self.store_word(PRIVATE_INPUT_START_INDEX, len_u32)?; - self.set_bytes_aligned( - PRIVATE_INPUT_START_INDEX + PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64, - &inputs, - )?; + let region = encode_private_input_region(&inputs, hints)?; + self.set_bytes_aligned(PRIVATE_INPUT_START_INDEX, ®ion)?; Ok(()) } @@ -313,7 +402,7 @@ mod tests { fn store_private_inputs_writes_le_length_prefix_then_data() { let mut memory = Memory::default(); let inputs = vec![0xAAu8, 0xBB, 0xCC]; - memory.store_private_inputs(inputs.clone()).unwrap(); + memory.store_private_inputs(inputs.clone(), &[]).unwrap(); assert_eq!( memory.load_word(PRIVATE_INPUT_START_INDEX).unwrap(), @@ -327,4 +416,158 @@ mod tests { .unwrap(); assert_eq!(data, inputs); } + + // Roundtrip with hints: the count word sits at `hint_arena_header_offset(main_len)` + // (8-aligned), the slots land right after the 8-byte header, and the region's + // extent is `header + 8 + 32 * hint_count`. + #[test] + fn store_private_inputs_roundtrip_with_hints() { + let mut memory = Memory::default(); + let inputs = vec![0x11u8; 5]; // odd length exercises the pad-to-8 + let hints = [[0x22u8; 32], [0x33u8; 32]]; + memory.store_private_inputs(inputs.clone(), &hints).unwrap(); + + // Legacy prefix: [len][data]. + assert_eq!( + memory.load_word(PRIVATE_INPUT_START_INDEX).unwrap(), + inputs.len() as u32 + ); + let data = memory + .load_bytes( + PRIVATE_INPUT_START_INDEX + PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64, + inputs.len() as u64, + ) + .unwrap(); + assert_eq!(data, inputs); + + // Hint-arena header: align8(4 + 5) = 16, count word at base + 16, pad zero. + let header = hint_arena_header_offset(inputs.len() as u64); + assert_eq!(header, 16); + assert!(header.is_multiple_of(8)); + assert_eq!( + memory + .load_word(PRIVATE_INPUT_START_INDEX + header) + .unwrap(), + hints.len() as u32 + ); + assert_eq!( + memory + .load_word(PRIVATE_INPUT_START_INDEX + header + 4) + .unwrap(), + 0 + ); + + // Slots land at header + 8, 32 bytes each, in order. + for (i, hint) in hints.iter().enumerate() { + let slot = memory + .load_bytes( + PRIVATE_INPUT_START_INDEX + header + HINT_ARENA_HEADER_BYTES + + i as u64 * HINT_SLOT_BYTES, + HINT_SLOT_BYTES, + ) + .unwrap(); + assert_eq!(slot, hint); + } + + // The encoder agrees with what was stored, byte for byte. + let encoded = encode_private_input_region(&inputs, &hints).unwrap(); + let stored = memory + .load_bytes(PRIVATE_INPUT_START_INDEX, encoded.len() as u64) + .unwrap(); + assert_eq!(stored, encoded); + } + + // Empty main input with hints: the `[0]` length prefix and the arena header are + // still written (no early return), and the slots follow the header. + #[test] + fn store_private_inputs_empty_main_with_hints() { + let mut memory = Memory::default(); + let hints = [[0xABu8; 32]]; + memory.store_private_inputs(vec![], &hints).unwrap(); + + assert_eq!(memory.load_word(PRIVATE_INPUT_START_INDEX).unwrap(), 0); + // align8(4 + 0) = 8: the count word is readable at base + 8. + let header = hint_arena_header_offset(0); + assert_eq!(header, 8); + assert_eq!( + memory + .load_word(PRIVATE_INPUT_START_INDEX + header) + .unwrap(), + 1 + ); + let slot = memory + .load_bytes( + PRIVATE_INPUT_START_INDEX + header + HINT_ARENA_HEADER_BYTES, + HINT_SLOT_BYTES, + ) + .unwrap(); + assert_eq!(slot, hints[0]); + } + + // Zero hints produce the legacy prefix plus the always-written 8-byte header + // (count = 0, pad = 0) — and nothing past it. + #[test] + fn store_private_inputs_zero_hints_writes_empty_header() { + let mut memory = Memory::default(); + let inputs = vec![0x42u8; 16]; + memory.store_private_inputs(inputs.clone(), &[]).unwrap(); + + let header = hint_arena_header_offset(inputs.len() as u64); + assert_eq!(header, 24); // align8(4 + 16) + assert_eq!( + memory + .load_word(PRIVATE_INPUT_START_INDEX + header) + .unwrap(), + 0 + ); + assert_eq!( + memory + .load_word(PRIVATE_INPUT_START_INDEX + header + 4) + .unwrap(), + 0 + ); + let encoded = encode_private_input_region(&inputs, &[]).unwrap(); + assert_eq!(encoded.len() as u64, header + HINT_ARENA_HEADER_BYTES); + } + + // No main input and no hints: nothing is written (a zero input program keeps + // zero private-input pages); the count word reads back as 0 regardless. + #[test] + fn store_private_inputs_empty_everything_writes_nothing() { + let mut memory = Memory::default(); + memory.store_private_inputs(vec![], &[]).unwrap(); + assert_eq!(memory.load_word(PRIVATE_INPUT_START_INDEX).unwrap(), 0); + assert_eq!( + memory + .load_word(PRIVATE_INPUT_START_INDEX + hint_arena_header_offset(0)) + .unwrap(), + 0 + ); + } + + // Size cap: `align8(4 + main_len) + 8 + 32 * hint_count` must stay within + // `4 + MAX_PRIVATE_INPUT_SIZE`. A main input exactly at the old cap leaves no + // room for the arena header, and even one hint on top must fail. + #[test] + fn encode_private_input_region_enforces_size_cap() { + // Rejection cases first: the cap is checked before the region is + // allocated, so these never touch the 512 MiB output buffer. + // + // The old single-section cap (main_len > MAX) still fails. + let err = encode_private_input_region(&vec![0u8; MAX_PRIVATE_INPUT_SIZE as usize + 1], &[]) + .unwrap_err(); + assert!(matches!(err, MemoryError::PrivateInputSizeExceeded)); + + // Largest main_len whose region (with the header, zero hints) still fits: + // align8(4 + main_len) + 8 <= 4 + MAX. With MAX a multiple of 8, that is + // 4 + main_len <= MAX - 8, i.e. main_len = MAX - 12. + let ok_len = MAX_PRIVATE_INPUT_SIZE as usize - 12; + let inputs = vec![0u8; ok_len]; + // The same main_len plus a single hint exceeds the reserved window. + let err = encode_private_input_region(&inputs, &[[0u8; 32]]).unwrap_err(); + assert!(matches!(err, MemoryError::PrivateInputSizeExceeded)); + + let encoded = encode_private_input_region(&inputs, &[]).unwrap(); + assert_eq!(encoded.len() as u64, MAX_PRIVATE_INPUT_SIZE); + } } diff --git a/executor/tests/asm.rs b/executor/tests/asm.rs index a1c9baf2b..fe765b35b 100644 --- a/executor/tests/asm.rs +++ b/executor/tests/asm.rs @@ -12,7 +12,7 @@ fn run_program(elf_path: &str) { println!("Testing {}", elf_path); let elf_data = std::fs::read(elf_path).unwrap(); let program = Elf::load(&elf_data).unwrap(); - let mut executor = Executor::new(&program, vec![]).expect("Failed to create executor"); + let mut executor = Executor::new(&program, vec![], &[]).expect("Failed to create executor"); while let Some(_logs) = executor.resume().expect("Failed to execute") {} @@ -31,7 +31,7 @@ fn test_private_input_memory_mapped() { let elf_data = std::fs::read("./program_artifacts/asm/test_private_input_xpage.elf").unwrap(); let program = Elf::load(&elf_data).unwrap(); let input: Vec = (0u8..16).collect(); - let executor = Executor::new(&program, input.clone()).unwrap(); + let executor = Executor::new(&program, input.clone(), &[]).unwrap(); let result = executor.run().unwrap(); // Committed bytes are at 0xFF000008 = data bytes [4..12] assert_eq!(result.return_values.memory_values, input[4..12].to_vec()); @@ -476,7 +476,7 @@ fn test_misalign_sd() { fn test_misaligned_pc_traps() { let elf_data = std::fs::read("./program_artifacts/asm/misaligned_pc.elf").unwrap(); let program = Elf::load(&elf_data).unwrap(); - let mut executor = Executor::new(&program, vec![]).expect("Failed to create executor"); + let mut executor = Executor::new(&program, vec![], &[]).expect("Failed to create executor"); let err = loop { match executor.resume() { Ok(Some(_)) => continue, @@ -886,7 +886,7 @@ fn test_keccak() { // Expected output is the FIPS-202 zero-input KAT. let elf_data = std::fs::read("./program_artifacts/asm/test_keccak.elf").unwrap(); let program = Elf::load(&elf_data).unwrap(); - let executor = Executor::new(&program, vec![]).expect("Failed to create executor"); + let executor = Executor::new(&program, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let expected_state: [u64; 25] = [ @@ -930,7 +930,7 @@ fn test_run_epochs_splits_execution_into_n_cycle_epochs() { let program = Elf::load(&elf_data).unwrap(); // Reference: full single-pass run. - let full = Executor::new(&program, vec![]).unwrap().run().unwrap(); + let full = Executor::new(&program, vec![], &[]).unwrap().run().unwrap(); // Pick an epoch size that splits this program into a few epochs, whatever // its exact length. @@ -938,7 +938,7 @@ fn test_run_epochs_splits_execution_into_n_cycle_epochs() { assert!(total_cycles >= 2); let epoch_size = (total_cycles / 3).max(1); - let epochs = Executor::new(&program, vec![]) + let epochs = Executor::new(&program, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); diff --git a/executor/tests/hint_arena_ecrecover.rs b/executor/tests/hint_arena_ecrecover.rs new file mode 100644 index 000000000..1a7980541 --- /dev/null +++ b/executor/tests/hint_arena_ecrecover.rs @@ -0,0 +1,121 @@ +//! Two-pass hint-arena measurement driver for the `ecrecover_hints` guest. +//! +//! Pass 1 runs with an empty arena: every hint request misses, is appended to +//! the guest's request log, and the guest recomputes in software (correct, +//! just slow). The host then answers the logged requests with `compute_hint`, +//! and pass 2 re-runs with a complete arena — the provable configuration. +//! +//! Run explicitly (prints cycle counts; the guest ELF must be built): +//! cargo test -p executor --test hint_arena_ecrecover -- --ignored --nocapture + +use executor::elf::Elf; +use executor::vm::instruction::execution::compute_hint; +use executor::vm::execution::Executor; +use std::time::Instant; + +/// Number of ecrecovers the guest performs (3 hint requests each). +const N: usize = 30; + +/// Build the guest's private input: `[u32 LE count]` then `count` records of +/// `sig(64) || recid(1) || msg(32)`. Any `(r, s, msg)` with `r, s` nonzero and +/// in range is a legal ecrecover input, but `r³ + 7` must be a quadratic +/// residue for decompression to succeed — so `r` values are rejection-sampled +/// for residuosity. `recid` alternates parity. +fn build_input() -> Vec { + use k256::{FieldElement, Scalar}; + + let mut out = Vec::with_capacity(4 + N * 97); + out.extend_from_slice(&(N as u32).to_le_bytes()); + + let mut r_int = 0u64; + for i in 0..N { + // Next r whose rhs = r³ + 7 is a residue. + let (r_bytes, y_is_odd) = loop { + r_int += 1; + let r = FieldElement::from(r_int); + let rhs = r * r * r + FieldElement::from(7u64); + if let Some(y) = Option::::from(rhs.sqrt()) { + // ecrecover parses r as a *scalar* (r < n); small ints qualify. + // `sqrt` yields an unnormalized element; `is_odd` requires a + // normalized one. + break (r.to_bytes(), bool::from(y.normalize().is_odd())); + } + }; + let s = Scalar::from(1000u64 + i as u64); + let msg_byte = (i as u8).wrapping_mul(7).wrapping_add(1); + + out.extend_from_slice(&r_bytes); + out.extend_from_slice(&s.to_bytes()); + out.push(u8::from(y_is_odd)); + out.extend_from_slice(&[msg_byte; 32]); + } + out +} + +#[test] +#[ignore = "measurement driver — run explicitly"] +fn ecrecover_two_pass_cycles() { + let elf_bytes = std::fs::read("program_artifacts/rust/ecrecover_hints.elf") + .expect("ecrecover_hints.elf missing — run `make executor/program_artifacts/rust/ecrecover_hints.elf`"); + let elf = Elf::load(&elf_bytes).expect("ELF load"); + let input = build_input(); + + // ── Pass 1: empty arena — all hints miss, logged, software fallback. ── + let t0 = Instant::now(); + let pass1 = Executor::new(&elf, input.clone(), &[]) + .expect("executor") + .run() + .expect("pass 1"); + let pass1_time = t0.elapsed(); + let pass1_cycles = pass1.logs.len(); + assert_eq!( + pass1.hint_requests.len(), + 3 * N, + "every recovery must log sqrt + scalar_inv + field_inv" + ); + + // Host answers the requests, in order — that IS the arena. + let hints: Vec<[u8; 32]> = pass1 + .hint_requests + .iter() + .map(|(id, input)| compute_hint(*id, input)) + .collect(); + + // ── Pass 2: complete arena — every hint hits and verifies. ── + let t0 = Instant::now(); + let pass2 = Executor::new(&elf, input.clone(), &hints) + .expect("executor") + .run() + .expect("pass 2"); + let pass2_time = t0.elapsed(); + let pass2_cycles = pass2.logs.len(); + assert!(pass2.hint_requests.is_empty(), "arena must cover pass 2"); + + // Same program, same input: identical committed output. + assert_eq!( + pass1.return_values.memory_values, pass2.return_values.memory_values, + "hint source must not change the result" + ); + + println!("[ecrecover-hints] N = {N} recoveries"); + println!("[ecrecover-hints] pass 1 (software fallback): {pass1_cycles} cycles in {pass1_time:?}"); + println!("[ecrecover-hints] pass 2 (arena hints): {pass2_cycles} cycles in {pass2_time:?}"); + println!( + "[ecrecover-hints] guest cycle ratio pass1/pass2: {:.2}x", + pass1_cycles as f64 / pass2_cycles as f64 + ); + + // Dump the fixtures the prover-side continuation measurement consumes: + // `.input.bin` (the private input) and `.hints.bin` (the arena + // slots, concatenated). + let dir = std::path::Path::new("program_artifacts/rust"); + let input_path = dir.join("ecrecover_hints.input.bin"); + let hints_path = dir.join("ecrecover_hints.hints.bin"); + std::fs::write(&input_path, &input).expect("write input fixture"); + let mut hints_bin = Vec::with_capacity(32 * hints.len()); + for h in &hints { + hints_bin.extend_from_slice(h); + } + std::fs::write(&hints_path, &hints_bin).expect("write hints fixture"); + println!("[ecrecover-hints] fixtures written: {input_path:?}, {hints_path:?}"); +} diff --git a/executor/tests/rust.rs b/executor/tests/rust.rs index 1c13ad1a5..7f2cbe242 100644 --- a/executor/tests/rust.rs +++ b/executor/tests/rust.rs @@ -13,7 +13,7 @@ fn run_program_without_expect( let elf_data = std::fs::read(elf_path).unwrap(); let program = Elf::load(&elf_data).unwrap(); println!("Program entry: 0x{:016x}", program.entry_point); - let mut executor = Executor::new(&program, private_inputs)?; + let mut executor = Executor::new(&program, private_inputs, &[])?; while let Some(_logs) = executor.resume()? {} executor.finish() } diff --git a/prover/benches/bench_continuation.rs b/prover/benches/bench_continuation.rs index c8638346f..7be769c93 100644 --- a/prover/benches/bench_continuation.rs +++ b/prover/benches/bench_continuation.rs @@ -47,7 +47,7 @@ fn main() { use executor::elf::Elf; use executor::vm::execution::Executor; let program = Elf::load(&elf).expect("bad ELF"); - let result = Executor::new(&program, private_inputs) + let result = Executor::new(&program, private_inputs, &[]) .expect("executor") .run() .expect("execution failed"); @@ -62,7 +62,7 @@ fn main() { use executor::vm::execution::Executor; use executor::vm::registers::STACK_TOP; let program = Elf::load(&elf).expect("bad ELF"); - let mut ex = Executor::new(&program, private_inputs).expect("executor"); + let mut ex = Executor::new(&program, private_inputs, &[]).expect("executor"); while ex.pc() != 0 { match ex.resume_with_limit(usize::MAX).expect("execution failed") { Some(_) => {} @@ -107,7 +107,7 @@ fn main() { } } "main" => { - lambda_vm_prover::prove_with_inputs(&elf, &private_inputs) + lambda_vm_prover::prove_with_inputs(&elf, &private_inputs, &[]) .expect("monolithic prove failed"); println!("main prove ok ({} bytes ELF)", elf.len()); } diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 85f2d6223..da6194ee0 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -303,7 +303,7 @@ fn global_memory_configs( ) -> Vec { // No private bytes: the verifier only builds the AIRs, and private-input pages are // non-preprocessed (their INIT is never recomputed). - let image = build_initial_image_paged(elf, &[]); + let image = build_initial_image_paged(elf, &[], &[]); let init_page_data = build_init_page_data(&image); global_memory_configs_from_init_page_data( page_bases, @@ -1050,6 +1050,7 @@ fn verify_global( pub fn prove_continuation( elf_bytes: &[u8], private_inputs: &[u8], + hints: &[[u8; 32]], epoch_size_log2: u32, opts: &ProofOptions, ) -> Result { @@ -1073,7 +1074,7 @@ pub fn prove_continuation( let __root = stark::instruments::span("prove_continuation_total"); let elf = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - let mut executor = Executor::new(&elf, private_inputs.to_vec()) + let mut executor = Executor::new(&elf, private_inputs.to_vec(), hints) .map_err(|e| Error::Execution(format!("{e}")))?; // The DECODE precomputed commitment depends only on (ELF, opts): compute // it once here instead of once per epoch inside `build_epoch_airs`. @@ -1086,7 +1087,7 @@ pub fn prove_continuation( // The cross-epoch memory image, carried forward: epoch i+1's init is epoch i's // fini, updated in place with each epoch's touched-cell final values. - let mut image = build_initial_image_paged(&elf, private_inputs); + let mut image = build_initial_image_paged(&elf, private_inputs, hints); let init_page_data = build_init_page_data(&image); let mut provenance = local_to_global::genesis_provenance(image.iter().map(|(a, v)| (a, v as u64))); @@ -1228,6 +1229,7 @@ pub fn prove_continuation( &job.register_init, &MaxRowsConfig::default(), private_inputs, + hints, job.is_final, true, #[cfg(feature = "disk-spill")] @@ -1429,7 +1431,7 @@ pub fn prove_continuation( let run = || -> Result { #[cfg(feature = "instruments")] let __sp = stark::instruments::span("prove_global"); - let num_private_input_pages = page::private_input_page_count(private_inputs); + let num_private_input_pages = page::private_input_page_count(private_inputs, hints); // SINGLE source of truth: the same page-base list drives the // committed GLOBAL_MEMORY tables and is shipped in the bundle, // so the two can never diverge in set or order. @@ -1755,7 +1757,7 @@ pub fn prove_and_verify_continuation( epoch_size_log2: u32, opts: &ProofOptions, ) -> Result>, Error> { - let bundle = prove_continuation(elf_bytes, private_inputs, epoch_size_log2, opts)?; + let bundle = prove_continuation(elf_bytes, private_inputs, &[], epoch_size_log2, opts)?; verify_continuation(elf_bytes, &bundle, opts) } @@ -1784,7 +1786,7 @@ mod tests { let elf_bytes = asm_elf_bytes("test_commit_split"); let expected_output: [u8; 4] = [0xAA, 0xBB, 0xCC, 0xDD]; - let total = Executor::new(&Elf::load(&elf_bytes).unwrap(), vec![]) + let total = Executor::new(&Elf::load(&elf_bytes).unwrap(), vec![], &[]) .unwrap() .run() .unwrap() @@ -1807,7 +1809,7 @@ mod tests { // Prove first so we can assert the run actually split into >1 epoch — without // this the test would silently pass even if it degraded to a single epoch. let bundle = - prove_continuation(&elf_bytes, &[], 4, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 4, &ProofOptions::default_test_options()).unwrap(); assert!( bundle.num_epochs() > 1, "16-cycle epochs must split the run into multiple epochs" @@ -1836,6 +1838,7 @@ mod tests { let r = prove_continuation( &elf_bytes, test_fault::MAGIC, + &[], 2, &ProofOptions::default_test_options(), ); @@ -1863,7 +1866,7 @@ mod tests { // Guard against silent degradation: the program must be longer than one // epoch, otherwise this collapses to a single final epoch and stops testing // the cross-epoch (intermediate-epoch) path. - let total = Executor::new(&Elf::load(&elf_bytes).unwrap(), vec![]) + let total = Executor::new(&Elf::load(&elf_bytes).unwrap(), vec![], &[]) .unwrap() .run() .unwrap() @@ -1892,7 +1895,7 @@ mod tests { fn test_verify_continuation_with_supplied_roots() { let elf_bytes = asm_elf_bytes("data_page_touch"); let opts = ProofOptions::default_test_options(); - let bundle = prove_continuation(&elf_bytes, &[], 3, &opts).unwrap(); + let bundle = prove_continuation(&elf_bytes, &[], &[], 3, &opts).unwrap(); let expected = verify_continuation(&elf_bytes, &bundle, &opts) .unwrap() @@ -1973,7 +1976,7 @@ mod tests { for name in ["data_page_touch", "all_loadstore_32"] { let elf_bytes = asm_elf_bytes(name); let opts = ProofOptions::default_test_options(); - let bundle = prove_continuation(&elf_bytes, &[], 3, &opts).unwrap(); + let bundle = prove_continuation(&elf_bytes, &[], &[], 3, &opts).unwrap(); let elf = Elf::load(&elf_bytes).unwrap(); let page_bases = canonical_page_bases(&bundle.touched_page_bases); @@ -2011,7 +2014,7 @@ mod tests { fn test_ecsm_across_epochs_verifies() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_ecsm_split"); - let total = Executor::new(&Elf::load(&elf_bytes).unwrap(), vec![]) + let total = Executor::new(&Elf::load(&elf_bytes).unwrap(), vec![], &[]) .unwrap() .run() .unwrap() @@ -2051,7 +2054,7 @@ mod tests { #[test] fn test_continuation_rejects_too_small_epoch_size_log2() { assert!(matches!( - prove_continuation(&[], &[], 1, &ProofOptions::default_test_options()), + prove_continuation(&[], &[], &[], 1, &ProofOptions::default_test_options()), Err(Error::InvalidContinuationEpochSize(_)) )); } @@ -2065,7 +2068,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_commit_split"); let bundle = - prove_continuation(&elf_bytes, &[], 4, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 4, &ProofOptions::default_test_options()).unwrap(); let out = verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()) .unwrap(); assert_eq!(out.as_deref(), Some(&[0xAA, 0xBB, 0xCC, 0xDD][..])); @@ -2078,7 +2081,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_commit_split"); let bundle = - prove_continuation(&elf_bytes, &[], 4, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 4, &ProofOptions::default_test_options()).unwrap(); let bytes = rkyv::to_bytes::(&bundle).unwrap(); let restored: ContinuationProof = @@ -2097,7 +2100,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!(bundle.epochs.len() >= 3, "need multiple epochs"); bundle.epochs.pop(); assert!( @@ -2115,7 +2118,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!(bundle.epochs.len() >= 3, "need multiple epochs"); bundle.epochs.swap(0, 1); assert!( @@ -2134,7 +2137,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!( bundle.epochs.len() >= 2, "need a second epoch to chain into" @@ -2157,7 +2160,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!(!bundle.epochs.is_empty()); bundle.epochs[0].reg_fini.pop(); assert!( @@ -2174,7 +2177,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 8, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 8, &ProofOptions::default_test_options()).unwrap(); bundle.epochs[0].table_counts.cpu += 1; assert!( verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()) @@ -2198,7 +2201,7 @@ mod tests { // Smallest epochs (2^2 = 4 cycles) so the short program splits across epochs. let bundle = - prove_continuation(&elf_bytes, &input, 2, &ProofOptions::default_test_options()) + prove_continuation(&elf_bytes, &input, &[], 2, &ProofOptions::default_test_options()) .unwrap(); assert!( bundle.num_epochs() > 1, @@ -2237,7 +2240,7 @@ mod tests { let elf_bytes = asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); let mut bundle = - prove_continuation(&elf_bytes, &input, 2, &ProofOptions::default_test_options()) + prove_continuation(&elf_bytes, &input, &[], 2, &ProofOptions::default_test_options()) .unwrap(); assert!( bundle.num_private_input_pages > 0, @@ -2271,7 +2274,7 @@ mod tests { let elf_bytes = asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); let mut bundle = - prove_continuation(&elf_bytes, &input, 2, &ProofOptions::default_test_options()) + prove_continuation(&elf_bytes, &input, &[], 2, &ProofOptions::default_test_options()) .unwrap(); assert_eq!( bundle.num_private_input_pages, 1, @@ -2321,19 +2324,37 @@ mod tests { // Pages below the region are never private. assert!(!page::is_private_input_page(start - page_size, 10)); - // private_input_page_count: wire format is [len:4][data], region is page-aligned. - assert_eq!(page::private_input_page_count(&[]), 0); - assert_eq!(page::private_input_page_count(&[0u8; 16]), 1); - // 4-byte prefix + (page_size - 4) data exactly fills one page. + // private_input_page_count: wire format is `[len:4][data][pad8][count:4][pad:4]` + // plus 32-byte hint slots; the region is page-aligned. The always-written + // 8-byte arena header shifts the old boundaries by +8 bytes. + assert_eq!(page::private_input_page_count(&[], &[]), 0); + assert_eq!(page::private_input_page_count(&[0u8; 16], &[]), 1); + // 4-byte prefix + (page_size - 12) data pads to page_size - 8, plus the + // 8-byte header exactly fills one page. assert_eq!( - page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 4]), + page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 12], &[]), 1 ); - // One more byte spills into a second page. + // One more byte pads up to page_size and the header spills into a second page. + assert_eq!( + page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 11], &[]), + 2 + ); + // The old single-page boundary (page_size - 4) now needs two pages: the + // data section alone fills the page and the +8 header spills over. + assert_eq!( + page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 4], &[]), + 2 + ); assert_eq!( - page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 3]), + page::private_input_page_count(&vec![0u8; page::DEFAULT_PAGE_SIZE - 3], &[]), 2 ); + // Hints extend the region: empty main + one hint = align8(4) + 8 + 32 = 48 bytes. + assert_eq!(page::private_input_page_count(&[], &[[0u8; 32]]), 1); + // The arena alone can push the span past a page boundary. + let hints = vec![[0u8; 32]; page::DEFAULT_PAGE_SIZE / 32]; + assert_eq!(page::private_input_page_count(&[], &hints), 2); } // `private_input_page_bases` must enumerate exactly the aligned bases that @@ -2416,7 +2437,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); bundle.num_private_input_pages = page::max_private_input_pages() + 1; assert!(matches!( verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()), @@ -2446,7 +2467,7 @@ mod tests { input[commit_off..commit_off + 8].copy_from_slice(&expected); let bundle = - prove_continuation(&elf_bytes, &input, 4, &ProofOptions::default_test_options()) + prove_continuation(&elf_bytes, &input, &[], 4, &ProofOptions::default_test_options()) .unwrap(); assert!( bundle.num_private_input_pages >= 2, @@ -2478,7 +2499,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!( !bundle.touched_page_bases.is_empty(), "baseline must have touched pages" @@ -2510,7 +2531,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!( !bundle.touched_page_bases.is_empty(), "baseline must have touched pages" @@ -2540,7 +2561,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!( !bundle.touched_page_bases.is_empty(), "baseline must have touched pages" @@ -2571,7 +2592,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &ProofOptions::default_test_options()).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); assert!( bundle.epochs.len() >= 2, "need multiple epochs to exercise the binding" @@ -2593,7 +2614,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); let mut bundle = - prove_continuation(&elf_bytes, &[], 3, &crate::recursion::MIN_PROOF_OPTIONS).unwrap(); + prove_continuation(&elf_bytes, &[], &[], 3, &crate::recursion::MIN_PROOF_OPTIONS).unwrap(); assert!( bundle.epochs.len() >= 2, "need multiple epochs to exercise the binding" @@ -2636,8 +2657,8 @@ mod tests { let input_a: Vec = (0u8..16).collect(); let input_b: Vec = (0u8..16).map(|b| b ^ 0xFF).collect(); - let mut bundle_a = prove_continuation(&elf_bytes, &input_a, 2, &opts).unwrap(); - let bundle_b = prove_continuation(&elf_bytes, &input_b, 2, &opts).unwrap(); + let mut bundle_a = prove_continuation(&elf_bytes, &input_a, &[], 2, &opts).unwrap(); + let bundle_b = prove_continuation(&elf_bytes, &input_b, &[], 2, &opts).unwrap(); assert!( verify_continuation(&elf_bytes, &bundle_a, &opts) .unwrap() @@ -2686,8 +2707,8 @@ mod tests { let input_a: Vec = (0u8..16).collect(); let input_b: Vec = (0u8..16).map(|b| b ^ 0xFF).collect(); - let mut bundle_a = prove_continuation(&elf_bytes, &input_a, 2, &opts).unwrap(); - let bundle_b = prove_continuation(&elf_bytes, &input_b, 2, &opts).unwrap(); + let mut bundle_a = prove_continuation(&elf_bytes, &input_a, &[], 2, &opts).unwrap(); + let bundle_b = prove_continuation(&elf_bytes, &input_b, &[], 2, &opts).unwrap(); assert_eq!(bundle_a.epochs.len(), bundle_b.epochs.len()); assert_eq!(bundle_a.touched_page_bases, bundle_b.touched_page_bases); assert_ne!(bundle_a.epochs[0].l2g_root, bundle_b.epochs[0].l2g_root); diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 79ef4c715..d9261b51e 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -53,7 +53,7 @@ use crate::tables::types::BusId; use crate::test_utils::{ E, F, VmAir, create_bitwise_air, create_branch_air, create_bytewise_air, create_commit_air, create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, create_ecdas_air, - create_ecsm_air, create_eq_air, create_halt_air, create_hint_air, create_keccak_air, + create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, create_keccak_rc_air, create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, create_memw_aligned_air, create_memw_register_air, create_mul_air, create_page_air, create_register_air, create_shift_air, create_store_air, @@ -82,8 +82,8 @@ pub struct RuntimePageRange { /// Number of tables that always contribute exactly one sub-proof, regardless /// of `TableCounts`: bitwise, decode, halt, commit, keccak, keccak_rnd, -/// keccak_rc, register, ecsm, ecdas, hint. -pub const FIXED_TABLE_COUNT: usize = 11; +/// keccak_rc, register, ecsm, ecdas. +pub const FIXED_TABLE_COUNT: usize = 10; /// Number of chunks for each split table. /// The verifier needs this to reconstruct matching AIRs. @@ -522,7 +522,6 @@ pub(crate) struct VmAirs { pub keccak_rc: VmAir, pub ecsm: VmAir, pub ecdas: VmAir, - pub hint: VmAir, pub register: VmAir, pub pages: Vec, pub memw_registers: Vec, @@ -548,7 +547,6 @@ impl VmAirs { (self.keccak_rc.as_ref(), &mut traces.keccak_rc, &()), (self.ecsm.as_ref(), &mut traces.ecsm, &()), (self.ecdas.as_ref(), &mut traces.ecdas, &()), - (self.hint.as_ref(), &mut traces.hint, &()), (self.register.as_ref(), &mut traces.register, &()), ]; if self.include_halt { @@ -623,7 +621,6 @@ impl VmAirs { self.keccak_rc.as_ref(), self.ecsm.as_ref(), self.ecdas.as_ref(), - self.hint.as_ref(), self.register.as_ref(), ]; if self.include_halt { @@ -795,7 +792,6 @@ impl VmAirs { )); let ecsm: VmAir = Box::new(create_ecsm_air(proof_options)); let ecdas: VmAir = Box::new(create_ecdas_air(proof_options)); - let hint: VmAir = Box::new(create_hint_air(proof_options)); let register: VmAir = if let Some((commitment, num_preprocessed_cols)) = register_preprocessed { Box::new( @@ -916,7 +912,6 @@ impl VmAirs { keccak_rc, ecsm, ecdas, - hint, register, pages, memw_registers, @@ -1044,14 +1039,23 @@ pub(crate) fn verify_l2g_commitment_binding_view( /// Prove an ELF binary execution. Returns a serializable proof bundle. pub fn prove(elf_bytes: &[u8]) -> Result { - prove_with_inputs(elf_bytes, &[]) + prove_with_inputs(elf_bytes, &[], &[]) } /// Prove an ELF binary execution with private inputs. Returns a serializable proof bundle. -pub fn prove_with_inputs(elf_bytes: &[u8], private_inputs: &[u8]) -> Result { +/// +/// `hints` are untrusted 32-byte values appended to the private-input memory +/// region's hint arena; the guest reads them with ordinary loads and must +/// verify them in-circuit. +pub fn prove_with_inputs( + elf_bytes: &[u8], + private_inputs: &[u8], + hints: &[[u8; 32]], +) -> Result { prove_with_options_and_inputs( elf_bytes, private_inputs, + hints, &GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is always valid"), &MaxRowsConfig::default(), ) @@ -1065,9 +1069,13 @@ pub fn prove_with_inputs(elf_bytes: &[u8], private_inputs: &[u8]) -> Result Result<(u64, u64), Error> { +pub fn count_elements( + elf_bytes: &[u8], + private_inputs: &[u8], + hints: &[[u8; 32]], +) -> Result<(u64, u64), Error> { let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - let executor = Executor::new(&program, private_inputs.to_vec()) + let executor = Executor::new(&program, private_inputs.to_vec(), hints) .map_err(|e| Error::Execution(format!("{e}")))?; let result = executor .run() @@ -1077,6 +1085,7 @@ pub fn count_elements(elf_bytes: &[u8], private_inputs: &[u8]) -> Result<(u64, u &result.logs, &MaxRowsConfig::default(), private_inputs, + hints, #[cfg(feature = "disk-spill")] StorageMode::Ram, )?; @@ -1092,7 +1101,7 @@ pub fn prove_with_options( proof_options: &ProofOptions, max_rows: &MaxRowsConfig, ) -> Result { - prove_with_options_and_inputs(elf_bytes, &[], proof_options, max_rows) + prove_with_options_and_inputs(elf_bytes, &[], &[], proof_options, max_rows) } /// Prove an ELF binary execution with custom proof options, max rows config, @@ -1100,6 +1109,7 @@ pub fn prove_with_options( pub fn prove_with_options_and_inputs( elf_bytes: &[u8], private_inputs: &[u8], + hints: &[[u8; 32]], proof_options: &ProofOptions, max_rows: &MaxRowsConfig, ) -> Result { @@ -1119,7 +1129,7 @@ pub fn prove_with_options_and_inputs( let __sp = stark::instruments::span("execute"); let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - let executor = Executor::new(&program, private_inputs.to_vec()) + let executor = Executor::new(&program, private_inputs.to_vec(), hints) .map_err(|e| Error::Execution(format!("{e}")))?; let result = executor .run() @@ -1140,7 +1150,7 @@ pub fn prove_with_options_and_inputs( #[cfg(feature = "disk-spill")] let storage_mode = { - let lengths = count_table_lengths(&program, &result.logs, max_rows, private_inputs)?; + let lengths = count_table_lengths(&program, &result.logs, max_rows, private_inputs, hints)?; auto_storage::decide(&lengths, proof_options.blowup_factor) }; @@ -1149,6 +1159,7 @@ pub fn prove_with_options_and_inputs( &result.logs, max_rows, private_inputs, + hints, #[cfg(feature = "disk-spill")] storage_mode, )?; diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index fc4c2f976..781bb02b0 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -188,11 +188,6 @@ pub struct CpuOperation { /// Whether this ECALL is an ECSM (elliptic-curve scalar multiply) syscall pub ecall_ecsm: bool, - - /// Whether this ECALL is a non-constraining Hint syscall. The hint operand - /// addresses (x10/x11/x12) are recovered from the register state in the trace - /// builder, exactly like ECSM. - pub ecall_hint: bool, } impl CpuOperation { @@ -240,8 +235,6 @@ impl CpuOperation { // in the trace builder. let ecall_ecsm = f.ecall && log.src1_val == executor::vm::instruction::execution::ECSM_SYSCALL_NUMBER; - let ecall_hint = - f.ecall && log.src1_val == executor::vm::instruction::execution::HINT_SYSCALL_NUMBER; // Word instructions are fully handled by CPU32; the main CPU row is a // delegate that only advances the PC and sends the CPU32 lookup. We still @@ -360,7 +353,6 @@ impl CpuOperation { ecall_keccak, keccak_state_addr, ecall_ecsm, - ecall_hint, } } diff --git a/prover/src/tables/hint.rs b/prover/src/tables/hint.rs deleted file mode 100644 index cb1dab9f3..000000000 --- a/prover/src/tables/hint.rs +++ /dev/null @@ -1,373 +0,0 @@ -//! HINT table — receiver for the non-constraining `hint` ecall. -//! -//! The `hint` ecall (syscall `u64::MAX - 30`) lets the executor hand the guest a -//! value that is expensive to compute but cheap to verify (modular inverse, sqrt, -//! …); the guest verifies it with ordinary constrained instructions. Unlike a -//! normal `STORE`, the ecall writes the 32-byte output to guest memory *directly* -//! (not through the CPU load/store decode), so those writes are invisible to the -//! CPU op stream — this table is what puts them into the memory argument. -//! -//! The table therefore does exactly four things, and constrains **nothing** about -//! *which* value was hinted (that is the point — soundness lives in the guest's -//! verify). It does constrain *where* the value lands and that it is 32 bytes: -//! -//! 1. **Receives** the `Hint` ecall on the `Ecall` bus (balances the CPU's send; -//! a syscall with no receiver leaves the LogUp argument unbalanced). -//! 2. **Reads `x12`** (`a2`) through the memory argument, which pins `out_addr` to -//! the value the CPU had in that register. The writes below take their base from -//! an ordinary trace column, so without this read that column is free and the -//! witness chooses *where* the 32 bytes land — an arbitrary memory write, which -//! is a strictly larger hole than the unconstrained value. -//! 3. **Sends** the four 8-byte MEMW writes of the output at `out_addr` +0/8/16/24 -//! (received by the MEMW table). Without these the output's initial→final -//! memory chain is unexplained and the memory argument fails to balance. -//! 4. **Range-checks** the 32 output cells as bytes (`AreBytes`). MEMW does not -//! range-check what it receives, so each table that writes fresh values into -//! memory checks its own cells; skipping it lets the witness put arbitrary field -//! elements where loads and the ALU expect bytes. -//! -//! The input read (the ecall also reads `in_addr`) is intentionally **not** modeled: -//! a read leaves the value unchanged, the guest supplies the input via ordinary -//! stores, and nothing depends on the ecall having re-read it — so omitting it is -//! sound and avoids the mixed-timestamp bookkeeping of a partial-buffer read. -//! -//! `mu` is constrained to a bit (`IS_BIT`, the table's only algebraic constraint) — -//! the same guard every other multiplicity-column table carries (ECSM/ECDAS/COMMIT/ -//! STORE/MEMW_R). The `Ecall` bus alone does not establish it: its tuple carries the -//! timestamp, a free column, so the LogUp identity pins only the *sum* of `mu` over -//! the rows sharing a `(ts, syscall)` tuple to the CPU's send — it does not rule out -//! a witness that spreads `mu` across rows with integer weights summing to 1 (a `+1` -//! row plus a `+1`/`-1` pair, each keeping its own `out_addr`, the base the four -//! output writes take). MEMW does NOT catch this: it only ever receives the legal -//! `+1`, while the `-1` cancels an honest STORE on the sender side, so MEMW's own -//! multiplicity constraints stay satisfied and nothing downstream rejects it. The -//! `IS_BIT` on `mu` here is therefore load-bearing -- not a redundant restatement of -//! a check some other table performs. -//! -//! ## Columns (41) -//! - `timestamp[0..1]` (DWordWL): the ecall timestamp `T` -//! - `out_addr[0..1]` (DWordWL): base address of the 32-byte output buffer -//! - `out_bytes[0..31]`: the 32 output bytes (the hint) — **unconstrained** -//! - `mu`: multiplicity flag (1 = real hint call, 0 = padding) — gates every bus -//! - `selector[0..1]` (DWordWL): `a0`, bound to `x10` and range-checked `< 3` -//! - `in_addr[0..1]` (DWordWL): `a1`, bound to `x11`; its low limb is range-checked -//! so the ecall's input range cannot straddle the 32-bit limb boundary -//! -//! Both address low limbs are range-checked against [`HINT_ADDR_LIMB_BOUND`]; see that -//! constant for why the memory bus alone does not bound `out_addr` tightly enough. - -use executor::vm::instruction::execution::HINT_SYSCALL_NUMBER; -use stark::constraints::builder::{ConstraintBuilder, ConstraintSet}; -use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; -use stark::trace::TraceTable; - -use crate::constraints::templates::emit_is_bit; - -use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable, alu_op}; - -/// One past the largest valid hint selector (`a0 ∈ {0, 1, 2}` = FIELD_INV / SCALAR_INV / -/// FIELD_SQRT). Re-exported from the executor, which const-asserts that the bound and its -/// `is_valid_hint_selector` set coincide — so the AIR's range-check cannot drift from the -/// set the executor accepts. -pub use executor::vm::instruction::execution::HINT_SELECTOR_BOUND; - -/// Bound the low 32-bit limb of `in_addr` and `out_addr` must stay under so the -/// ecall's 32-byte range (`+0..+31`) cannot straddle the 2^32 limb boundary. Mirrors -/// the executor's `addr_limb_ok(addr, 31)`: `(addr % 2^32) + 31 < 2^32`, i.e. the -/// largest accepted limb is `2^32 - 32`. -/// -/// Both operands need this explicitly. `in_addr` because it is not on the memory bus -/// at all (the input read is not modelled). `out_addr` because the bus bounds it only -/// to `2^32 - 25`: the write bases are `out_addr_lo + 8i`, so the largest one -/// (`+24`) stops being a canonical limb at `2^32 - 24`, while MEMW's `carry` -/// columns resolve the *bytes* past it correctly. That left a seven-value window -/// (`2^32-31 ..= 2^32-25`) the AIR accepted and the executor rejected with -/// `HintAddressOverflow` — a prover could prove a hint call the VM halts on. -pub const HINT_ADDR_LIMB_BOUND: u64 = (1 << 32) - 31; - -pub mod cols { - /// timestamp[0]: lower 32 bits of the ecall timestamp - pub const TIMESTAMP_0: usize = 0; - /// timestamp[1]: upper 32 bits (always 0 — timestamps fit u32) - pub const TIMESTAMP_1: usize = 1; - /// out_addr[0]: lower 32 bits of the output base address - pub const ADDR_OUT_0: usize = 2; - /// out_addr[1]: upper 32 bits of the output base address - pub const ADDR_OUT_1: usize = 3; - /// out_bytes[0..31]: the 32 output bytes, one per column - pub const OUT: usize = 4; - /// multiplicity flag (1 = real hint call, 0 = padding) - pub const MU: usize = 36; - /// selector[0]: lower 32 bits of `a0` (the hint id) - pub const SEL_0: usize = 37; - /// selector[1]: upper 32 bits of `a0` - pub const SEL_1: usize = 38; - /// in_addr[0]: lower 32 bits of `a1` (the input base address) - pub const ADDR_IN_0: usize = 39; - /// in_addr[1]: upper 32 bits of `a1` - pub const ADDR_IN_1: usize = 40; - - pub const NUM_COLUMNS: usize = 41; - - /// Column of output byte `i` (0..32). - #[inline] - pub const fn out(i: usize) -> usize { - OUT + i - } -} - -/// One `hint` ecall: the timestamp, the output base address, and the 32 output -/// bytes the executor wrote to guest memory (recomputed by the trace builder). -#[derive(Debug, Clone)] -pub struct HintOperation { - pub timestamp: u64, - pub out_addr: u64, - pub out_bytes: [u8; 32], - /// `a0` — the hint selector, bound to `x10` and range-checked `< 3`. - pub hint_id: u64, - /// `a1` — the input base address, bound to `x11` and low-limb range-checked. - pub in_addr: u64, -} - -/// Generates the HINT trace: one row per hint-ecall call (in program order), -/// `mu = 1`; padding rows are all-zero (`mu = 0`, inert on the bus). Empty (all -/// padding) for programs that make no hint calls. -pub fn generate_hint_trace( - ops: &[HintOperation], -) -> TraceTable { - let num_rows = ops.len().next_power_of_two().max(4); - let mut trace = TraceTable::new_main( - crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS), - cols::NUM_COLUMNS, - 1, - ); - let table = &mut trace.main_table; - - for (row, op) in ops.iter().enumerate() { - debug_assert!( - op.timestamp <= u32::MAX as u64, - "HINT timestamp {} exceeds u32", - op.timestamp - ); - table.set_dword_wl(row, cols::TIMESTAMP_0, op.timestamp); - table.set_dword_wl(row, cols::ADDR_OUT_0, op.out_addr); - table.set_bytes(row, cols::OUT, &op.out_bytes); - table.set_dword_wl(row, cols::SEL_0, op.hint_id); - table.set_dword_wl(row, cols::ADDR_IN_0, op.in_addr); - table.set_fe(row, cols::MU, FE::one()); - } - - trace -} - -// ========================================================================= -// Bus interactions -// ========================================================================= - -fn packed(col: usize) -> BusValue { - BusValue::Packed { - start_column: col, - packing: Packing::Direct, - } -} - -/// The eight output bytes of doubleword `chunk` (`out_bytes[8*chunk .. 8*chunk+7]`) -/// as MEMW value elements. -fn out_dword_bytes(chunk: usize) -> [BusValue; 8] { - std::array::from_fn(|b| packed(cols::out(8 * chunk + b))) -} - -/// A 16-element MEMW **write** tuple (CO25): `[is_register=0, base_lo, base_hi, -/// value[8], ts_lo, ts_hi, w2=0, w4=0, w8=1]`. The MEMW table supplies `old`. -fn memw_write(value: [BusValue; 8], base_lo: BusValue, base_hi: BusValue) -> Vec { - let mut v = Vec::with_capacity(16); - v.push(BusValue::constant(0)); // is_register = 0 (memory) - v.push(base_lo); - v.push(base_hi); - v.extend(value); - v.push(packed(cols::TIMESTAMP_0)); // ts_lo - v.push(packed(cols::TIMESTAMP_1)); // ts_hi - v.push(BusValue::constant(0)); // w2 - v.push(BusValue::constant(0)); // w4 - v.push(BusValue::constant(1)); // w8 = 1 (8-byte write) - v -} - -/// A 24-element MEMW **read** tuple (CO24) for a register: `[old[8], is_register=1, -/// base_lo=2*reg, base_hi=0, value[8], ts_lo, ts_hi, w2=1, w4=0, w8=0]`, with -/// `old == value` because a read leaves the register unchanged. Binds `x{reg}` to -/// the `(lo, hi)` column pair at the ecall timestamp. -fn memw_register_read(reg: u64, lo_col: usize, hi_col: usize) -> Vec { - let value = || [packed(lo_col), packed(hi_col)]; - let mut v = Vec::with_capacity(24); - v.extend(value()); // old[0..2] - v.extend(std::iter::repeat_n(BusValue::constant(0), 6)); // old[2..8] - v.push(BusValue::constant(1)); // is_register = 1 - v.push(BusValue::constant(2 * reg)); // base_address lo - v.push(BusValue::constant(0)); // base_address hi - v.extend(value()); // value[0..2] == old - v.extend(std::iter::repeat_n(BusValue::constant(0), 6)); // value[2..8] - v.push(packed(cols::TIMESTAMP_0)); - v.push(packed(cols::TIMESTAMP_1)); - v.push(BusValue::constant(1)); // w2 = 1 (register = 2 words) - v.push(BusValue::constant(0)); // w4 - v.push(BusValue::constant(0)); // w8 - v -} - -/// Bus interactions: -/// - **`Ecall` receiver** (mult `mu`): `[timestamp, cast(HINT_SYSCALL_NUMBER, -/// DWordWL)]` — HALT-shaped, balances the CPU's ECALL send. -/// - **MEMW register-read sender** (mult `mu`): binds `out_addr` to `x12`, the -/// ecall's `a2`. Without it the write addresses below are free columns, so a -/// witness could place the output bytes at any address it likes — an arbitrary -/// memory write, independent of whether the hinted *value* is constrained. -/// - **MEMW write senders** (mult `mu`, ×4): the four 8-byte writes of the output -/// at `out_addr` +0/8/16/24, timestamp `T`. Received by the MEMW table. -/// - **`AreBytes` senders** (mult `mu`, ×16): range-check the 32 output cells. -/// -/// - **MEMW register-read senders** (mult `mu`, ×2): bind `a0` (`x10`, the selector) -/// and `a1` (`x11`, the input address) to their register columns. -/// - **ALU `LT` senders** (mult `mu`, ×3): assert `selector < 3` and that both -/// `in_addr`'s and `out_addr`'s low limbs are `< 2^32 − 31`, matching the executor's -/// up-front rejections (`HintUnknownSelector`, `HintAddressOverflow`). Without them -/// the AIR would accept hints the executor rejects — a malicious prover could prove -/// an execution the VM would halt on. The value stays unconstrained (the guest -/// verifies it); this only pins the *operands* to the executor's accepted set. -pub fn bus_interactions() -> Vec { - let mu = || Multiplicity::Column(cols::MU); - let mut out = Vec::with_capacity(27); - - // ECALL receiver: [ts_lo, ts_hi, syscall_lo32, syscall_hi32]. - out.push(BusInteraction::receiver( - BusId::Ecall, - mu(), - vec![ - packed(cols::TIMESTAMP_0), - packed(cols::TIMESTAMP_1), - BusValue::constant(HINT_SYSCALL_NUMBER & 0xFFFF_FFFF), - BusValue::constant(HINT_SYSCALL_NUMBER >> 32), - ], - )); - - // Bind out_addr to x12 (a2): without this the write base below is a free column. - out.push(BusInteraction::sender( - BusId::Memw, - mu(), - memw_register_read(12, cols::ADDR_OUT_0, cols::ADDR_OUT_1), - )); - - // Bind a0 (x10 = selector) and a1 (x11 = in_addr). Without these the range-checks - // below would constrain free columns instead of the registers the CPU held. - out.push(BusInteraction::sender( - BusId::Memw, - mu(), - memw_register_read(10, cols::SEL_0, cols::SEL_1), - )); - out.push(BusInteraction::sender( - BusId::Memw, - mu(), - memw_register_read(11, cols::ADDR_IN_0, cols::ADDR_IN_1), - )); - - // ALU LT: selector < 3 (full 64-bit value), asserting the result is 1. A witness - // with an out-of-range selector has no matching LT row and unbalances the bus. - // ALU LT tuple (matching the LT table's receiver): `[lhs_lo, lhs_hi, rhs_lo, - // rhs_hi, op_encoding, result, 0]` — both operands are two elements (low, high - // 32-bit words), `op_encoding = LT` for an unsigned non-inverted compare, and - // `result = 1` asserts the strict inequality holds. - // - // selector < 3 (full 64-bit value: SEL_0/SEL_1). - out.push(BusInteraction::sender( - BusId::Alu, - mu(), - vec![ - BusValue::Packed { - start_column: cols::SEL_0, - packing: Packing::DWordWL, - }, - BusValue::constant(HINT_SELECTOR_BOUND), - BusValue::constant(0), - BusValue::constant(alu_op::LT as u64), - BusValue::constant(1), - BusValue::constant(0), - ], - )); - - // in_addr's and out_addr's low limbs < 2^32 - 31, matching addr_limb_ok(addr, 31). - // The lhs high word is a literal 0, so only the low limb is compared — exactly the - // executor's check, which ignores the high limb. `out_addr` needs its own check even - // though it is on the memory bus: the bus only bounds it to 2^32 - 25 (see - // HINT_ADDR_LIMB_BOUND), leaving a window the executor rejects. - for addr_lo in [cols::ADDR_IN_0, cols::ADDR_OUT_0] { - out.push(BusInteraction::sender( - BusId::Alu, - mu(), - vec![ - packed(addr_lo), - BusValue::constant(0), - BusValue::constant(HINT_ADDR_LIMB_BOUND), - BusValue::constant(0), - BusValue::constant(alu_op::LT as u64), - BusValue::constant(1), - BusValue::constant(0), - ], - )); - } - - // write output: 4 doublewords at out_addr + 8i (timestamp T). - for i in 0..4 { - let base_lo = BusValue::linear(vec![ - LinearTerm::Column { - coefficient: 1, - column: cols::ADDR_OUT_0, - }, - LinearTerm::Constant((8 * i) as i64), - ]); - out.push(BusInteraction::sender( - BusId::Memw, - mu(), - memw_write(out_dword_bytes(i), base_lo, packed(cols::ADDR_OUT_1)), - )); - } - - // ARE_BYTES[out_bytes[2i], out_bytes[2i+1]]: the output cells are free columns - // that enter memory as MEMW write values, and MEMW range-checks nothing it - // receives. Every other table that puts fresh values into memory (STORE, KECCAK, - // ECSM, PAGE) range-checks its own cells for this reason: the value is allowed to - // be *wrong* here, but it must still be 32 bytes, or the witness can smuggle - // arbitrary field elements into memory and break the byte decomposition that - // loads and the ALU depend on. 16 sends, pairing cells as ECSM/KECCAK do. - for i in 0..16 { - out.push(BusInteraction::sender( - BusId::AreBytes, - mu(), - vec![packed(cols::out(2 * i)), packed(cols::out(2 * i + 1))], - )); - } - - out -} - -// ========================================================================= -// Single-source constraint set (ConstraintBuilder front-end) -// ========================================================================= - -/// The HINT table's single transition constraint: `mu·(1−mu) = 0`. -/// -/// `mu` is the multiplicity gating every one of this table's bus interactions -/// (the `Ecall` receive, the three register reads, the three `LT` range-checks, the -/// four output writes, the 16 byte range-checks). It must be boolean, or a witness -/// could put a non-`{0,1}` value on the `AreBytes`/MEMW sends. This is load-bearing, -/// not a redundant restatement of a bus check: the `Ecall` bus pins only the *sum* -/// of `mu` over the rows sharing a tuple — see the module-level docs for the -/// spread-multiplicity witness it rules out. -#[derive(Clone, Copy)] -pub struct HintConstraints; - -impl ConstraintSet for HintConstraints { - fn eval>(&self, b: &mut B) { - // idx 0: IS_BIT for mu. - emit_is_bit(b, 0, cols::MU, None); - } -} diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index f1a899f56..0a86e4149 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -34,7 +34,6 @@ pub mod ecsm; pub mod eq; pub mod global_memory; pub mod halt; -pub mod hint; pub mod keccak; pub mod keccak_rc; pub mod keccak_rnd; diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index 6788bee08..a1784724a 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -170,10 +170,11 @@ impl PageConfig { // ========================================================================= /// Number of pages the private input occupies, starting at -/// `PRIVATE_INPUT_START_INDEX`. The wire format is the 4-byte length prefix plus -/// the data ([`Memory::store_private_inputs`]), and `PRIVATE_INPUT_START_INDEX` is -/// page-aligned, so the span is `ceil((prefix + len) / page_size)` consecutive -/// pages (0 when there is no input). +/// `PRIVATE_INPUT_START_INDEX`. The wire format is `[len:4][data][pad8]` plus the +/// always-written 8-byte hint-arena header and its 32-byte slots +/// ([`Memory::store_private_inputs`]), and `PRIVATE_INPUT_START_INDEX` is +/// page-aligned, so the span is `ceil((align8(4 + len) + 8 + 32 * hint_count) / +/// page_size)` consecutive pages (0 when there is neither input nor hints). /// /// SINGLE source of truth: the monolithic trace builder, the continuation prover, /// and both verifiers' classification all derive from this count — a divergence @@ -181,12 +182,15 @@ impl PageConfig { /// the other commits it, which is a soundness bug, so do not reimplement it. /// /// [`Memory::store_private_inputs`]: executor::vm::memory::Memory::store_private_inputs -pub(crate) fn private_input_page_count(private_inputs: &[u8]) -> usize { - use executor::vm::memory::PRIVATE_INPUT_LENGTH_PREFIX_BYTES; - if private_inputs.is_empty() { +pub(crate) fn private_input_page_count(private_inputs: &[u8], hints: &[[u8; 32]]) -> usize { + use executor::vm::memory::{HINT_ARENA_HEADER_BYTES, HINT_SLOT_BYTES, hint_arena_header_offset}; + if private_inputs.is_empty() && hints.is_empty() { return 0; } - (PRIVATE_INPUT_LENGTH_PREFIX_BYTES + private_inputs.len()).div_ceil(DEFAULT_PAGE_SIZE) + let extent = hint_arena_header_offset(private_inputs.len() as u64) + + HINT_ARENA_HEADER_BYTES + + hints.len() as u64 * HINT_SLOT_BYTES; + (extent as usize).div_ceil(DEFAULT_PAGE_SIZE) } /// Whether `page_base` is one of the first `num_private_input_pages` pages starting @@ -224,6 +228,11 @@ pub(crate) fn private_input_page_bases( /// a MAX-size input including its length prefix — no slack (an honest max-size /// input occupies exactly this many pages). Both the monolithic and continuation /// verifiers bound the deserialized, untrusted count with this before sizing AIRs. +/// +/// Still valid with the hint arena: `encode_private_input_region` caps the whole +/// two-section region (main section + always-written arena header + hint slots) +/// at `4 + MAX_PRIVATE_INPUT_SIZE` bytes, so no honest region can span more pages +/// than this bound. pub(crate) fn max_private_input_pages() -> usize { use executor::vm::memory::{MAX_PRIVATE_INPUT_SIZE, PRIVATE_INPUT_LENGTH_PREFIX_BYTES}; (MAX_PRIVATE_INPUT_SIZE as usize + PRIVATE_INPUT_LENGTH_PREFIX_BYTES) diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 29874caef..3dffde11f 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -51,7 +51,6 @@ use super::ecdas; use super::ecsm; use super::eq; use super::halt; -use super::hint; use super::keccak::{self, KeccakOperation}; use super::keccak_rc; use super::keccak_rnd::{self, KeccakRoundOperation}; @@ -550,7 +549,6 @@ fn collect_ops_from_cpu( Vec, Vec, Vec, - Vec, ) { let mut memw = MemwBuckets::with_register_capacity(cpu_ops.len() * 3); let mut load_ops = Vec::with_capacity(cpu_ops.len() / 8 + 1); @@ -562,7 +560,6 @@ fn collect_ops_from_cpu( let mut cpu32_ops = Vec::new(); let mut ecsm_ops = Vec::new(); let mut ecdas_ops = Vec::new(); - let mut hint_ops = Vec::new(); // Seed from the carried x254 (0 for a monolithic run or the first epoch) so a // continuation epoch indexes its commits globally, matching the x254 the // register binding transports across epochs. Resetting to 0 here would drift @@ -657,13 +654,6 @@ fn collect_ops_from_cpu( ecdas_ops.extend(ecdas_rows); } - // Collect Hint ecall operations (the 32-byte output write). - if op.ecall_hint { - let (hint_memw, hint_op) = collect_hint_ops(op, memory_state, register_state); - memw.extend_ops(hint_memw); - hint_ops.push(hint_op); - } - // --- ALU chip dispatch (no state tracking) --- // Word (`*W`) instructions are delegated to CPU32 (which itself drives // the ALU chips); the main CPU does not send the ALU bus for them, so we @@ -719,7 +709,6 @@ fn collect_ops_from_cpu( cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, ) } @@ -959,81 +948,6 @@ fn collect_ecsm_ops( (memw_ops, ecsm_op, ecdas_ops) } -/// Collects the memory operations for a `Hint` ecall. -/// -/// The `hint` ecall writes a 32-byte value (a modular inverse / sqrt) to guest -/// memory *directly* — bypassing the CPU load/store decode — so the trace builder -/// must reproduce that write itself: the value is not carried in the CPU log. We -/// re-derive the operand addresses from the register state (a0/a1/a2 = x10/x11/x12, -/// like ECSM), read the input from the replayed memory, recompute the output with -/// the executor's `compute_hint` (deterministic, same k256 arithmetic), then emit -/// four 8-byte MEMW writes at `out_addr` +0/8/16/24 and advance `memory_state`. -/// -/// The input read is intentionally not modeled (a read leaves the value unchanged; -/// the guest supplied the input via ordinary stores). The value itself is -/// unconstrained — soundness lives in the guest's in-circuit verify. -fn collect_hint_ops( - op: &CpuOperation, - memory_state: &mut MemoryState, - register_state: &mut RegisterState, -) -> (Vec, hint::HintOperation) { - let t = op.timestamp; - let hint_id = register_state.read(10).0; - let in_addr = register_state.read(11).0; - let out_addr = register_state.read(12).0; - - let mut memw_ops = Vec::with_capacity(7); - - // Bind a0/a1/a2 (x10/x11/x12) at ts through the memory argument. x12 ties the - // output-write base below to the ecall's a2; x10 (selector) and x11 (in_addr) pin - // the operands the HINT table range-checks against the executor's accepted set, so - // the AIR cannot prove a hint the executor would reject. All three are register - // reads (old == value; a read leaves the register unchanged). See `tables::hint`. - for (reg, value) in [(10u8, hint_id), (11, in_addr), (12, out_addr)] { - let reg_value = pack_register_value(value); - let (_old_val, old_ts) = register_state.read(reg); - memw_ops.push( - MemwOperation::new(true, 2 * reg as u64, reg_value, t, 2, true) - .with_old(reg_value, [old_ts, old_ts, 0, 0, 0, 0, 0, 0]), - ); - register_state.write(reg, value, t); - } - - // Read the 32-byte big-endian input from the replayed memory. - let mut input = [0u8; 32]; - for (i, b) in input.iter_mut().enumerate() { - *b = memory_state.read_byte(in_addr.wrapping_add(i as u64)).0; - } - - // Recompute the output exactly as the executor did (the value isn't in the log). - let out_bytes = executor::vm::instruction::execution::compute_hint(hint_id, &input); - - // Emit the 32-byte output as four 8-byte MEMW writes at ts = T. - for i in 0..4 { - let addr = out_addr.wrapping_add((8 * i) as u64); - let mut value = [0u32; 8]; - let mut dword = 0u64; - for j in 0..8 { - let byte = out_bytes[8 * i + j]; - value[j] = byte as u32; - dword |= (byte as u64) << (8 * j); - } - let (old_vals, old_ts) = memory_state.read_bytes(addr, 8); - memw_ops - .push(MemwOperation::new(false, addr, value, t, 8, false).with_old(old_vals, old_ts)); - memory_state.write_bytes(addr, dword, 8, t); - } - - let hint_op = hint::HintOperation { - timestamp: t, - out_addr, - out_bytes, - hint_id, - in_addr, - }; - (memw_ops, hint_op) -} - /// Collects register read/write operations (M1, M3, M5) from CpuOperation, /// pushing them into `memw_ops`. fn collect_register_ops_from_cpu( @@ -2090,21 +2004,27 @@ fn add_padding_byte_checks(hist: &mut bitwise::BitwiseHistogram, num_padding_row /// /// This must be called BEFORE bitwise multiplicities are updated. /// -/// Encode private input as `[len_u32_LE][data]` — the canonical wire format. -/// Must match `executor::vm::memory::Memory::store_private_inputs`. -fn private_input_bytes(private_input: &[u8]) -> Vec { - let len_bytes = (private_input.len() as u32).to_le_bytes(); - len_bytes - .iter() - .chain(private_input.iter()) - .copied() - .collect() +/// Encode the private-input region in the canonical wire format +/// (`[len][data][pad8][count][pad][slots]`) by delegating to the executor's +/// single source of truth, [`executor::vm::memory::encode_private_input_region`]. +/// +/// Panics if the region exceeds the reserved window. Inputs reaching this via the +/// prove path were already validated by `Memory::store_private_inputs` inside +/// `Executor::new`; a test calling the trace builder directly with an oversized +/// input gets a loud panic here instead of a silently truncated region. +fn private_input_bytes(private_input: &[u8], hints: &[[u8; 32]]) -> Vec { + executor::vm::memory::encode_private_input_region(private_input, hints) + .expect("private input plus hints exceed the reserved private-input window") } /// Build the initial-memory image (byte address -> value) from the ELF segments /// and the private-input region. Single source of "what memory starts as", read /// by both `MemoryState` seeding and PAGE/bitwise init. -pub(crate) fn build_initial_image(elf: &Elf, private_input: &[u8]) -> HashMap { +pub(crate) fn build_initial_image( + elf: &Elf, + private_input: &[u8], + hints: &[[u8; 32]], +) -> HashMap { let mut image: HashMap = HashMap::new(); for segment in &elf.data { for (i, &word) in segment.values.iter().enumerate() { @@ -2116,9 +2036,11 @@ pub(crate) fn build_initial_image(elf: &Elf, private_input: &[u8]) -> HashMap HashMap PagedMem { +pub(crate) fn build_initial_image_paged( + elf: &Elf, + private_input: &[u8], + hints: &[[u8; 32]], +) -> PagedMem { let mut image = PagedMem::new(0u8); for segment in &elf.data { for (i, &word) in segment.values.iter().enumerate() { @@ -2141,9 +2067,9 @@ pub(crate) fn build_initial_image_paged(elf: &Elf, private_input: &[u8]) -> Page } } } - if !private_input.is_empty() { + if !(private_input.is_empty() && hints.is_empty()) { use executor::vm::memory::PRIVATE_INPUT_START_INDEX; - for (i, &b) in private_input_bytes(private_input).iter().enumerate() { + for (i, &b) in private_input_bytes(private_input, hints).iter().enumerate() { image.set(PRIVATE_INPUT_START_INDEX + i as u64, b); } } @@ -2334,23 +2260,6 @@ fn collect_bitwise_from_commit(commit_ops: &[CommitOperation]) -> Vec Vec { - let mut lookups = Vec::with_capacity(16 * hint_ops.len()); - for op in hint_ops { - for i in 0..16 { - lookups.push(BitwiseOperation::byte_op( - BitwiseOperationType::AreBytes, - op.out_bytes[2 * i], - op.out_bytes[2 * i + 1], - )); - } - } - lookups -} - // ============================================================================= // BITWISE lookup helpers // ============================================================================= @@ -2688,6 +2597,7 @@ fn generate_page_tables( image: &I, memory_state: &MemoryState, private_input: &[u8], + hints: &[[u8; 32]], exclude_touched: bool, ) -> ( Vec>, @@ -2715,7 +2625,7 @@ fn generate_page_tables( // Determine which page bases hold private input data — count-based, via the // shared helpers (single source of truth with the continuation path). - let num_private_input_pages = page::private_input_page_count(private_input); + let num_private_input_pages = page::private_input_page_count(private_input, hints); for &page_base in &page_bases { let config = if page::is_private_input_page(page_base, num_private_input_pages) { @@ -2870,9 +2780,6 @@ pub struct Traces { /// ECDAS double/add table (variable rows per ecall) pub ecdas: TraceTable, - /// HINT table (one row per non-constraining hint ecall). - pub hint: TraceTable, - /// MEMW_R register-only fast-path traces (split into chunks of max_rows::MEMW_R) pub memw_registers: Vec>, /// Local-to-global boundary table for continuation epochs. Empty unless the @@ -2915,8 +2822,6 @@ struct CollectedOps { // EC scalar-multiplication accelerator chips. ecsm_ops: Vec, ecdas_ops: Vec, - // Non-constraining hint ecall. - hint_ops: Vec, } /// Chunk raw ops and generate one trace table per chunk. When `storage_mode` @@ -2971,7 +2876,6 @@ fn collect_all_ops( cpu32_ops: Vec, ecsm_ops: Vec, ecdas_ops: Vec, - hint_ops: Vec, register_state: &mut RegisterState, is_final: bool, ) -> CollectedOps { @@ -3114,7 +3018,6 @@ fn collect_all_ops( cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, } } @@ -3135,6 +3038,7 @@ fn build_traces( max_rows: &super::MaxRowsConfig, #[cfg(feature = "disk-spill")] storage_mode: StorageMode, private_input: &[u8], + hints: &[[u8; 32]], is_final: bool, l2g_memory_bookend: bool, ) -> Result { @@ -3158,7 +3062,6 @@ fn build_traces( cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, } = ops; // ===================================================================== @@ -3166,16 +3069,6 @@ fn build_traces( // ===================================================================== lt_ops.extend(collect_lt_from_memw(&memw_ops)); lt_ops.extend(collect_lt_from_memw_aligned(&memw_aligned_ops)); - // HINT range-checks: selector < 3 and both address low limbs < 2^32 - 31 (matching - // the executor's HintUnknownSelector / HintAddressOverflow rejections). Three LT ops - // per hint call; the HINT table sends the matching ALU LT interactions. - lt_ops.extend(hint_ops.iter().flat_map(|op| { - [ - LtOperation::new(op.hint_id, hint::HINT_SELECTOR_BOUND, false), - LtOperation::new(op.in_addr & 0xFFFF_FFFF, hint::HINT_ADDR_LIMB_BOUND, false), - LtOperation::new(op.out_addr & 0xFFFF_FFFF, hint::HINT_ADDR_LIMB_BOUND, false), - ] - })); // ===================================================================== // PHASE 4: All → Bitwise lookups @@ -3205,8 +3098,7 @@ fn build_traces( // chunk size used to split them into instances so multiplicities match the per-instance // sends. MEMW_R sends IS_HALFWORD[timestamp_0 - old_timestamp_lo - 1]. PAGE does a // batched ARE_BYTES[init, fini] per row (skipped in continuation epochs, which the L2G - // table owns). COMMIT sends AreBytes+IsHalfword; KECCAK_RND sends XOR/AND/ARE_BYTES/HWSL; - // HINT sends ARE_BYTES for its 32 output cells. + // table owns). COMMIT sends AreBytes+IsHalfword; KECCAK_RND sends XOR/AND/ARE_BYTES/HWSL. // We never concatenate the lookups into one giant `Vec` (~140 M ops / // ~560 MB at 10-tx whose only consumer is the multiplicity count). Each collector bumps // the `BitwiseHistogram` it is handed: the heavy sources (MEMW_R one-per-row, PAGE @@ -3245,7 +3137,6 @@ fn build_traces( Box::new(|h| h.add_ops(&collect_bitwise_from_keccak(&keccak_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecsm(&ecsm_ops))), Box::new(|h| h.add_ops(&collect_bitwise_from_ecdas(&ecdas_ops))), - Box::new(|h| h.add_ops(&collect_bitwise_from_hint(&hint_ops))), Box::new(|h| add_padding_byte_checks(h, num_padding_rows)), ]; if let Some(image) = initial_image @@ -3523,7 +3414,7 @@ fn build_traces( // every touched cell's Memory init/fini, and every untouched PAGE row // self-cancels (init==fini, ts=0), so PAGE contributes nothing here. Some(image) if !l2g_memory_bookend => { - generate_page_tables(image, memory_state, private_input, l2g_memory_bookend) + generate_page_tables(image, memory_state, private_input, hints, l2g_memory_bookend) } _ => (Vec::new(), Vec::new()), }; @@ -3532,8 +3423,6 @@ fn build_traces( // ECSM accelerator traces (empty/all-padding for programs that do not use ECSM). let gen_ecsm = || ecsm::generate_ecsm_trace(&ecsm_ops); let gen_ecdas = || ecdas::generate_ecdas_trace(&ecdas_ops); - // HINT table (all-padding for programs that make no hint ecalls). - let gen_hint = || hint::generate_hint_trace(&hint_ops); let (mut cpus_slot, mut memws_slot, mut memw_aligneds_slot, mut memw_registers_slot) = (None, None, None, None); @@ -3546,7 +3435,6 @@ fn build_traces( let (mut eqs_slot, mut bytewises_slot, mut stores_slot, mut cpu32s_slot) = (None, None, None, None); let (mut ecsm_slot, mut ecdas_slot) = (None, None); - let mut hint_slot = None; #[cfg(feature = "disk-spill")] let sequential = storage_mode == StorageMode::Disk || cfg!(not(feature = "parallel")); @@ -3588,7 +3476,6 @@ fn build_traces( spawn_into!(cpu32s_slot, gen_cpu32s); spawn_into!(ecsm_slot, gen_ecsm); spawn_into!(ecdas_slot, gen_ecdas); - spawn_into!(hint_slot, gen_hint); }); } else { cpus_slot = Some(gen_cpus()); @@ -3616,7 +3503,6 @@ fn build_traces( cpu32s_slot = Some(gen_cpu32s()); ecsm_slot = Some(gen_ecsm()); ecdas_slot = Some(gen_ecdas()); - hint_slot = Some(gen_hint()); } const PHASE5_RAN: &str = "phase 5 generation ran in one of the branches above"; @@ -3651,7 +3537,6 @@ fn build_traces( let mut halt_trace = halt_slot.expect(PHASE5_RAN); let ecsm_trace = ecsm_slot.expect(PHASE5_RAN); let ecdas_trace = ecdas_slot.expect(PHASE5_RAN); - let hint_trace = hint_slot.expect(PHASE5_RAN); // Fixed-size and per-page tables aren't built through `chunk_and_generate`, // so spill them here before returning. @@ -3719,7 +3604,6 @@ fn build_traces( keccak_rc: keccak_rc_trace, ecsm: ecsm_trace, ecdas: ecdas_trace, - hint: hint_trace, memw_registers, local_to_global, touched_memory_cells, @@ -3778,6 +3662,7 @@ pub fn count_table_lengths( logs: &[Log], max_rows: &super::MaxRowsConfig, private_input: &[u8], + hints: &[[u8; 32]], ) -> Result { // Phase 0: ELF → instructions + DECODE row count. let instructions = decode::instructions_from_elf(elf) @@ -3786,7 +3671,8 @@ pub fn count_table_lengths( let decode_rows = (instructions.len() as u64 + 1).next_power_of_two().max(2); // Memory + register state for partition predicates that need timestamps. - let mut memory_state = MemoryState::from_image(&build_initial_image(elf, private_input)); + let mut memory_state = + MemoryState::from_image(&build_initial_image(elf, private_input, hints)); let mut register_state = RegisterState::new(elf.entry_point); // Raw counts (pre-chunking + pre-padding). @@ -3893,25 +3779,6 @@ pub fn count_table_lengths( .ok_or_else(|| Error::Execution("commit index exceeds u32 range".into()))?; } - if cpu_op.ecall_hint { - // Mirror `collect_hint_ops`: three register reads (a0/a1/a2) and four - // 8-byte output writes go through the memory argument, plus the three LT - // range-checks (selector < 3, in_addr and out_addr low limbs). Replaying it - // here keeps memory/register state in sync with generation, exactly like - // commit above. - let (hint_memw, _hint_op) = - collect_hint_ops(&cpu_op, &mut memory_state, &mut register_state); - for memw_op in &hint_memw { - partition_memw( - memw_op, - &mut memw_by_width, - &mut memw_aligned_count, - &mut memw_register_count, - ); - } - lt_count += 3; - } - // CPU-side per-instruction-kind counters (non-word; word → CPU32, B5b) let f = &cpu_op.decode.fields; if !f.word_instr && f.is_lt() { @@ -4001,7 +3868,6 @@ impl Traces { use super::ecsm::cols::NUM_COLUMNS as ECSM_COLS; use super::eq::cols::NUM_COLUMNS as EQ_COLS; use super::halt::cols::NUM_COLUMNS as HALT_COLS; - use super::hint::cols::NUM_COLUMNS as HINT_COLS; use super::keccak::cols::NUM_COLUMNS as KECCAK_COLS; use super::keccak_rc::NUM_PRECOMPUTED_COLS as KECCAK_RC_PRECOMPUTED; use super::keccak_rc::cols::NUM_COLUMNS as KECCAK_RC_COLS; @@ -4040,7 +3906,6 @@ impl Traces { keccak_rc, ecsm, ecdas, - hint, memw_registers, eqs, bytewises, @@ -4108,7 +3973,6 @@ impl Traces { } total += (ecsm.num_rows() * ECSM_COLS) as u64; total += (ecdas.num_rows() * ECDAS_COLS) as u64; - total += (hint.num_rows() * HINT_COLS) as u64; total } @@ -4150,7 +4014,6 @@ impl Traces { let n_cpu32 = aux_cols(super::cpu32::bus_interactions().len()); let n_ecsm = aux_cols(super::ecsm::bus_interactions().len()); let n_ecdas = aux_cols(super::ecdas::bus_interactions().len()); - let n_hint = aux_cols(super::hint::bus_interactions().len()); let Traces { cpus, @@ -4173,7 +4036,6 @@ impl Traces { keccak_rc, ecsm, ecdas, - hint, memw_registers, eqs, bytewises, @@ -4241,7 +4103,6 @@ impl Traces { } total += (ecsm.num_rows() * n_ecsm) as u64; total += (ecdas.num_rows() * n_ecdas) as u64; - total += (hint.num_rows() * n_hint) as u64; total } @@ -4273,7 +4134,7 @@ impl Traces { pub fn page_configs_from_elf(elf: &Elf) -> Vec { use std::collections::BTreeSet; - let init_page_data = build_init_page_data(&build_initial_image(elf, &[])); + let init_page_data = build_init_page_data(&build_initial_image(elf, &[], &[])); let page_bases: BTreeSet = init_page_data.keys().copied().collect(); @@ -4460,9 +4321,10 @@ impl Traces { logs: &[Log], max_rows: &super::MaxRowsConfig, private_input: &[u8], + hints: &[[u8; 32]], #[cfg(feature = "disk-spill")] storage_mode: StorageMode, ) -> Result { - let initial_image = build_initial_image(elf, private_input); + let initial_image = build_initial_image(elf, private_input, hints); let register_init = register::register_init_from_entry_point(elf.entry_point); Self::from_image_and_logs( elf, @@ -4471,6 +4333,7 @@ impl Traces { logs, max_rows, private_input, + hints, true, false, #[cfg(feature = "disk-spill")] @@ -4498,6 +4361,7 @@ impl Traces { logs: &[Log], max_rows: &super::MaxRowsConfig, private_input: &[u8], + hints: &[[u8; 32]], is_final: bool, l2g_memory_bookend: bool, #[cfg(feature = "disk-spill")] storage_mode: StorageMode, @@ -4510,6 +4374,7 @@ impl Traces { logs, max_rows, private_input, + hints, is_final, l2g_memory_bookend, #[cfg(feature = "disk-spill")] @@ -4531,6 +4396,7 @@ impl Traces { logs: &[Log], max_rows: &super::MaxRowsConfig, private_input: &[u8], + hints: &[[u8; 32]], is_final: bool, l2g_memory_bookend: bool, #[cfg(feature = "disk-spill")] storage_mode: StorageMode, @@ -4544,6 +4410,7 @@ impl Traces { register_init, max_rows, private_input, + hints, is_final, l2g_memory_bookend, #[cfg(feature = "disk-spill")] @@ -4595,7 +4462,6 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, ) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); #[cfg(feature = "instruments")] drop(__sp); @@ -4614,7 +4480,6 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, &mut register_state, is_final, ); @@ -4642,6 +4507,7 @@ impl Traces { register_init: &[u32], max_rows: &super::MaxRowsConfig, private_input: &[u8], + hints: &[[u8; 32]], is_final: bool, l2g_memory_bookend: bool, #[cfg(feature = "disk-spill")] storage_mode: StorageMode, @@ -4670,6 +4536,7 @@ impl Traces { #[cfg(feature = "disk-spill")] storage_mode, private_input, + hints, is_final, l2g_memory_bookend, ); @@ -4708,7 +4575,6 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, ) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); let ops = collect_all_ops( @@ -4723,7 +4589,6 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, - hint_ops, &mut register_state, true, ); @@ -4744,6 +4609,7 @@ impl Traces { #[cfg(feature = "disk-spill")] StorageMode::Ram, &[], + &[], true, false, ) diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index d6a8b8608..c0426a538 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -66,9 +66,6 @@ use crate::tables::ecsm::{ }; use crate::tables::eq::{EqConstraints, bus_interactions as eq_bus_interactions, cols as eq_cols}; use crate::tables::halt::{bus_interactions as halt_bus_interactions, cols as halt_cols}; -use crate::tables::hint::{ - HintConstraints, bus_interactions as hint_bus_interactions, cols as hint_cols, -}; use crate::tables::keccak::{ KeccakConstraints, bus_interactions as keccak_bus_interactions, cols as keccak_cols, }; @@ -246,7 +243,7 @@ pub fn asm_elf_bytes(name: &str) -> Vec { pub fn run_asm_elf(name: &str) -> (Elf, Vec, U64HashMap) { let elf_data = asm_elf_bytes(name); let elf = Elf::load(&elf_data).expect("Failed to load ELF"); - let executor = Executor::new(&elf, vec![]).expect("Failed to create executor"); + let executor = Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); (elf, result.logs, result.instructions) } @@ -897,21 +894,6 @@ pub fn create_halt_air(proof_options: &ProofOptions) -> ConcreteVmAir ConcreteVmAir { - build_air( - hint_cols::NUM_COLUMNS, - hint_bus_interactions(), - proof_options, - 1, - HintConstraints, - "HINT", - ) -} - /// Create COMMIT AIR with constraints and bus interactions. pub fn create_commit_air(proof_options: &ProofOptions) -> ConcreteVmAir { build_air( diff --git a/prover/src/tests/constraint_program_device_tests.rs b/prover/src/tests/constraint_program_device_tests.rs index a29a7cb49..a2863b2f0 100644 --- a/prover/src/tests/constraint_program_device_tests.rs +++ b/prover/src/tests/constraint_program_device_tests.rs @@ -181,5 +181,4 @@ fn all_table_programs_lower_and_match_folders() { check_air_device(&create_keccak_rc_air(&opts), "KECCAK_RC"); check_air_device(&create_ecsm_air(&opts), "ECSM"); check_air_device(&create_ecdas_air(&opts), "ECDAS"); - check_air_device(&create_hint_air(&opts), "HINT"); } diff --git a/prover/src/tests/constraint_program_tests.rs b/prover/src/tests/constraint_program_tests.rs index e227da53d..3ae46494d 100644 --- a/prover/src/tests/constraint_program_tests.rs +++ b/prover/src/tests/constraint_program_tests.rs @@ -179,5 +179,4 @@ fn all_table_programs_match_folders() { check_air(&create_keccak_rc_air(&opts), "KECCAK_RC"); check_air(&create_ecsm_air(&opts), "ECSM"); check_air(&create_ecdas_air(&opts), "ECDAS"); - check_air(&create_hint_air(&opts), "HINT"); } diff --git a/prover/src/tests/constraint_set_tests_b.rs b/prover/src/tests/constraint_set_tests_b.rs index a7f68ecfd..0348c2b70 100644 --- a/prover/src/tests/constraint_set_tests_b.rs +++ b/prover/src/tests/constraint_set_tests_b.rs @@ -299,19 +299,3 @@ mod cpu { check_table("cpu", &CpuConstraints, cols::NUM_COLUMNS); } } - -// ============================================================================= -// hint.rs -// ============================================================================= - -mod hint { - use super::*; - use crate::tables::hint::{HintConstraints, cols}; - - #[test] - fn hint_constraint_set_folder_capture_agree() { - // The one constraint is IS_BIT(mu): a single dense, idx-0, base-field root. - assert_eq!(HintConstraints.meta().len(), 1); - check_table("hint", &HintConstraints, cols::NUM_COLUMNS); - } -} diff --git a/prover/src/tests/count_table_lengths_drift_tests.rs b/prover/src/tests/count_table_lengths_drift_tests.rs index 7337f0790..4fad8cdae 100644 --- a/prover/src/tests/count_table_lengths_drift_tests.rs +++ b/prover/src/tests/count_table_lengths_drift_tests.rs @@ -4,16 +4,15 @@ use crate::tables::MaxRowsConfig; use crate::tables::trace_builder::{Traces, count_table_lengths}; use crate::test_utils::run_asm_elf; use executor::elf::Elf; -use executor::vm::execution::Executor; use executor::vm::logs::Log; fn assert_count_table_lengths_matches(elf: &Elf, logs: &[Log]) { let max_rows = MaxRowsConfig::default(); let predicted = - count_table_lengths(elf, logs, &max_rows, &[]).expect("count_table_lengths succeeds"); + count_table_lengths(elf, logs, &max_rows, &[], &[]).expect("count_table_lengths succeeds"); let traces = - Traces::from_elf_and_logs_minimal(elf, logs, &max_rows, &[]).expect("trace build succeeds"); + Traces::from_elf_and_logs_minimal(elf, logs, &max_rows, &[], &[]).expect("trace build succeeds"); let sum_heights = |tables: &[stark::trace::TraceTable<_, _>]| -> u64 { tables.iter().map(|t| t.main_table.height as u64).sum() @@ -98,31 +97,3 @@ fn count_table_lengths_matches_traces() { let (elf, logs, _) = run_asm_elf("fib_iterative_372k"); assert_count_table_lengths_matches(&elf, &logs); } - -/// The `hint` ecall routes three register reads (`a0`/`a1`/`a2`) and four output -/// writes through the memory argument, plus two LT range-checks (selector, in_addr). -/// `count_table_lengths` must replay all of that exactly, or `memw_register` (an -/// exact-match table) drifts. Uses a real hint guest so the counts are non-trivial. -#[test] -fn count_table_lengths_matches_nonempty_hint_trace() { - let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("workspace root") - .to_path_buf(); - let elf_bytes = - std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_min.elf")) - .expect("hint_min.elf not found — run `make compile-programs-rust`"); - let elf = Elf::load(&elf_bytes).expect("valid hint guest ELF"); - let result = Executor::new(&elf, vec![]) - .expect("executor") - .run() - .expect("hint guest execution"); - - assert!( - result.logs.iter().any(|log| { - log.src1_val == executor::vm::instruction::execution::HINT_SYSCALL_NUMBER - }), - "fixture must contain a hint ecall" - ); - assert_count_table_lengths_matches(&elf, &result.logs); -} diff --git a/prover/src/tests/hint_tests.rs b/prover/src/tests/hint_tests.rs deleted file mode 100644 index 479e7b001..000000000 --- a/prover/src/tests/hint_tests.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! HINT constraint tests. - -use crate::tables::hint::{ - HINT_ADDR_LIMB_BOUND, HintConstraints, HintOperation, bus_interactions, cols, - generate_hint_trace, -}; -use crate::tables::types::{BusId, FE, GoldilocksExtension, GoldilocksField}; -use math::field::element::FieldElement; -use stark::constraints::builder::{ConstraintSet, ProverEvalFolder}; -use stark::frame::Frame; -use stark::lookup::{BusValue, LinearTerm}; -use stark::table::TableView; -use stark::traits::TransitionEvaluationContext; - -/// Evaluate the HINT constraint set on one main-trace row. -fn eval_main_row(main: Vec) -> Vec { - let n = HintConstraints.meta().len(); - let frame = Frame::::new(vec![TableView::new( - vec![main], - vec![vec![]], - )]); - let no_e: Vec> = vec![]; - let offset_e = FieldElement::::zero(); - let ctx = - TransitionEvaluationContext::new_prover(frame.as_row_frame(), &no_e, &no_e, &offset_e); - let mut base = vec![FE::zero(); n]; - let mut ext = vec![FieldElement::::zero(); n]; - let mut folder = ProverEvalFolder::new(&ctx, &mut base, &mut ext); - HintConstraints.eval(&mut folder); - base -} - -fn op(timestamp: u64, out_addr: u64) -> HintOperation { - HintOperation { - timestamp, - out_addr, - out_bytes: std::array::from_fn(|i| i as u8), - hint_id: 0, - in_addr: 0x3000, - } -} - -#[test] -fn constraint_set_count() { - assert_eq!(HintConstraints.meta().len(), 1); -} - -/// Every constraint holds on a generated trace — real rows (`mu = 1`) and the -/// all-zero padding rows (`mu = 0`) alike. -#[test] -fn constraints_hold_on_generated_trace() { - let trace = generate_hint_trace(&[op(4, 0x1000), op(8, 0x2000)]); - for row in 0..trace.num_rows() { - let main: Vec = (0..cols::NUM_COLUMNS) - .map(|c| *trace.main_table.get(row, c)) - .collect(); - for (i, v) in eval_main_row(main).iter().enumerate() { - assert_eq!(*v, FE::zero(), "constraint {i} must hold at row {row}"); - } - } -} - -/// `IS_BIT(mu)` rejects a row whose multiplicity is not a bit. -/// -/// The `Ecall` bus does not establish this on its own: its tuple carries a -/// per-instruction timestamp, so LogUp pins the *sum* of `mu` over the rows sharing a -/// tuple, which a witness can satisfy by spreading `mu` across rows with integer -/// weights summing to 1 (the real exploit uses a `+1`/`-1` pair, not a fractional -/// split; MEMW does not catch it — it only sees the legal `+1`, the `-1` cancelling an -/// honest STORE). This constraint rejects any non-boolean `mu` locally. The test below -/// tampers with a fractional `1/2`, which `IS_BIT` also rejects. -#[test] -fn is_bit_mu_rejects_non_boolean_multiplicity() { - let trace = generate_hint_trace(&[op(4, 0x1000)]); - let mut main: Vec = (0..cols::NUM_COLUMNS) - .map(|c| *trace.main_table.get(0, c)) - .collect(); - assert_eq!(main[cols::MU], FE::one(), "row 0 must be a real hint row"); - - // A halved multiplicity: 1/2 + 1/2 across two rows keeps the Ecall bus balanced. - let half = (FE::one() / (FE::one() + FE::one())).expect("2 is invertible"); - main[cols::MU] = half; - assert_ne!( - eval_main_row(main.clone())[0], - FE::zero(), - "IS_BIT(mu) must reject a fractional multiplicity" - ); - - // And any other non-bit value. - main[cols::MU] = FE::from(2u64); - assert_ne!( - eval_main_row(main)[0], - FE::zero(), - "IS_BIT(mu) must reject mu = 2" - ); -} - -/// The lhs column of an ALU `LT` sender, and the constant it is compared against. -fn alu_lt_senders() -> Vec<(usize, u64)> { - let id: u64 = BusId::Alu.into(); - bus_interactions() - .iter() - .filter(|i| i.is_sender && i.bus_id == id) - .map(|i| { - let lhs = match &i.values[0] { - BusValue::Packed { start_column, .. } => *start_column, - BusValue::Linear(_) => panic!("LT lhs must be a column, not a constant"), - }; - let bound = match &i.values[2] { - BusValue::Linear(terms) => match terms.as_slice() { - [LinearTerm::Constant(c)] => *c as u64, - _ => panic!("LT rhs must be a single constant"), - }, - BusValue::Packed { .. } => panic!("LT rhs must be a constant"), - }; - (lhs, bound) - }) - .collect() -} - -/// Both address low limbs are range-checked, not just `in_addr`. -/// -/// `out_addr` is on the memory bus, which is why it originally had no LT sender — but the -/// bus bounds it only to `2^32 - 25` (the largest write base is `out_addr_lo + 24`, and -/// MEMW's carry columns resolve the bytes past it), while the executor rejects anything -/// above `2^32 - 32`. Without this sender the AIR accepted the seven-value window in -/// [`addr_limb_bound_rejects_every_operand_the_executor_rejects`]. -#[test] -fn alu_lt_senders_range_check_selector_and_both_address_limbs() { - let senders = alu_lt_senders(); - assert_eq!(senders.len(), 3, "selector + in_addr + out_addr"); - - for col in [cols::ADDR_IN_0, cols::ADDR_OUT_0] { - let bound = senders - .iter() - .find_map(|(lhs, bound)| (*lhs == col).then_some(*bound)) - .unwrap_or_else(|| panic!("column {col} must have an ALU LT range-check")); - assert_eq!( - bound, HINT_ADDR_LIMB_BOUND, - "column {col} must be checked against the executor's bound" - ); - } -} - -/// The bound accepts exactly the operands `addr_limb_ok(addr, 31)` accepts. -/// -/// The seven values in `2^32-31 ..= 2^32-25` are the regression: the executor rejects -/// them with `HintAddressOverflow`, and before the `out_addr` sender existed the AIR -/// accepted them for the output address — a provable hint call the VM halts on. -#[test] -fn addr_limb_bound_rejects_every_operand_the_executor_rejects() { - // `addr_limb_ok(addr, 31)`: the 32-byte range must fit under 2^32. - let executor_accepts = |limb: u64| limb + 31 < (1 << 32); - // The AIR accepts iff the LT range-check passes. - let air_accepts = |limb: u64| limb < HINT_ADDR_LIMB_BOUND; - - for limb in (1u64 << 32) - 40..1u64 << 32 { - assert_eq!( - air_accepts(limb), - executor_accepts(limb), - "AIR and executor disagree on out_addr low limb {limb:#x}" - ); - } - - // The window that used to verify while the executor halted on it. - for limb in (1u64 << 32) - 31..=(1u64 << 32) - 25 { - assert!(!air_accepts(limb), "{limb:#x} must be rejected"); - } - // And the largest operand that must still run. - assert!(air_accepts((1 << 32) - 32)); -} diff --git a/prover/src/tests/mod.rs b/prover/src/tests/mod.rs index 9288cf2ac..2730a9d98 100644 --- a/prover/src/tests/mod.rs +++ b/prover/src/tests/mod.rs @@ -47,8 +47,6 @@ pub mod ecsm_tests; #[cfg(test)] pub mod eq_tests; #[cfg(test)] -pub mod hint_tests; -#[cfg(test)] pub mod ir_stats_dump; #[cfg(test)] pub mod keccak_rnd_tests; diff --git a/prover/src/tests/ood_window_ir_tests.rs b/prover/src/tests/ood_window_ir_tests.rs index 29d224627..b4ff5766c 100644 --- a/prover/src/tests/ood_window_ir_tests.rs +++ b/prover/src/tests/ood_window_ir_tests.rs @@ -114,5 +114,4 @@ fn all_table_windows_match_captured_ir() { assert_ood_window_matches_ir(&create_keccak_rc_air(&opts), true, "KECCAK_RC"); assert_ood_window_matches_ir(&create_ecsm_air(&opts), true, "ECSM"); assert_ood_window_matches_ir(&create_ecdas_air(&opts), true, "ECDAS"); - assert_ood_window_matches_ir(&create_hint_air(&opts), true, "HINT"); } diff --git a/prover/src/tests/page_offset_forgery_poc.rs b/prover/src/tests/page_offset_forgery_poc.rs index 5e2e24d78..492d5271b 100644 --- a/prover/src/tests/page_offset_forgery_poc.rs +++ b/prover/src/tests/page_offset_forgery_poc.rs @@ -147,7 +147,7 @@ fn craft_proof( // Execution logs come from whatever `run_elf` is. let run_program = Elf::load(run_elf).expect("run ELF load"); let executor = - Executor::new(&run_program, private_inputs.to_vec()).expect("executor construction"); + Executor::new(&run_program, private_inputs.to_vec(), &[]).expect("executor construction"); let result = executor.run().expect("run"); let max_rows = MaxRowsConfig::default(); @@ -156,6 +156,7 @@ fn craft_proof( &result.logs, &max_rows, private_inputs, + &[], #[cfg(feature = "disk-spill")] stark::storage_mode::StorageMode::Ram, ) @@ -541,7 +542,7 @@ fn poc_real_ethrex_inputs_produce_private_input_pages() { let Ok(bytes) = std::fs::read(&path) else { continue; // fixture not present in this checkout }; - let pages = private_input_page_count(&bytes); + let pages = private_input_page_count(&bytes, &[]); println!( "{name}: {} bytes -> {pages} private-input page(s) = {} free-OFFSET rows", bytes.len(), @@ -633,7 +634,7 @@ fn craft_proof_with_duplicate_page( let options = opts(); let program = Elf::load(honest_elf).expect("honest ELF load"); let run_program = Elf::load(run_elf).expect("run ELF load"); - let executor = Executor::new(&run_program, vec![]).expect("executor construction"); + let executor = Executor::new(&run_program, vec![], &[]).expect("executor construction"); let result = executor.run().expect("run"); let max_rows = MaxRowsConfig::default(); @@ -642,6 +643,7 @@ fn craft_proof_with_duplicate_page( &result.logs, &max_rows, &[], + &[], #[cfg(feature = "disk-spill")] stark::storage_mode::StorageMode::Ram, ) diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index bbc8d2c63..1b80d7b63 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -105,13 +105,19 @@ fn prove_and_verify_vm_minimal(elf: &Elf, traces: &mut Traces) -> bool { /// /// Same unsoundness caveats as [`Traces::from_elf_and_logs_minimal`]. The full /// preprocessed bitwise path is covered by `test_prove_elfs_all_instructions_64_full`. -fn prove_vm_minimal(elf_bytes: &[u8], private_inputs: &[u8], max_rows: &MaxRowsConfig) -> VmProof { +fn prove_vm_minimal( + elf_bytes: &[u8], + private_inputs: &[u8], + hints: &[[u8; 32]], + max_rows: &MaxRowsConfig, +) -> VmProof { let proof_options = ProofOptions::default_test_options(); let elf = Elf::load(elf_bytes).expect("ELF load"); - let executor = Executor::new(&elf, private_inputs.to_vec()).expect("executor"); + let executor = Executor::new(&elf, private_inputs.to_vec(), hints).expect("executor"); let result = executor.run().expect("execution"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, max_rows, private_inputs).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, max_rows, private_inputs, hints) + .unwrap(); let table_counts = traces.table_counts(); let airs = VmAirs::new( &elf, @@ -274,7 +280,7 @@ fn test_prove_elfs_sub_fast() { let (elf, logs, _instructions) = run_asm_elf("sub"); // Use from_elf_and_logs_minimal to get PAGE and REGISTER tables for Memory bus let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), @@ -618,7 +624,7 @@ fn test_prove_elfs_sign_ext_edge_cases_8() { fn test_prove_elfs_misalign_lh() { let (elf, logs, _instructions) = run_asm_elf("misalign_lh"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_lh failed" @@ -629,7 +635,7 @@ fn test_prove_elfs_misalign_lh() { fn test_prove_elfs_misalign_lhu() { let (elf, logs, _instructions) = run_asm_elf("misalign_lhu"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_lhu failed" @@ -640,7 +646,7 @@ fn test_prove_elfs_misalign_lhu() { fn test_prove_elfs_misalign_lw() { let (elf, logs, _instructions) = run_asm_elf("misalign_lw"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_lw failed" @@ -651,7 +657,7 @@ fn test_prove_elfs_misalign_lw() { fn test_prove_elfs_misalign_lwu() { let (elf, logs, _instructions) = run_asm_elf("misalign_lwu"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_lwu failed" @@ -662,7 +668,7 @@ fn test_prove_elfs_misalign_lwu() { fn test_prove_elfs_misalign_ld() { let (elf, logs, _instructions) = run_asm_elf("misalign_ld"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_ld failed" @@ -673,7 +679,7 @@ fn test_prove_elfs_misalign_ld() { fn test_prove_elfs_misalign_sh() { let (elf, logs, _instructions) = run_asm_elf("misalign_sh"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_sh failed" @@ -684,7 +690,7 @@ fn test_prove_elfs_misalign_sh() { fn test_prove_elfs_misalign_sw() { let (elf, logs, _instructions) = run_asm_elf("misalign_sw"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_sw failed" @@ -695,7 +701,7 @@ fn test_prove_elfs_misalign_sw() { fn test_prove_elfs_misalign_sd() { let (elf, logs, _instructions) = run_asm_elf("misalign_sd"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "misalign_sd failed" @@ -925,7 +931,7 @@ fn test_prove_elfs_test_xor_8() { fn test_prove_elfs_test_lb_lh_8() { let (elf, logs, _instructions) = run_asm_elf("test_lb_lh_8"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "test_lb_lh_8 failed" @@ -936,7 +942,7 @@ fn test_prove_elfs_test_lb_lh_8() { fn test_prove_elfs_test_sb_sh_8() { let (elf, logs, _instructions) = run_asm_elf("test_sb_sh_8"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( !traces.memws.is_empty(), "test_sb_sh_8 should produce MEMW rows for byte/halfword memory accesses" @@ -954,7 +960,7 @@ fn test_prove_elfs_test_sb_sh_8() { fn test_prove_elfs_lw_sw() { let (elf, logs, _instructions) = run_asm_elf("lw_sw"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( !traces.memw_aligneds.is_empty(), "lw_sw should produce MEMW_A rows for aligned word accesses" @@ -975,7 +981,7 @@ fn test_prove_elfs_lw_sw() { fn test_prove_elfs_test_memw_split_ts() { let (elf, logs, _instructions) = run_asm_elf("test_memw_split_ts"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( !traces.memws.is_empty(), "test_memw_split_ts should produce MEMW rows (split old_timestamps from sb+sb+lh)" @@ -1015,7 +1021,7 @@ fn test_prove_elfs_all_branches_16() { fn test_prove_elfs_all_loadstore_32() { let (elf, logs, _instructions) = run_asm_elf("all_loadstore_32"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "all_loadstore_32 failed" @@ -1056,7 +1062,7 @@ fn test_prove_elfs_keccak() { // Must use from_elf_and_logs (not from_logs_minimal) because keccak accesses // RAM (stack memory), which requires PAGE tables for Memory bus balance. let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), @@ -1071,7 +1077,7 @@ fn test_prove_elfs_keccak_multi_call() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_keccak_multi"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // The guest initializes lane[i] = i + 1 and applies keccak-f[1600] three times. @@ -1092,7 +1098,7 @@ fn test_prove_elfs_keccak_multi_call() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); assert_eq!( traces.public_output_bytes, result.return_values.memory_values @@ -1111,7 +1117,7 @@ fn test_prove_elfs_ecsm() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // The guest computes 5·G and commits the 32-byte x-coordinate; cross-check it against @@ -1132,7 +1138,7 @@ fn test_prove_elfs_ecsm() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "ECSM prove/verify failed" @@ -1146,7 +1152,7 @@ fn test_prove_elfs_ecsm_multi() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm_multi"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // Gx little-endian. @@ -1171,7 +1177,7 @@ fn test_prove_elfs_ecsm_multi() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "ECSM multi-call prove/verify failed" @@ -1191,7 +1197,7 @@ fn test_prove_ecsm_rust_guest() { let elf_bytes = std::fs::read(workspace_root.join("executor/program_artifacts/rust/ecsm.elf")) .expect("ecsm.elf not found — run `make compile-programs-rust`"); - let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, &[], &[], &Default::default()); assert!( verify_vm_minimal(&proof, &elf_bytes), "ecsm rust guest should verify" @@ -1212,15 +1218,14 @@ fn test_prove_ecsm_rust_guest() { ); } -/// End-to-end prove→verify for the non-constraining `Hint` ecall: the minimal Rust -/// guest does one `hint` call (secp256k1 base-field inverse of 3) and commits the result. -/// This exercises the whole HINT table bus surface (Ecall receive, the x10/x11/x12 -/// register reads, the two ALU `LT` operand range-checks, the four 8-byte output MEMW -/// writes and the output byte range-checks) end-to-end through prove→verify, de-risking -/// the bus balance before scaling to real consumers. The committed output must equal -/// the value the executor's `compute_hint` produced (= 3^{-1} mod p). +/// End-to-end prove→verify for the hint arena: three hint values (one per +/// selector — secp256k1 base-field inverse of 3, scalar inverse of 5, square +/// root of 4) supplied as private-input hint-arena slots. The guest reads them +/// with ordinary aligned loads (MEMR reads chained to the private-input pages) — +/// the hint mechanism is just more private-input bytes. Committed output = XOR +/// of the three slots. #[test] -fn test_prove_hint_min_rust_guest() { +fn test_prove_hint_arena_rust_guest() { let _ = env_logger::builder().is_test(true).try_init(); let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -1228,316 +1233,44 @@ fn test_prove_hint_min_rust_guest() { .expect("workspace root") .to_path_buf(); let elf_bytes = - std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_min.elf")) - .expect("hint_min.elf not found — run `make compile-programs-rust`"); + std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_arena.elf")) + .expect("hint_arena.elf not found — run `make compile-programs-rust`"); - let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); - assert!( - verify_vm_minimal(&proof, &elf_bytes), - "hint_min rust guest should verify" - ); - - // Committed output must equal the hinted value (field inverse of 3, 32-byte BE). - let mut input = [0u8; 32]; - input[31] = 3; - let expected = - executor::vm::instruction::execution::compute_hint(0 /* HINT_FIELD_INV */, &input); - assert_eq!(proof.public_output, expected.to_vec()); -} - -/// Multi-hint: three `hint` ecalls, one per selector, each result read back with -/// ordinary `LOAD`s. Complements `test_prove_hint_min_rust_guest` by proving the -/// paths the ethrex consumer relies on that a single-call guest doesn't: **multiple -/// real HINT rows** (padded), **all three selectors** (so the AIR's `selector < 3` -/// range-check is exercised at every accepted value, not only at 0) and **read-back -/// via normal LOAD** (MEMW reads chaining to the HINT writes). Committed output = -/// XOR of the three hinted values. -#[test] -fn test_prove_hint_multi_rust_guest() { - let _ = env_logger::builder().is_test(true).try_init(); - - let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("workspace root") - .to_path_buf(); - let elf_bytes = - std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_multi.elf")) - .expect("hint_multi.elf not found — run `make compile-programs-rust`"); - - let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); - assert!( - verify_vm_minimal(&proof, &elf_bytes), - "hint_multi rust guest should verify" - ); - - // Expected = XOR of inv(3) mod p, inv(5) mod n and sqrt(4) mod p (32-byte BE), - // matching the guest's one-call-per-selector loop. + // The host computes the three hints up front (the known-beforehand case) and + // passes them as arena slots, in the guest's request order. use executor::vm::instruction::execution::{ HINT_FIELD_INV, HINT_FIELD_SQRT, HINT_SCALAR_INV, compute_hint, }; - let mut expected = [0u8; 32]; - for (hint_id, seed) in [ + let hints: Vec<[u8; 32]> = [ (HINT_FIELD_INV, 3u8), (HINT_SCALAR_INV, 5u8), (HINT_FIELD_SQRT, 4u8), - ] { + ] + .into_iter() + .map(|(hint_id, seed)| { let mut input = [0u8; 32]; input[31] = seed; - let out = compute_hint(hint_id, &input); - for i in 0..32 { - expected[i] ^= out[i]; - } - } - assert_eq!(proof.public_output, expected.to_vec()); -} - -/// Consistency: the verifier REJECTS a HINT row that disagrees with the -/// MEMW rows. -/// -/// The HINT table's `out_bytes` are unconstrained *by the table* — the point of a -/// non-constraining hint. Editing one output byte on the (single) real HINT row makes -/// the MEMW write it sends stop matching the write the MEMW table received (the honest -/// value `collect_hint_ops` derived), so the Memw LogUp bus unbalances and the proof -/// must fail to verify. -/// -/// What this covers is an *internally inconsistent* trace — the failure mode of a buggy -/// trace builder. It is **not** a forgery test: a prover that edits the HINT row and the -/// corresponding MEMW rows together satisfies every constraint, because nothing in the -/// AIR pins *which* value was hinted. That guarantee lives in the guest's verify -/// (`x·inv == 1`, `y² == x³+7`), which this minimal guest deliberately omits. What the -/// AIR does pin is *where* the value lands and that it is 32 bytes — see -/// `test_hint_binds_out_addr_to_x12` and `test_hint_range_checks_its_output_bytes`. -#[test] -fn test_prove_hint_min_inconsistent_output_rejected() { - use crate::tables::hint::cols as hint_cols; + compute_hint(hint_id, &input) + }) + .collect(); - let _ = env_logger::builder().is_test(true).try_init(); - - let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("workspace root") - .to_path_buf(); - let elf_bytes = - std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_min.elf")) - .expect("hint_min.elf not found — run `make compile-programs-rust`"); - let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = Executor::new(&elf, vec![]).expect("Failed to create executor"); - let result = executor.run().expect("Failed to run program"); - let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); - - // Forge the low byte of the output on the (single) real HINT row. - let orig = *traces.hint.main_table.get(0, hint_cols::out(0)); - let forged = orig + FieldElement::::one(); - traces.hint.main_table.set(0, hint_cols::out(0), forged); - - assert!( - !prove_and_verify_vm_minimal(&elf, &mut traces), - "Verifier must reject a forged hint output byte" - ); -} - -/// Load `hint_min` and build its minimal traces (for the operand-forgery tests below). -fn hint_min_traces() -> (Elf, Traces) { - let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("workspace root") - .to_path_buf(); - let elf_bytes = - std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_min.elf")) - .expect("hint_min.elf not found — run `make compile-programs-rust`"); - let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let result = Executor::new(&elf, vec![]) - .expect("Failed to create executor") - .run() - .expect("Failed to run program"); - let traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); - (elf, traces) -} - -/// Soundness: the verifier REJECTS a HINT row whose selector is out of range. -/// -/// The executor rejects `hint_id ∉ {0,1,2}` up front (`HintUnknownSelector`). The AIR -/// now matches that: it binds the selector to `x10` and range-checks it `< 3`, so a -/// witness cannot prove a hint the executor would reject. Before `a0` was bound this -/// forgery verified. Forcing the selector to 3 (one past the valid set) unbalances both -/// the `x10` register read and the `LT(selector, 3)` interaction. -#[test] -fn test_prove_hint_min_forged_selector_rejected() { - use crate::tables::hint::cols as hint_cols; - let (elf, mut traces) = hint_min_traces(); - traces.hint.main_table.set( - 0, - hint_cols::SEL_0, - FieldElement::::from(3u64), - ); + let proof = prove_vm_minimal(&elf_bytes, &[], &hints, &Default::default()); assert!( - !prove_and_verify_vm_minimal(&elf, &mut traces), - "Verifier must reject a hint with an out-of-range selector" - ); -} - -/// Soundness: the verifier REJECTS a HINT row whose input address would straddle the -/// 32-bit limb boundary — the executor rejects it (`HintAddressOverflow`), and the AIR -/// now binds `in_addr` to `x11` and range-checks its low limb `< 2^32 - 31`. Forcing -/// the low limb to `2^32 - 1` unbalances the `x11` read and the `LT` interaction. -#[test] -fn test_prove_hint_min_forged_input_address_rejected() { - use crate::tables::hint::cols as hint_cols; - let (elf, mut traces) = hint_min_traces(); - traces.hint.main_table.set( - 0, - hint_cols::ADDR_IN_0, - FieldElement::::from(0xFFFF_FFFFu64), - ); - assert!( - !prove_and_verify_vm_minimal(&elf, &mut traces), - "Verifier must reject a hint whose input range crosses the limb boundary" - ); -} - -/// Column a bus value reads, for the structural HINT tests below. -fn hint_bus_column(v: &stark::lookup::BusValue) -> Option { - match v { - stark::lookup::BusValue::Packed { start_column, .. } => Some(*start_column), - stark::lookup::BusValue::Linear(_) => None, - } -} - -/// Constant a bus value holds, for the structural HINT tests below. -fn hint_bus_constant(v: &stark::lookup::BusValue) -> Option { - match v { - stark::lookup::BusValue::Linear(terms) => match terms.as_slice() { - [stark::lookup::LinearTerm::Constant(c)] => Some(*c), - _ => None, - }, - stark::lookup::BusValue::Packed { .. } => None, - } -} - -/// Soundness: the HINT table must bind its output address to `x12` (the ecall's `a2`). -/// -/// The four output writes take their base from `ADDR_OUT_0`, an ordinary column in a -/// table with no algebraic constraints, so the register read asserted here is the only -/// thing pinning that column to the register the CPU actually held. Without it the -/// witness chooses *where* the 32 hinted bytes land — an arbitrary memory write, which -/// is a strictly larger hole than the unconstrained value the table is designed around. -/// -/// Asserted structurally rather than by tampering: editing `ADDR_OUT_0` in a trace also -/// unbalances the honest MEMW rows, so a tamper test passes either way and would not -/// notice this interaction being dropped. -#[test] -fn test_hint_binds_out_addr_to_x12() { - use crate::tables::hint::{bus_interactions, cols as hint_cols}; - use crate::tables::types::BusId; - use stark::lookup::Multiplicity; - - let memw_id = u64::from(BusId::Memw); - let reads: Vec<_> = bus_interactions() - .into_iter() - .filter(|i| i.bus_id == memw_id && i.is_sender && i.values.len() == 24) - .collect(); - assert_eq!( - reads.len(), - 3, - "HINT must send three MEMW register reads (a0 → x10, a1 → x11, a2 → x12)" - ); - // The out_addr binding is the x12 read (base address 2*12); the a0/a1 reads bind - // the selector and input address, checked by the range-check interactions. - let out_read = reads - .iter() - .find(|r| hint_bus_constant(&r.values[9]) == Some(2 * 12)) - .expect("HINT must send a MEMW register read for x12 (out_addr)"); - let v = &out_read.values; - - // CO24 read layout: old[8], is_register, base_lo, base_hi, value[8], ts_lo, ts_hi, - // w2, w4, w8. - assert_eq!(hint_bus_constant(&v[8]), Some(1), "is_register must be 1"); - assert_eq!( - hint_bus_constant(&v[9]), - Some(2 * 12), - "register address must be x12 (the ecall's a2)" - ); - assert_eq!(hint_bus_constant(&v[10]), Some(0), "address hi must be 0"); - assert_eq!( - hint_bus_constant(&v[21]), - Some(1), - "w2 must be 1 for a 2-word register access" - ); - for (slot, col) in [(0, hint_cols::ADDR_OUT_0), (1, hint_cols::ADDR_OUT_1)] { - assert_eq!( - hint_bus_column(&v[slot]), - Some(col), - "old[{slot}] must carry out_addr" - ); - assert_eq!( - hint_bus_column(&v[11 + slot]), - Some(col), - "value[{slot}] must carry out_addr (a read leaves the register unchanged)" - ); - } - // The read must happen at THE ecall's timestamp (ts_lo/ts_hi = slots 19/20). A - // register read bound to x12 but at some other timestamp would pin out_addr to - // whatever x12 held then, not at the ecall — the writes below all use the same - // TIMESTAMP columns, so the binding is only meaningful if it reads x12 at T. - assert_eq!( - hint_bus_column(&v[19]), - Some(hint_cols::TIMESTAMP_0), - "ts_lo must be the ecall timestamp (the read must occur at T)" - ); - assert_eq!( - hint_bus_column(&v[20]), - Some(hint_cols::TIMESTAMP_1), - "ts_hi must be the ecall timestamp (the read must occur at T)" - ); - assert!( - matches!(out_read.multiplicity, Multiplicity::Column(c) if c == hint_cols::MU), - "the register read must be gated by mu, like every other HINT interaction" + verify_vm_minimal(&proof, &elf_bytes), + "hint_arena rust guest should verify" ); -} -/// Soundness: the HINT table must range-check all 32 output cells as bytes. -/// -/// The cells are free columns that enter memory as MEMW write values, and MEMW -/// range-checks nothing it receives — every table that writes fresh values into memory -/// (STORE, KECCAK, ECSM, PAGE) checks its own cells for that reason. The hinted value is -/// allowed to be wrong; it is not allowed to be a field element outside `[0, 256)`, or -/// the witness can smuggle non-bytes into memory and break the byte decomposition that -/// loads and the ALU rely on. -#[test] -fn test_hint_range_checks_its_output_bytes() { - use crate::tables::hint::{bus_interactions, cols as hint_cols}; - use crate::tables::types::BusId; - use stark::lookup::Multiplicity; - - let are_bytes_id = u64::from(BusId::AreBytes); - let checks: Vec<_> = bus_interactions() - .into_iter() - .filter(|i| i.bus_id == are_bytes_id) - .collect(); - assert_eq!(checks.len(), 16, "32 output cells, paired two per lookup"); + // Empty main input + 3 slots: the region spans align8(4) + 8 + 96 = 112 bytes, + // one private-input page. + assert_eq!(proof.num_private_input_pages, 1); - let mut covered = std::collections::BTreeSet::new(); - for check in &checks { - assert!(check.is_sender, "range checks are sends; BITWISE receives"); - assert_eq!(check.values.len(), 2, "ARE_BYTES takes exactly two values"); - assert!( - matches!(check.multiplicity, Multiplicity::Column(c) if c == hint_cols::MU), - "range checks must be gated by mu, or padding rows unbalance BITWISE" - ); - for v in &check.values { - covered - .insert(hint_bus_column(v).expect("a range check must reference an output column")); + let mut expected = [0u8; 32]; + for hint in &hints { + for i in 0..32 { + expected[i] ^= hint[i]; } } - - // 16 lookups × 2 slots = 32 slots; 32 distinct columns means each cell exactly once. - let expected: std::collections::BTreeSet = (0..32).map(hint_cols::out).collect(); - assert_eq!( - covered, expected, - "every output cell must be range-checked exactly once" - ); + assert_eq!(proof.public_output, expected.to_vec()); } /// Soundness: the verifier REJECTS a forged ECSM result. @@ -1556,10 +1289,10 @@ fn test_prove_elfs_ecsm_forged_result_rejected() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); // Forge the low byte of xR on the (single) real ECSM row. let orig = *traces.ecsm.main_table.get(0, ecsm_cols::xr(0)); @@ -1584,10 +1317,10 @@ fn test_prove_elfs_ecsm_forged_ecdas_mu_rejected() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); // Row 0 is a real ECDAS step (µ=1); forge µ to a non-boolean value. traces.ecdas.main_table.set( @@ -1620,10 +1353,10 @@ fn test_prove_elfs_keccak_unaligned_state_addr() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_keccak_multi"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); // Tamper the first real keccak row: replace addr(1) (a byte cell) with a // value outside [0, 256). The new ARE_BYTES bus sender will emit this @@ -1646,7 +1379,7 @@ fn test_prove_elfs_test_commit_4() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_commit_4"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // Verify public output matches the committed bytes [0xAA, 0xBB, 0xCC, 0xDD] @@ -1657,7 +1390,7 @@ fn test_prove_elfs_test_commit_4() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); assert_eq!( traces.public_output_bytes, result.return_values.memory_values @@ -1680,10 +1413,10 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { let proof_options = ProofOptions::default_test_options(); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); // Prover uses correct page configs let table_counts = traces.table_counts(); @@ -2080,6 +1813,7 @@ fn test_debug_memory_tokens_sb_sh() { &logs, &Default::default(), &[], + &[], #[cfg(feature = "disk-spill")] stark::storage_mode::StorageMode::Ram, ) @@ -2415,7 +2149,7 @@ fn test_debug_memory_tokens_sb_sh() { fn test_deep_stack_passes() { let (elf, logs, _instructions) = run_asm_elf("deep_stack"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), @@ -2434,10 +2168,10 @@ fn test_deep_stack_runtime_pages_roundtrip() { let proof_options = ProofOptions::default_test_options(); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -2516,10 +2250,10 @@ fn test_deep_stack_missing_pages_rejected() { let proof_options = ProofOptions::default_test_options(); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); // Prover uses correct page configs (auto-detected from MemoryState) let table_counts = traces.table_counts(); @@ -2592,7 +2326,7 @@ fn test_deep_stack_missing_pages_rejected() { fn test_heap_alloc_passes() { let (elf, logs, _instructions) = run_asm_elf("heap_alloc"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &logs, &Default::default(), &[], &[]).unwrap(); // Verify runtime_page_ranges includes the heap page let ranges = traces.runtime_page_ranges(); @@ -2618,10 +2352,10 @@ fn test_heap_alloc_runtime_pages_roundtrip() { let proof_options = ProofOptions::default_test_options(); let executor = - executor::vm::execution::Executor::new(&elf, vec![]).expect("Failed to create executor"); + executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -2878,7 +2612,7 @@ fn test_small_max_rows_splits_tables() { let elf_bytes = crate::test_utils::asm_elf_bytes("all_instructions_64"); let max_rows = crate::tables::MaxRowsConfig::small(); - let vm_proof = prove_vm_minimal(&elf_bytes, &[], &max_rows); + let vm_proof = prove_vm_minimal(&elf_bytes, &[], &[], &max_rows); // With 2^5 max rows and 64+ instructions, tables should have multiple chunks. assert!( @@ -2956,7 +2690,7 @@ fn test_verify_rejects_inflated_table_counts() { #[test] fn test_prove_wsuffix_64bit() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_wsuffix_64bit"); - let vm_proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + let vm_proof = prove_vm_minimal(&elf_bytes, &[], &[], &Default::default()); assert!( verify_vm_minimal(&vm_proof, &elf_bytes), "W-suffix 64-bit register test should verify" @@ -2977,7 +2711,7 @@ fn test_prove_allocator_minimal_reproducer() { let elf_bytes = std::fs::read(workspace_root.join("executor/program_artifacts/rust/allocator.elf")) .expect("allocator.elf not found — run `make compile-programs-rust`"); - let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, &[], &[], &Default::default()); assert!( verify_vm_minimal(&proof, &elf_bytes), "allocator.elf should verify" @@ -2996,7 +2730,7 @@ fn test_pure_commit_rust() { let elf_bytes = std::fs::read(workspace_root.join("executor/program_artifacts/rust/pure_commit.elf")) .expect("pure_commit.elf not found — run `make compile-programs-rust`"); - let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, &[], &[], &Default::default()); assert!( verify_vm_minimal(&proof, &elf_bytes), "pure_commit.elf should verify" @@ -3009,7 +2743,7 @@ fn test_pure_commit_rust() { fn test_prove_with_input_empty() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); let result = - crate::prove_with_inputs(&elf_bytes, &[]).expect("prove_with_inputs should succeed on sub"); + crate::prove_with_inputs(&elf_bytes, &[], &[]).expect("prove_with_inputs should succeed on sub"); assert!( crate::verify(&result, &elf_bytes).expect("verify should not error"), "prove_with_inputs(empty) proof should verify" @@ -3021,7 +2755,7 @@ fn test_prove_with_input_empty() { fn test_prove_private_input_xpage() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let proof = prove_vm_minimal(&elf_bytes, &input, &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, &input, &[], &Default::default()); assert!(verify_vm_minimal(&proof, &elf_bytes), "proof should verify"); assert_eq!(proof.public_output, input[4..12].to_vec()); } @@ -3034,7 +2768,7 @@ fn test_prove_private_input_different_values() { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, ]; - let proof = prove_vm_minimal(&elf_bytes, &input, &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, &input, &[], &Default::default()); assert!(verify_vm_minimal(&proof, &elf_bytes), "proof should verify"); assert_eq!(proof.public_output, input[4..12].to_vec()); } @@ -3053,7 +2787,7 @@ fn test_prove_ef_io_demo_concatenates() { std::fs::read(workspace_root.join("executor/program_artifacts/rust/ef_io_demo.elf")) .expect("ef_io_demo.elf not found — run `make compile-programs-rust`"); let input: &[u8] = b"hello world!"; - let proof = crate::prove_with_inputs(&elf_bytes, input).expect("prove should succeed"); + let proof = crate::prove_with_inputs(&elf_bytes, input, &[]).expect("prove should succeed"); assert!( crate::verify(&proof, &elf_bytes).expect("verify should not error"), "ef_io_demo should verify" @@ -3075,7 +2809,7 @@ fn test_prove_commit_sum() { std::fs::read(workspace_root.join("executor/program_artifacts/rust/commit_sum.elf")) .expect("commit_sum.elf not found — run `make compile-programs-rust`"); let input = &[3u8, 5u8]; - let proof = prove_vm_minimal(&elf_bytes, input, &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, input, &[], &Default::default()); assert!( verify_vm_minimal(&proof, &elf_bytes), "commit_sum should verify" @@ -3096,7 +2830,7 @@ fn test_prove_ethrex_empty_block() { .expect("need ethrex.elf"); let input = std::fs::read(workspace_root.join("executor/tests/ethrex_empty_block.bin")).unwrap(); - let proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove"); + let proof = crate::prove_with_inputs(&elf_bytes, &input, &[]).expect("prove"); assert!( crate::verify(&proof, &elf_bytes).expect("verify"), "ethrex empty block should verify" @@ -3115,7 +2849,7 @@ fn test_prove_ethrex_empty_block() { fn test_verify_rejects_tampered_num_private_input_pages_zero() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); + let vm_proof = crate::prove_with_inputs(&elf_bytes, &input, &[]).expect("prove should succeed"); // Baseline: untampered proof must verify. assert!( @@ -3146,7 +2880,7 @@ fn test_verify_rejects_tampered_num_private_input_pages_zero() { fn test_verify_rejects_inflated_num_private_input_pages() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); + let vm_proof = crate::prove_with_inputs(&elf_bytes, &input, &[]).expect("prove should succeed"); assert_eq!( vm_proof.num_private_input_pages, 1, @@ -3171,7 +2905,7 @@ fn test_verify_rejects_inflated_num_private_input_pages() { fn test_verify_rejects_num_private_input_pages_exceeds_max() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); + let vm_proof = crate::prove_with_inputs(&elf_bytes, &input, &[]).expect("prove should succeed"); let tampered = crate::VmProof { num_private_input_pages: crate::tables::page::max_private_input_pages() + 1, @@ -3190,7 +2924,7 @@ fn test_verify_rejects_num_private_input_pages_exceeds_max() { fn test_verify_rejects_private_input_with_tampered_public_output() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); + let vm_proof = crate::prove_with_inputs(&elf_bytes, &input, &[]).expect("prove should succeed"); assert!( crate::verify(&vm_proof, &elf_bytes).expect("verify should not error"), @@ -3219,7 +2953,7 @@ fn test_verify_rejects_private_input_with_tampered_public_output() { fn test_proof_does_not_contain_private_input_field() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0xA0u8..0xB0).collect(); - let vm_proof = crate::prove_with_inputs(&elf_bytes, &input).expect("prove should succeed"); + let vm_proof = crate::prove_with_inputs(&elf_bytes, &input, &[]).expect("prove should succeed"); // The VmProof struct should only contain num_private_input_pages (a count), // not the actual bytes. Verify the proof's public fields don't contain them. @@ -3242,7 +2976,7 @@ fn test_proof_does_not_contain_private_input_field() { #[test] fn test_addiw_neg_immediate() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_addiw_neg"); - let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + let proof = prove_vm_minimal(&elf_bytes, &[], &[], &Default::default()); assert!( verify_vm_minimal(&proof, &elf_bytes), "addiw with negative immediate should verify" @@ -3255,7 +2989,7 @@ fn test_addiw_neg_immediate() { #[test] fn test_count_elements_nonzero() { let elf_bytes = crate::test_utils::asm_elf_bytes("addi_one"); - let (main, aux) = crate::count_elements(&elf_bytes, &[]).expect("count_elements failed"); + let (main, aux) = crate::count_elements(&elf_bytes, &[], &[]).expect("count_elements failed"); assert!( main > 0, "total_field_elements should be nonzero (got {main})" @@ -3283,7 +3017,7 @@ fn test_prove_first_epoch_without_halt() { // intermediate epoch (4 cycles → no CPU padding rows) with the program // continuing past it. let epoch_size = 4; - let epochs = Executor::new(&elf, vec![]) + let epochs = Executor::new(&elf, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); @@ -3291,7 +3025,7 @@ fn test_prove_first_epoch_without_halt() { // Epoch 0's starting memory/registers are the program-start image; it does // not halt (is_final=false). - let image = build_initial_image(&elf, &[]); + let image = build_initial_image(&elf, &[], &[]); let register_init = crate::tables::register::register_init_from_entry_point(elf.entry_point); let mut traces = Traces::from_image_and_logs( &elf, @@ -3300,6 +3034,7 @@ fn test_prove_first_epoch_without_halt() { &epochs[0].logs, &MaxRowsConfig::default(), &[], + &[], false, false, #[cfg(feature = "disk-spill")] @@ -3370,7 +3105,7 @@ fn test_prove_second_epoch_from_snapshot() { // arith_8 is ~10 cycles; epoch_size 4 (power of two) yields epochs 4/4/2, so // epoch 1 is intermediate (4 cycles → no CPU padding rows). let epoch_size = 4; - let epochs = Executor::new(&elf, vec![]) + let epochs = Executor::new(&elf, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); @@ -3388,6 +3123,7 @@ fn test_prove_second_epoch_from_snapshot() { &epochs[1].logs, &MaxRowsConfig::default(), &[], + &[], false, false, #[cfg(feature = "disk-spill")] @@ -3463,13 +3199,13 @@ fn test_epoch_proof_commits_l2g() { // Power-of-two epoch size: all_loadstore_32 is ~34 cycles, so epoch_size 8 // makes epoch 0 an intermediate epoch with no CPU padding rows. let epoch_size = 8; - let epochs = Executor::new(&elf, vec![]) + let epochs = Executor::new(&elf, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); assert!(epochs.len() >= 2); - let image = build_initial_image(&elf, &[]); + let image = build_initial_image(&elf, &[], &[]); let register_init = register::register_init_from_entry_point(elf.entry_point); let mut traces = Traces::from_image_and_logs( &elf, @@ -3478,6 +3214,7 @@ fn test_epoch_proof_commits_l2g() { &epochs[0].logs, &MaxRowsConfig::default(), &[], + &[], false, false, #[cfg(feature = "disk-spill")] @@ -3595,13 +3332,13 @@ fn test_continuation_pipeline_end_to_end() { // Split execution into power-of-two epochs (all_loadstore_32 is ~34 cycles, so // epoch_size 8 gives intermediate epochs with no CPU padding rows). let epoch_size = 8; - let epochs = Executor::new(&elf, vec![]) + let epochs = Executor::new(&elf, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); assert!(epochs.len() >= 2); - let image0 = build_initial_image(&elf, &[]); + let image0 = build_initial_image(&elf, &[], &[]); let initial_memory: HashMap = image0.iter().map(|(&a, &v)| (a, v as u64)).collect(); // Pass 1: each epoch's starting state + the cells it touches. Epoch 0 starts @@ -3644,6 +3381,7 @@ fn test_continuation_pipeline_end_to_end() { &epoch.logs, &MaxRowsConfig::default(), &[], + &[], is_final, false, #[cfg(feature = "disk-spill")] @@ -3765,14 +3503,14 @@ fn test_epoch_memory_bus_with_l2g_bookend() { // Power-of-two epoch size: all_loadstore_32 is ~34 cycles, so epoch_size 8 // makes epoch 0 an intermediate epoch with no CPU padding rows. let epoch_size = 8; - let epochs = Executor::new(&elf, vec![]) + let epochs = Executor::new(&elf, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); assert!(epochs.len() >= 2); // Epoch 0 starts from the program image; build it with the L2G memory bookend. - let image = build_initial_image(&elf, &[]); + let image = build_initial_image(&elf, &[], &[]); let register_init = register::register_init_from_entry_point(elf.entry_point); let mut traces = Traces::from_image_and_logs( &elf, @@ -3781,6 +3519,7 @@ fn test_epoch_memory_bus_with_l2g_bookend() { &epochs[0].logs, &MaxRowsConfig::default(), &[], + &[], false, true, #[cfg(feature = "disk-spill")] diff --git a/prover/src/tests/recursion_smoke_test.rs b/prover/src/tests/recursion_smoke_test.rs index 90482a3a4..44bf8b420 100644 --- a/prover/src/tests/recursion_smoke_test.rs +++ b/prover/src/tests/recursion_smoke_test.rs @@ -48,6 +48,7 @@ fn prove_inner_and_encode_blob( let inner_proof = crate::prove_with_options_and_inputs( inner_elf, inner_input, + &[], opts, &crate::MaxRowsConfig::default(), ) @@ -76,7 +77,7 @@ fn execute_outer_and_commit(label: &str, recursion_elf_bytes: &[u8], blob: &[u8] eprintln!("[{label}] executing outer (recursion guest, in-VM verify, streaming) ..."); let program = Elf::load(recursion_elf_bytes).expect("load recursion elf"); - let mut executor = Executor::new(&program, blob.to_vec()).expect("executor new"); + let mut executor = Executor::new(&program, blob.to_vec(), &[]).expect("executor new"); let (total_cycles, exec_time) = drive_executor( &mut executor, @@ -196,7 +197,7 @@ fn setup_guest_run( preset.name() ); let executor = - executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed"); + executor::vm::execution::Executor::new(&program, blob, &[]).expect("Executor::new failed"); (guest_elf_bytes, program, executor, expected) } @@ -242,7 +243,7 @@ fn setup_block4_blowup4_guest_run() -> ( "recursion-cont-blowup4 ELF has entry_point=0 — build artifact is malformed", ); let executor = - executor::vm::execution::Executor::new(&program, blob).expect("Executor::new failed"); + executor::vm::execution::Executor::new(&program, blob, &[]).expect("Executor::new failed"); (guest_elf_bytes, program, executor, expected) } @@ -702,6 +703,7 @@ fn test_recursion_continuation_blob_decodes_and_verifies_on_host() { let bundle = crate::continuation::prove_continuation( &fib_elf_bytes, &inner_input, + &[], 4, &MIN_PROOF_OPTIONS, ) @@ -991,6 +993,7 @@ fn test_dump_recursion_input() { let bundle = crate::continuation::prove_continuation( &inner_elf_bytes, &inner_input, + &[], epoch_log2, &opts, ) diff --git a/prover/src/tests/recursion_soundness_gap_poc.rs b/prover/src/tests/recursion_soundness_gap_poc.rs index 73410ff62..5a54229d5 100644 --- a/prover/src/tests/recursion_soundness_gap_poc.rs +++ b/prover/src/tests/recursion_soundness_gap_poc.rs @@ -48,7 +48,7 @@ fn read_guest_elf(name: &str) -> Vec { /// The set of program-counter values fetched during a run of `elf_bytes`. fn executed_pcs(elf_bytes: &[u8]) -> HashSet { let elf = Elf::load(elf_bytes).expect("ELF load failed"); - let executor = Executor::new(&elf, vec![]).expect("executor new"); + let executor = Executor::new(&elf, vec![], &[]).expect("executor new"); let result = executor.run().expect("run failed"); result.logs.iter().map(|l| l.current_pc).collect() } @@ -132,7 +132,7 @@ fn custom_prove_with_statement_elf( opts: &stark::proof::options::ProofOptions, ) -> VmProof { let program = Elf::load(prove_elf).expect("prove ELF load failed"); - let executor = Executor::new(&program, vec![]).expect("executor new"); + let executor = Executor::new(&program, vec![], &[]).expect("executor new"); let result = executor.run().expect("run failed"); let max_rows = MaxRowsConfig::default(); @@ -141,6 +141,7 @@ fn custom_prove_with_statement_elf( &result.logs, &max_rows, &[], + &[], #[cfg(feature = "disk-spill")] stark::storage_mode::StorageMode::Ram, ) diff --git a/prover/src/tests/trace_builder_tests.rs b/prover/src/tests/trace_builder_tests.rs index 428fd4700..3acdd8bf7 100644 --- a/prover/src/tests/trace_builder_tests.rs +++ b/prover/src/tests/trace_builder_tests.rs @@ -837,7 +837,7 @@ fn test_from_image_and_logs_matches_from_elf_and_logs() { let elf_bytes = asm_elf_bytes("basic_program"); let program = Elf::load(&elf_bytes).unwrap(); - let logs = Executor::new(&program, vec![]).unwrap().run().unwrap().logs; + let logs = Executor::new(&program, vec![], &[]).unwrap().run().unwrap().logs; let max_rows = MaxRowsConfig::default(); let from_elf = Traces::from_elf_and_logs( @@ -845,12 +845,13 @@ fn test_from_image_and_logs_matches_from_elf_and_logs() { &logs, &max_rows, &[], + &[], #[cfg(feature = "disk-spill")] stark::storage_mode::StorageMode::Ram, ) .unwrap(); - let image = build_initial_image(&program, &[]); + let image = build_initial_image(&program, &[], &[]); let register_init = crate::tables::register::register_init_from_entry_point(program.entry_point); let from_image = Traces::from_image_and_logs( @@ -860,6 +861,7 @@ fn test_from_image_and_logs_matches_from_elf_and_logs() { &logs, &max_rows, &[], + &[], true, false, #[cfg(feature = "disk-spill")] @@ -889,14 +891,14 @@ fn test_epoch_end_memory_converts_to_image() { let elf_bytes = asm_elf_bytes("basic_program"); let program = Elf::load(&elf_bytes).unwrap(); - let total = Executor::new(&program, vec![]) + let total = Executor::new(&program, vec![], &[]) .unwrap() .run() .unwrap() .logs .len(); let epoch_size = (total / 3).max(1); - let epochs = Executor::new(&program, vec![]) + let epochs = Executor::new(&program, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); @@ -920,14 +922,14 @@ fn test_build_traces_for_all_epochs() { let elf_bytes = asm_elf_bytes("basic_program"); let program = Elf::load(&elf_bytes).unwrap(); - let total = Executor::new(&program, vec![]) + let total = Executor::new(&program, vec![], &[]) .unwrap() .run() .unwrap() .logs .len(); let epoch_size = (total / 3).max(1); - let epochs = Executor::new(&program, vec![]) + let epochs = Executor::new(&program, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); @@ -941,7 +943,7 @@ fn test_build_traces_for_all_epochs() { // previous epoch's ending memory + register snapshot. let (image, register_init): (HashMap, Vec) = if i == 0 { ( - build_initial_image(&program, &[]), + build_initial_image(&program, &[], &[]), crate::tables::register::register_init_from_entry_point(program.entry_point), ) } else { @@ -961,6 +963,7 @@ fn test_build_traces_for_all_epochs() { &epoch.logs, &max_rows, &[], + &[], i == last, false, #[cfg(feature = "disk-spill")] @@ -989,14 +992,14 @@ fn test_terminating_epoch_rejected_when_not_final() { let elf_bytes = asm_elf_bytes("basic_program"); let program = Elf::load(&elf_bytes).unwrap(); - let total = Executor::new(&program, vec![]) + let total = Executor::new(&program, vec![], &[]) .unwrap() .run() .unwrap() .logs .len(); let epoch_size = (total / 3).max(1); - let epochs = Executor::new(&program, vec![]) + let epochs = Executor::new(&program, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); @@ -1016,6 +1019,7 @@ fn test_terminating_epoch_rejected_when_not_final() { &epochs[last].logs, &MaxRowsConfig::default(), &[], + &[], false, false, #[cfg(feature = "disk-spill")] @@ -1043,20 +1047,20 @@ fn test_local_to_global_traces_from_real_execution() { let elf_bytes = asm_elf_bytes("all_loadstore_32"); let program = Elf::load(&elf_bytes).unwrap(); - let total = Executor::new(&program, vec![]) + let total = Executor::new(&program, vec![], &[]) .unwrap() .run() .unwrap() .logs .len(); let epoch_size = (total / 3).max(1); - let epochs = Executor::new(&program, vec![]) + let epochs = Executor::new(&program, vec![], &[]) .unwrap() .run_epochs(epoch_size) .unwrap(); assert!(epochs.len() >= 2); - let elf_image = build_initial_image(&program, &[]); + let elf_image = build_initial_image(&program, &[], &[]); let total_memory = elf_image.len(); // Per-epoch touched cells from real execution (epoch 0 from the ELF image, diff --git a/prover/src/tests/trace_test_helpers.rs b/prover/src/tests/trace_test_helpers.rs index 5544be69d..0ec59a2ad 100644 --- a/prover/src/tests/trace_test_helpers.rs +++ b/prover/src/tests/trace_test_helpers.rs @@ -131,12 +131,14 @@ impl Traces { logs: &[Log], max_rows: &MaxRowsConfig, private_input: &[u8], + hints: &[[u8; 32]], ) -> Result { let mut traces = Self::from_elf_and_logs( elf, logs, max_rows, private_input, + hints, #[cfg(feature = "disk-spill")] StorageMode::Ram, )?; diff --git a/prover/tests/calibration.rs b/prover/tests/calibration.rs index ff11bcf4b..92ac000e5 100644 --- a/prover/tests/calibration.rs +++ b/prover/tests/calibration.rs @@ -33,7 +33,7 @@ fn peak_bytes_does_not_underestimate_measured_heap() { let max_rows = MaxRowsConfig::default(); let lengths = - count_table_lengths(&elf, &logs, &max_rows, &[]).expect("count_table_lengths succeeds"); + count_table_lengths(&elf, &logs, &max_rows, &[], &[]).expect("count_table_lengths succeeds"); let opts = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is valid"); let predicted = peak_bytes(&lengths, opts.blowup_factor, table_parallelism()) as usize; @@ -56,7 +56,7 @@ fn peak_bytes_does_not_underestimate_measured_heap() { }; let _proof = - prove_with_options_and_inputs(&elf_bytes, &[], &opts, &max_rows).expect("proof succeeds"); + prove_with_options_and_inputs(&elf_bytes, &[], &[], &opts, &max_rows).expect("proof succeeds"); stop.store(true, Ordering::Relaxed); sampler.join().expect("sampler joins"); diff --git a/prover/tests/ecrecover_hints_continuation.rs b/prover/tests/ecrecover_hints_continuation.rs new file mode 100644 index 000000000..4147f8ddc --- /dev/null +++ b/prover/tests/ecrecover_hints_continuation.rs @@ -0,0 +1,61 @@ +//! Continuation-proof measurement for the `ecrecover_hints` guest on the hint +//! arena: proves the pass-2 run (arena hints) with continuations to bound +//! prover memory, and verifies the bundle. +//! +//! Fixtures are produced by the executor-side driver — run it first: +//! cargo test -p executor --test hint_arena_ecrecover -- --ignored --nocapture +//! Then: +//! cargo test -p lambda-vm-prover --test ecrecover_hints_continuation -- --ignored --nocapture + +use lambda_vm_prover::continuation::{prove_continuation, verify_continuation}; +use stark::proof::options::ProofOptions; +use std::time::Instant; + +/// Epoch size: 2^16 cycles. ~866k cycles → ~14 epochs, bounding per-epoch +/// prover memory. +const EPOCH_SIZE_LOG2: u32 = 16; + +#[test] +#[ignore = "measurement — run explicitly"] +fn ecrecover_arena_continuation_proof() { + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let dir = workspace_root.join("executor/program_artifacts/rust"); + let elf_bytes = std::fs::read(dir.join("ecrecover_hints.elf")) + .expect("ecrecover_hints.elf missing — run `make executor/program_artifacts/rust/ecrecover_hints.elf`"); + let input = std::fs::read(dir.join("ecrecover_hints.input.bin")).expect( + "ecrecover_hints.input.bin missing — run the executor driver: \ + cargo test -p executor --test hint_arena_ecrecover -- --ignored", + ); + let hints_bin = std::fs::read(dir.join("ecrecover_hints.hints.bin")) + .expect("ecrecover_hints.hints.bin missing — run the executor driver"); + assert_eq!(hints_bin.len() % 32, 0, "hints fixture must be 32-byte slots"); + let hints: Vec<[u8; 32]> = hints_bin + .chunks_exact(32) + .map(|c| c.try_into().unwrap()) + .collect(); + + let opts = ProofOptions::default_test_options(); + + let t0 = Instant::now(); + let bundle = prove_continuation(&elf_bytes, &input, &hints, EPOCH_SIZE_LOG2, &opts) + .expect("continuation prove"); + let prove_time = t0.elapsed(); + + let t0 = Instant::now(); + let public_output = verify_continuation(&elf_bytes, &bundle, &opts) + .expect("continuation verify") + .expect("bundle must verify"); + let verify_time = t0.elapsed(); + + println!("[ecrecover-continuation] epochs = {}", bundle.num_epochs()); + println!("[ecrecover-continuation] hints = {} slots", hints.len()); + println!("[ecrecover-continuation] prove = {prove_time:?}, verify = {verify_time:?}"); + println!( + "[ecrecover-continuation] public output = {} bytes, first 8: {:02x?}", + public_output.len(), + &public_output[..8.min(public_output.len())] + ); +} diff --git a/prover/tests/gpu_constraint_interp_real.rs b/prover/tests/gpu_constraint_interp_real.rs index 4446fb446..2cea4be1b 100644 --- a/prover/tests/gpu_constraint_interp_real.rs +++ b/prover/tests/gpu_constraint_interp_real.rs @@ -271,5 +271,4 @@ fn all_table_programs_gpu_match_cpu_oracle() { check_air(&create_keccak_rc_air(&opts), "KECCAK_RC"); check_air(&create_ecsm_air(&opts), "ECSM"); check_air(&create_ecdas_air(&opts), "ECDAS"); - check_air(&create_hint_air(&opts), "HINT"); } diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index 5228455ea..54a2cb88e 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -33,12 +33,7 @@ const KECCAK_SYSCALL_NUMBER: usize = usize::MAX - 1; #[cfg(target_arch = "riscv64")] const ECSM_SYSCALL_NUMBER: usize = usize::MAX - 10; -/// Syscall number for the non-constraining Hint ecall. -/// Must match `executor::...::execution::HINT_SYSCALL_NUMBER` (u64::MAX - 30). -#[cfg(target_arch = "riscv64")] -const HINT_SYSCALL_NUMBER: usize = usize::MAX - 30; - -/// Hint selectors passed in `a0` (must match the executor's `HINT_*`). +/// Hint selectors for the request log (must match the executor's `HINT_*`). pub const HINT_FIELD_INV: usize = 0; pub const HINT_SCALAR_INV: usize = 1; pub const HINT_FIELD_SQRT: usize = 2; @@ -138,6 +133,166 @@ pub fn get_private_input_slice() -> &'static [u8] { unimplemented!("syscalls are only implemented for riscv64 targets"); } +// ============================================================================= +// Hint arena — untrusted, host-supplied 32-byte hint values appended to the +// private-input region after the length-prefixed main data: +// +// [u32 LE main_len][main data][zero-pad to 8][u32 LE hint_count][u32 pad] +// then `hint_count` slots of 32 bytes, 8-aligned. +// +// Must match `executor::vm::memory::{hint_arena_header_offset, +// HINT_ARENA_HEADER_BYTES, HINT_SLOT_BYTES}` — the executor writes the region, +// this crate reads it. +// +// The values are UNTRUSTED (the prover chooses them): the caller MUST verify +// each hint in-guest (e.g. `x·inv == 1`) and recompute in software on failure. +// Consumption is positional — one slot per request, pass or fail — so a lying +// host can only trigger fallbacks, never shift the hint stream. +// ============================================================================= + +/// Byte size of one hint slot in the hint arena. +/// Must match `executor::vm::memory::HINT_SLOT_BYTES`. +#[cfg(target_arch = "riscv64")] +pub const HINT_SLOT_BYTES: usize = 32; + +/// Byte size of the arena header (`[u32 LE hint_count][u32 zero pad]`). +/// Must match `executor::vm::memory::HINT_ARENA_HEADER_BYTES`. +#[cfg(target_arch = "riscv64")] +const HINT_ARENA_HEADER_BYTES: usize = 8; + +/// Offset from `PRIVATE_INPUT_START` of the arena header for a given main-input +/// length: the 4-byte length prefix plus the data, padded up to 8 bytes. +/// Must match `executor::vm::memory::hint_arena_header_offset`. +#[cfg(target_arch = "riscv64")] +const fn hint_arena_header_offset(main_len: usize) -> usize { + (4 + main_len + 7) & !7 +} + +/// Absolute address of the arena header, derived from the (clamped) length +/// prefix — the same value `get_private_input_slice` trusts. +#[cfg(target_arch = "riscv64")] +fn hint_arena_header_addr() -> usize { + let len = (unsafe { core::ptr::read_volatile(PRIVATE_INPUT_START as *const u32) } as usize) + .min(MAX_PRIVATE_INPUT_SIZE); + PRIVATE_INPUT_START + hint_arena_header_offset(len) +} + +/// Number of hint slots the host supplied. 0 when no arena was written (the +/// header reads back as zero-filled memory). +#[cfg(target_arch = "riscv64")] +pub fn hint_count() -> usize { + unsafe { core::ptr::read_volatile(hint_arena_header_addr() as *const u32) as usize } +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn hint_count() -> usize { + unimplemented!("syscalls are only implemented for riscv64 targets"); +} + +/// Read hint slot `i` as raw bytes, or `None` when the arena has no slot `i`. +/// The slot is read as four aligned 8-byte words (the fast aligned-load path); +/// slots are 8-aligned by construction. +#[cfg(target_arch = "riscv64")] +pub fn hint_slot(i: usize) -> Option<[u8; 32]> { + if i >= hint_count() { + return None; + } + let addr = hint_arena_header_addr() + HINT_ARENA_HEADER_BYTES + i * HINT_SLOT_BYTES; + debug_assert_eq!(addr % 8, 0, "hint slot address must stay 8-aligned"); + // Read as four u64 words and re-serialize little-endian: the VM is + // little-endian, so this reproduces the exact byte sequence the host wrote. + let words: [u64; 4] = unsafe { core::ptr::read_volatile(addr as *const [u64; 4]) }; + let mut out = [0u8; 32]; + for (k, w) in words.iter().enumerate() { + out[8 * k..8 * k + 8].copy_from_slice(&w.to_le_bytes()); + } + Some(out) +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn hint_slot(_i: usize) -> Option<[u8; 32]> { + unimplemented!("syscalls are only implemented for riscv64 targets"); +} + +/// Consume the next hint slot positionally. Returns `None` once the arena is +/// exhausted — the caller then recomputes in software. One slot is consumed per +/// call whether or not the hint verifies, so a host that supplies fewer hints +/// than requested only forces fallbacks; it cannot desynchronize the stream. +#[cfg(target_arch = "riscv64")] +pub fn next_hint() -> Option<[u8; 32]> { + // Single-threaded guest: a relaxed atomic cursor is sufficient (the guest + // target lowers atomics via `-C passes=lower-atomic`). + static CURSOR: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0); + let i = CURSOR.fetch_add(1, core::sync::atomic::Ordering::Relaxed); + hint_slot(i) +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn next_hint() -> Option<[u8; 32]> { + unimplemented!("syscalls are only implemented for riscv64 targets"); +} + +// ============================================================================= +// Hint request log — the recording pass for hints that are NOT known +// beforehand. When the arena is exhausted, `request_hint` appends +// `(hint_id, input)` to a fixed scratch region above the private-input window; +/// the host reads it after the run (executor's `Memory::hint_requests`), +/// computes the answers, and re-runs with a complete arena. The log region: +/// +// [u32 LE count][u32 pad], then entries of [u64 LE hint_id][32-byte input]. +// +// Must match `executor::vm::memory::{HINT_LOG_START_INDEX, +// HINT_LOG_HEADER_BYTES, HINT_LOG_ENTRY_BYTES}`. +// ============================================================================= + +/// Start of the hint request log: just past the reserved private-input window +/// (`PRIVATE_INPUT_START + 4 + MAX_PRIVATE_INPUT_SIZE`, rounded up to 8). The +/// stack lives at the top of the 64-bit space and the heap grows up from the +/// ELF image, so this region is collision-free in practice. +/// Must match `executor::vm::memory::HINT_LOG_START_INDEX`. +#[cfg(target_arch = "riscv64")] +pub const HINT_LOG_START: usize = 0xFF000000 + 4 + 512 * 1024 * 1024 + 4; + +/// Log header size (`[u32 LE count][u32 pad]`). +/// Must match `executor::vm::memory::HINT_LOG_HEADER_BYTES`. +#[cfg(target_arch = "riscv64")] +const HINT_LOG_HEADER_BYTES: usize = 8; + +/// Log entry size (`[u64 LE hint_id][32-byte input]`). +/// Must match `executor::vm::memory::HINT_LOG_ENTRY_BYTES`. +#[cfg(target_arch = "riscv64")] +const HINT_LOG_ENTRY_BYTES: usize = 40; + +/// Consume the next hint slot for a `(hint_id, input)` request; on an exhausted +/// arena, append the request to the hint request log and return `None` (the +/// caller then recomputes in software). One slot is consumed per call whether +/// or not the hint verifies — see [`next_hint`]. +#[cfg(target_arch = "riscv64")] +pub fn request_hint(hint_id: usize, input: &[u8; 32]) -> Option<[u8; 32]> { + if let Some(slot) = next_hint() { + return Some(slot); + } + let count_addr = HINT_LOG_START; + let count = unsafe { core::ptr::read_volatile(count_addr as *const u32) } as usize; + let entry = count_addr + HINT_LOG_HEADER_BYTES + count * HINT_LOG_ENTRY_BYTES; + unsafe { + core::ptr::write_volatile(entry as *mut u64, hint_id as u64); + for k in 0..4 { + let word = u64::from_le_bytes(input[8 * k..8 * k + 8].try_into().unwrap()); + core::ptr::write_volatile((entry + 8 + 8 * k) as *mut u64, word); + } + core::ptr::write_volatile(count_addr as *mut u32, count as u32 + 1); + } + None +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn request_hint(_hint_id: usize, _input: &[u8; 32]) -> Option<[u8; 32]> { + unimplemented!("syscalls are only implemented for riscv64 targets"); +} + + + #[cfg(target_arch = "riscv64")] pub fn sys_halt() -> ! { // NOTE: no print_string here — the Print ecall is unmatched on the Ecall bus @@ -197,32 +352,6 @@ pub fn ecsm_mul(_xr: &mut [u8; 32], _xg: &[u8; 32], _k: &[u8; 32]) { unimplemented!("syscalls are only implemented for riscv64 targets"); } -/// Ask the host for a non-constraining hint (modular inverse/sqrt). -/// `hint_id` selects the operation ([`HINT_FIELD_INV`]/[`HINT_SCALAR_INV`]/ -/// [`HINT_FIELD_SQRT`]); `input`/`out` are 32-byte **big-endian** field/scalar -/// elements — k256's own serialization, so consumers pass `to_bytes()` straight -/// through. Note this differs from [`ecsm_mul`], which is little-endian. -/// The result is UNTRUSTED — the caller MUST verify it in-guest (e.g. `x·inv == 1`) -/// AND recompute in software on failure, since this ecall adds no correctness -/// constraint and the prover chooses the returned bytes. -#[cfg(target_arch = "riscv64")] -pub fn hint(hint_id: usize, out: &mut [u8; 32], input: &[u8; 32]) { - unsafe { - asm!( - "ecall", - in("a0") hint_id, // x10 = hint selector - in("a1") input.as_ptr(), // x11 = input address (32-byte BE) - in("a2") out.as_mut_ptr(), // x12 = output address (32-byte BE) - in("a7") HINT_SYSCALL_NUMBER, - ) - } -} - -#[cfg(not(target_arch = "riscv64"))] -pub fn hint(_hint_id: usize, _out: &mut [u8; 32], _input: &[u8; 32]) { - unimplemented!("syscalls are only implemented for riscv64 targets"); -} - // ============================================================================= // Stub implementations for unsupported std functions // These functions are required by Rust's std zkvm module but are not supported From 7e519fd161d03a5853bb1aa5e6aacf2a441319ac Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 19 Aug 2026 18:04:03 -0300 Subject: [PATCH 2/4] fix(guests): bump ecrecover_hints' ethrex-crypto pin to 4f658c2b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The restack brought in the ethrex bump, which moved lambda-vm-ethrex-crypto to ethrex 4f658c2b while the guest kept its direct dep at 156cb8d6 — two distinct Crypto traits, so the guest no longer compiled. Unify on the bumped rev (lockfile collapses to a single ethrex-crypto entry). --- .../programs/rust/ecrecover_hints/Cargo.lock | 68 +------------------ .../programs/rust/ecrecover_hints/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 67 deletions(-) diff --git a/executor/programs/rust/ecrecover_hints/Cargo.lock b/executor/programs/rust/ecrecover_hints/Cargo.lock index 56d252d8c..652ee8e28 100644 --- a/executor/programs/rust/ecrecover_hints/Cargo.lock +++ b/executor/programs/rust/ecrecover_hints/Cargo.lock @@ -162,18 +162,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -183,19 +171,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bls12_381" -version = "0.8.0" -source = "git+https://github.com/lambdaclass/bls12_381?branch=expose-affine-constructors#78cad0378b17fc3157b83f514be192bf46edf9a1" -dependencies = [ - "digest", - "ff", - "group", - "pairing", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "byteorder" version = "1.5.0" @@ -376,15 +351,13 @@ dependencies = [ [[package]] name = "ethrex-crypto" -version = "13.0.0" -source = "git+https://github.com/lambdaclass/ethrex.git?rev=156cb8d6a3974f411d71622eecd1b249ee37ff1c#156cb8d6a3974f411d71622eecd1b249ee37ff1c" +version = "22.0.0" +source = "git+https://github.com/lambdaclass/ethrex.git?rev=4f658c2b3d10e3f21d35ce546870f55ca3f940fc#4f658c2b3d10e3f21d35ce546870f55ca3f940fc" dependencies = [ "ark-bn254", "ark-ec", "ark-ff", - "bls12_381", "ethereum-types", - "ff", "hex-literal", "k256", "num-bigint", @@ -401,7 +374,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitvec", "rand_core 0.6.4", "subtle", ] @@ -423,12 +395,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "generic-array" version = "0.14.9" @@ -604,15 +570,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" -dependencies = [ - "group", -] - [[package]] name = "paste" version = "1.0.15" @@ -671,12 +628,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.7" @@ -855,12 +806,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "1.0.69" @@ -961,15 +906,6 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" version = "0.8.56" diff --git a/executor/programs/rust/ecrecover_hints/Cargo.toml b/executor/programs/rust/ecrecover_hints/Cargo.toml index c4980d5cf..fffe10138 100644 --- a/executor/programs/rust/ecrecover_hints/Cargo.toml +++ b/executor/programs/rust/ecrecover_hints/Cargo.toml @@ -13,4 +13,4 @@ lambda-vm-syscalls = { path = "../../../../syscalls" } lambda-vm-ethrex-crypto = { path = "../../../../crypto/ethrex-crypto" } # The `Crypto` trait the guest calls through. Same rev + default-features=false # as lambda-vm-ethrex-crypto's own dep, so feature unification adds nothing. -ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "156cb8d6a3974f411d71622eecd1b249ee37ff1c", package = "ethrex-crypto", default-features = false } +ethrex-crypto = { git = "https://github.com/lambdaclass/ethrex.git", rev = "4f658c2b3d10e3f21d35ce546870f55ca3f940fc", package = "ethrex-crypto", default-features = false } From cd1d6110eddfae6eaf793ea8d65f576a6393c93a Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 19 Aug 2026 18:40:50 -0300 Subject: [PATCH 3/4] perf(hints): auto-record the hint arena in prove/count entry points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The removed hint ecall accelerated hint-consuming guests on EVERY prove, no caller opt-in; the arena made acceleration opt-in, so hint-less flows (plain cli prove, bench_abba.sh) silently proved the software-fallback trace — measured on ethrex_10_transfers: 3.35M cycles without hints vs 1.31M hinted (the ecall baseline was 1.70M). resolve_hints restores the always-on ergonomics: when the caller passes an empty arena, prove_with_options_and_inputs / prove_continuation / count_elements first run the recording pass (collect_hints) and prove the hinted trace. Both runs commit the same output — the arena only changes cost — so the statement being proved is unchanged. Guests that request nothing get an empty arena and an identical trace; the only cost is one extra execution, negligible against proving. Regression coverage: test_prove_ecrecover_hints_auto_records_arena asserts hint-less and explicit-arena calls produce identical element counts and equal verified public outputs. --- prover/src/continuation.rs | 5 ++- prover/src/lib.rs | 46 ++++++++++++++++--- prover/src/tests/prove_elfs_tests.rs | 66 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index da6194ee0..783debf2c 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -1074,6 +1074,8 @@ pub fn prove_continuation( let __root = stark::instruments::span("prove_continuation_total"); let elf = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; + let resolved_hints = crate::resolve_hints(&elf, private_inputs, hints)?; + let hints: &[[u8; 32]] = &resolved_hints; let mut executor = Executor::new(&elf, private_inputs.to_vec(), hints) .map_err(|e| Error::Execution(format!("{e}")))?; // The DECODE precomputed commitment depends only on (ELF, opts): compute @@ -1431,7 +1433,8 @@ pub fn prove_continuation( let run = || -> Result { #[cfg(feature = "instruments")] let __sp = stark::instruments::span("prove_global"); - let num_private_input_pages = page::private_input_page_count(private_inputs, hints); + let num_private_input_pages = + page::private_input_page_count(private_inputs, hints); // SINGLE source of truth: the same page-base list drives the // committed GLOBAL_MEMORY tables and is shipped in the bundle, // so the two can never diverge in set or order. diff --git a/prover/src/lib.rs b/prover/src/lib.rs index d9261b51e..61ab82d0c 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -1037,6 +1037,34 @@ pub(crate) fn verify_l2g_commitment_binding_view( // Public API: Prove / Verify // ============================================================================= +/// Resolve the hint arena for a prove/count entry point: `hints` as-is when +/// non-empty, otherwise the two-pass recording flow — execute once with an +/// empty arena, answer the guest's logged requests with +/// [`executor::vm::instruction::execution::compute_hint`], and use the answers +/// as the arena for the proved run. +/// +/// The auto-record restores the ergonomics the removed hint ecall had: a +/// hint-consuming guest proved with no explicit arena still gets the cheap +/// in-guest-verify trace instead of the software-fallback one (measured on +/// `ethrex_10_transfers`: 3.35M cycles hint-less vs 1.31M hinted). Both runs +/// commit the same output — the arena only changes how cheaply the guest gets +/// there — so this never changes the statement being proved. For a guest that +/// requests nothing the record pass yields an empty arena and the proved run +/// is identical to what it would have been anyway; the only cost is one extra +/// execution, negligible against proving. +fn resolve_hints<'a>( + program: &Elf, + private_inputs: &[u8], + hints: &'a [[u8; 32]], +) -> Result, Error> { + if !hints.is_empty() { + return Ok(std::borrow::Cow::Borrowed(hints)); + } + let recorded = executor::vm::execution::collect_hints(program, private_inputs.to_vec()) + .map_err(|e| Error::Execution(format!("hint recording pass: {e}")))?; + Ok(std::borrow::Cow::Owned(recorded)) +} + /// Prove an ELF binary execution. Returns a serializable proof bundle. pub fn prove(elf_bytes: &[u8]) -> Result { prove_with_inputs(elf_bytes, &[], &[]) @@ -1046,7 +1074,10 @@ pub fn prove(elf_bytes: &[u8]) -> Result { /// /// `hints` are untrusted 32-byte values appended to the private-input memory /// region's hint arena; the guest reads them with ordinary loads and must -/// verify them in-circuit. +/// verify them in-circuit. When empty, the recording pass of the two-pass +/// hint flow runs automatically (see [`resolve_hints`]), so hint-consuming +/// guests get the cheap in-guest-verify trace without the caller supplying +/// anything. pub fn prove_with_inputs( elf_bytes: &[u8], private_inputs: &[u8], @@ -1075,7 +1106,8 @@ pub fn count_elements( hints: &[[u8; 32]], ) -> Result<(u64, u64), Error> { let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - let executor = Executor::new(&program, private_inputs.to_vec(), hints) + let hints = resolve_hints(&program, private_inputs, hints)?; + let executor = Executor::new(&program, private_inputs.to_vec(), &hints) .map_err(|e| Error::Execution(format!("{e}")))?; let result = executor .run() @@ -1085,7 +1117,7 @@ pub fn count_elements( &result.logs, &MaxRowsConfig::default(), private_inputs, - hints, + &hints, #[cfg(feature = "disk-spill")] StorageMode::Ram, )?; @@ -1129,7 +1161,8 @@ pub fn prove_with_options_and_inputs( let __sp = stark::instruments::span("execute"); let program = Elf::load(elf_bytes).map_err(|e| Error::ElfLoad(format!("{e}")))?; - let executor = Executor::new(&program, private_inputs.to_vec(), hints) + let hints = resolve_hints(&program, private_inputs, hints)?; + let executor = Executor::new(&program, private_inputs.to_vec(), &hints) .map_err(|e| Error::Execution(format!("{e}")))?; let result = executor .run() @@ -1150,7 +1183,8 @@ pub fn prove_with_options_and_inputs( #[cfg(feature = "disk-spill")] let storage_mode = { - let lengths = count_table_lengths(&program, &result.logs, max_rows, private_inputs, hints)?; + let lengths = + count_table_lengths(&program, &result.logs, max_rows, private_inputs, &hints)?; auto_storage::decide(&lengths, proof_options.blowup_factor) }; @@ -1159,7 +1193,7 @@ pub fn prove_with_options_and_inputs( &result.logs, max_rows, private_inputs, - hints, + &hints, #[cfg(feature = "disk-spill")] storage_mode, )?; diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index 05ea29d91..b80df3216 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1273,6 +1273,72 @@ fn test_prove_hint_arena_rust_guest() { assert_eq!(proof.public_output, expected.to_vec()); } +/// Auto-record policy: proving a hint-consuming guest with NO explicit arena +/// must transparently run the two-pass recording flow ([`crate::resolve_hints`]) +/// and cover the cheap hinted trace — not the software-fallback one. Asserts +/// the trace element counts match an explicit-arena call and that both proofs +/// verify with identical public outputs. Regression coverage for the removed +/// hint ecall's always-on ergonomics (hint-less `ethrex_10_transfers` measured +/// 3.35M cycles vs 1.31M hinted — flows that supply nothing must not regress). +#[test] +fn test_prove_ecrecover_hints_auto_records_arena() { + let _ = env_logger::builder().is_test(true).try_init(); + + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/ecrecover_hints.elf")) + .expect("ecrecover_hints.elf not found — run `make compile-programs-rust`"); + let program = executor::elf::Elf::load(&elf_bytes).expect("ELF load"); + + // Two ecrecover records (sig(64) || recid(1) || msg(32)), as built by the + // executor driver's fixture generator: r = 1 and 2 (both quadratic + // residues for r³ + 7), s = 1000/1001, recid parity 0/1, msg = 0x01.. / 0x08... + const REC0: [u8; 97] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xE8, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + ]; + const REC1: [u8; 97] = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0xE9, 0x01, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, + ]; + let mut input = Vec::with_capacity(4 + 2 * 97); + input.extend_from_slice(&2u32.to_le_bytes()); + input.extend_from_slice(&REC0); + input.extend_from_slice(&REC1); + + // The recording pass answers the guest's requests (sqrt + batched field + // inverse + scalar inverse per recovery, in that order). + let hints = executor::vm::execution::collect_hints(&program, input.clone()) + .expect("collect_hints"); + assert_eq!(hints.len(), 6, "2 recoveries × 3 hint requests"); + + // The policy under test: no explicit arena ⇒ auto-record ⇒ the SAME + // (hinted) trace an explicit arena produces. + let auto = crate::count_elements(&elf_bytes, &input, &[]).expect("count auto"); + let explicit = crate::count_elements(&elf_bytes, &input, &hints).expect("count explicit"); + assert_eq!(auto, explicit, "auto-record must produce the hinted trace"); + + // And both proofs verify with identical committed output. + let proof_auto = prove_vm_minimal(&elf_bytes, &input, &[], &Default::default()); + let proof_hints = prove_vm_minimal(&elf_bytes, &input, &hints, &Default::default()); + assert!(verify_vm_minimal(&proof_auto, &elf_bytes)); + assert!(verify_vm_minimal(&proof_hints, &elf_bytes)); + assert_eq!(proof_auto.public_output, proof_hints.public_output); +} + /// Soundness: the verifier REJECTS a forged ECSM result. /// /// A malicious prover must not be able to claim a wrong `k·G`. We tamper the result From 5781ff513782d29ecf002d1e455582bf119d6872 Mon Sep 17 00:00:00 2001 From: diegokingston Date: Wed, 19 Aug 2026 18:53:26 -0300 Subject: [PATCH 4/4] chore(hints): satisfy the lint gate (fmt + clippy) - cargo fmt over the hint-arena diff - group private_input/hints paths into ProveInputPaths so cmd_prove and cmd_prove_continuation stay under clippy's too_many_arguments (8/7), matching the FlamegraphCliOptions precedent Verified: cargo fmt --check --all plus all four CI clippy passes (default, debug-checks, disk-spill, cuda) green; the ethrex fixture checksum gate passes. --- bin/cli/src/main.rs | 45 ++--- executor/src/vm/execution.rs | 4 +- executor/src/vm/memory.rs | 18 +- executor/tests/hint_arena_ecrecover.rs | 10 +- prover/src/continuation.rs | 187 ++++++++++++++---- prover/src/lib.rs | 4 +- prover/src/tables/page.rs | 4 +- prover/src/tables/trace_builder.rs | 13 +- .../tests/count_table_lengths_drift_tests.rs | 4 +- prover/src/tests/prove_elfs_tests.rs | 85 ++++---- prover/src/tests/trace_builder_tests.rs | 6 +- prover/tests/calibration.rs | 8 +- prover/tests/ecrecover_hints_continuation.rs | 6 +- syscalls/src/syscalls.rs | 2 - 14 files changed, 260 insertions(+), 136 deletions(-) diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 64607f356..6c9f02ce4 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -297,19 +297,14 @@ fn main() -> ExitCode { epoch_size_log2, hints, } => { + let inputs = ProveInputPaths { + private_input, + hints, + }; if continuations { - cmd_prove_continuation( - elf, - output, - private_input, - epoch_size_log2, - blowup, - time, - cycles, - hints, - ) + cmd_prove_continuation(elf, output, inputs, epoch_size_log2, blowup, time, cycles) } else { - cmd_prove(elf, output, private_input, blowup, time, cycles, elements, hints) + cmd_prove(elf, output, inputs, blowup, time, cycles, elements) } } Commands::Verify { @@ -366,11 +361,7 @@ fn read_hints(path: Option<&PathBuf>) -> Result, String> { } } -fn count_cycles( - elf_data: &[u8], - private_inputs: &[u8], - hints: &[[u8; 32]], -) -> Result { +fn count_cycles(elf_data: &[u8], private_inputs: &[u8], hints: &[[u8; 32]]) -> Result { let program = Elf::load(elf_data).map_err(|e| format!("Failed to load ELF for cycle count: {e:?}"))?; let executor = Executor::new(&program, private_inputs.to_vec(), hints) @@ -638,15 +629,22 @@ fn cmd_execute( ExitCode::SUCCESS } +/// Input file paths shared by the prove commands: the guest's private input +/// and an optional hint arena (see [`read_hints`]). Grouped so the prove +/// commands stay under the argument-count lint. +struct ProveInputPaths { + private_input: Option, + hints: Option, +} + fn cmd_prove( elf_path: PathBuf, output_path: PathBuf, - private_input_path: Option, + inputs: ProveInputPaths, blowup: u8, time: bool, cycles: bool, elements: bool, - hints_path: Option, ) -> ExitCode { eprintln!("Reading ELF file..."); let elf_data = match std::fs::read(&elf_path) { @@ -657,7 +655,7 @@ fn cmd_prove( } }; - let private_inputs = match read_private_input(private_input_path.as_ref()) { + let private_inputs = match read_private_input(inputs.private_input.as_ref()) { Ok(inputs) => inputs, Err(e) => { eprintln!("{e}"); @@ -665,7 +663,7 @@ fn cmd_prove( } }; - let hints = match read_hints(hints_path.as_ref()) { + let hints = match read_hints(inputs.hints.as_ref()) { Ok(hints) => hints, Err(e) => { eprintln!("{e}"); @@ -841,12 +839,11 @@ fn cmd_verify(proof_path: PathBuf, elf_path: PathBuf, blowup: u8, time: bool) -> fn cmd_prove_continuation( elf_path: PathBuf, output_path: PathBuf, - private_input_path: Option, + inputs: ProveInputPaths, epoch_size_log2: Option, blowup: u8, time: bool, cycles: bool, - hints_path: Option, ) -> ExitCode { eprintln!("Reading ELF file..."); let elf_data = match std::fs::read(&elf_path) { @@ -857,7 +854,7 @@ fn cmd_prove_continuation( } }; - let private_inputs = match read_private_input(private_input_path.as_ref()) { + let private_inputs = match read_private_input(inputs.private_input.as_ref()) { Ok(inputs) => inputs, Err(e) => { eprintln!("{e}"); @@ -865,7 +862,7 @@ fn cmd_prove_continuation( } }; - let hints = match read_hints(hints_path.as_ref()) { + let hints = match read_hints(inputs.hints.as_ref()) { Ok(hints) => hints, Err(e) => { eprintln!("{e}"); diff --git a/executor/src/vm/execution.rs b/executor/src/vm/execution.rs index 24bb85101..3e5fe6b07 100644 --- a/executor/src/vm/execution.rs +++ b/executor/src/vm/execution.rs @@ -220,9 +220,7 @@ pub fn collect_hints( Ok(result .hint_requests .iter() - .map(|(hint_id, input)| { - crate::vm::instruction::execution::compute_hint(*hint_id, input) - }) + .map(|(hint_id, input)| crate::vm::instruction::execution::compute_hint(*hint_id, input)) .collect()) } diff --git a/executor/src/vm/memory.rs b/executor/src/vm/memory.rs index 705d20922..b19f5a6e7 100644 --- a/executor/src/vm/memory.rs +++ b/executor/src/vm/memory.rs @@ -68,10 +68,8 @@ pub const HINT_ARENA_HEADER_BYTES: u64 = 8; /// the recording pass of the two-pass hint flow. The host reads them back with /// [`Memory::hint_requests`]. Must match `HINT_LOG_START` in /// `syscalls/src/syscalls.rs`. -pub const HINT_LOG_START_INDEX: u64 = 0xFF000000 - + PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64 - + MAX_PRIVATE_INPUT_SIZE - + 4; +pub const HINT_LOG_START_INDEX: u64 = + 0xFF000000 + PRIVATE_INPUT_LENGTH_PREFIX_BYTES as u64 + MAX_PRIVATE_INPUT_SIZE + 4; /// Size in bytes of the request-log header (`[u32 LE count][u32 zero pad]`). pub const HINT_LOG_HEADER_BYTES: u64 = 8; /// Size in bytes of one request-log entry (`[u64 LE hint_id][32-byte input]`). @@ -97,7 +95,8 @@ pub fn encode_private_input_region( inputs: &[u8], hints: &[[u8; 32]], ) -> Result, MemoryError> { - let main_len = u32::try_from(inputs.len()).map_err(|_| MemoryError::PrivateInputSizeExceeded)?; + let main_len = + u32::try_from(inputs.len()).map_err(|_| MemoryError::PrivateInputSizeExceeded)?; let header_offset = hint_arena_header_offset(inputs.len() as u64); let hints_bytes = (hints.len() as u64) .checked_mul(HINT_SLOT_BYTES) @@ -296,9 +295,8 @@ impl Memory { let count = self.load_word(HINT_LOG_START_INDEX)? as usize; let mut out = Vec::with_capacity(count); for i in 0..count { - let entry = HINT_LOG_START_INDEX - + HINT_LOG_HEADER_BYTES - + i as u64 * HINT_LOG_ENTRY_BYTES; + let entry = + HINT_LOG_START_INDEX + HINT_LOG_HEADER_BYTES + i as u64 * HINT_LOG_ENTRY_BYTES; let hint_id = self.load_doubleword(entry)?; let bytes = self.load_bytes(entry + 8, 32)?; let mut input = [0u8; 32]; @@ -461,7 +459,9 @@ mod tests { for (i, hint) in hints.iter().enumerate() { let slot = memory .load_bytes( - PRIVATE_INPUT_START_INDEX + header + HINT_ARENA_HEADER_BYTES + PRIVATE_INPUT_START_INDEX + + header + + HINT_ARENA_HEADER_BYTES + i as u64 * HINT_SLOT_BYTES, HINT_SLOT_BYTES, ) diff --git a/executor/tests/hint_arena_ecrecover.rs b/executor/tests/hint_arena_ecrecover.rs index 1a7980541..0e24eaf2c 100644 --- a/executor/tests/hint_arena_ecrecover.rs +++ b/executor/tests/hint_arena_ecrecover.rs @@ -9,8 +9,8 @@ //! cargo test -p executor --test hint_arena_ecrecover -- --ignored --nocapture use executor::elf::Elf; -use executor::vm::instruction::execution::compute_hint; use executor::vm::execution::Executor; +use executor::vm::instruction::execution::compute_hint; use std::time::Instant; /// Number of ecrecovers the guest performs (3 hint requests each). @@ -98,8 +98,12 @@ fn ecrecover_two_pass_cycles() { ); println!("[ecrecover-hints] N = {N} recoveries"); - println!("[ecrecover-hints] pass 1 (software fallback): {pass1_cycles} cycles in {pass1_time:?}"); - println!("[ecrecover-hints] pass 2 (arena hints): {pass2_cycles} cycles in {pass2_time:?}"); + println!( + "[ecrecover-hints] pass 1 (software fallback): {pass1_cycles} cycles in {pass1_time:?}" + ); + println!( + "[ecrecover-hints] pass 2 (arena hints): {pass2_cycles} cycles in {pass2_time:?}" + ); println!( "[ecrecover-hints] guest cycle ratio pass1/pass2: {:.2}x", pass1_cycles as f64 / pass2_cycles as f64 diff --git a/prover/src/continuation.rs b/prover/src/continuation.rs index 783debf2c..f2490ad6c 100644 --- a/prover/src/continuation.rs +++ b/prover/src/continuation.rs @@ -1433,8 +1433,7 @@ pub fn prove_continuation( let run = || -> Result { #[cfg(feature = "instruments")] let __sp = stark::instruments::span("prove_global"); - let num_private_input_pages = - page::private_input_page_count(private_inputs, hints); + let num_private_input_pages = page::private_input_page_count(private_inputs, hints); // SINGLE source of truth: the same page-base list drives the // committed GLOBAL_MEMORY tables and is shipped in the bundle, // so the two can never diverge in set or order. @@ -1811,8 +1810,14 @@ mod tests { // 16-cycle epoch forces it into a later epoch where x254 is already 2. // Prove first so we can assert the run actually split into >1 epoch — without // this the test would silently pass even if it degraded to a single epoch. - let bundle = - prove_continuation(&elf_bytes, &[], &[], 4, &ProofOptions::default_test_options()).unwrap(); + let bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 4, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( bundle.num_epochs() > 1, "16-cycle epochs must split the run into multiple epochs" @@ -2070,8 +2075,14 @@ mod tests { fn test_split_verify_roundtrip() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_commit_split"); - let bundle = - prove_continuation(&elf_bytes, &[], &[], 4, &ProofOptions::default_test_options()).unwrap(); + let bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 4, + &ProofOptions::default_test_options(), + ) + .unwrap(); let out = verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()) .unwrap(); assert_eq!(out.as_deref(), Some(&[0xAA, 0xBB, 0xCC, 0xDD][..])); @@ -2083,8 +2094,14 @@ mod tests { fn test_continuation_rkyv_roundtrip() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_commit_split"); - let bundle = - prove_continuation(&elf_bytes, &[], &[], 4, &ProofOptions::default_test_options()).unwrap(); + let bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 4, + &ProofOptions::default_test_options(), + ) + .unwrap(); let bytes = rkyv::to_bytes::(&bundle).unwrap(); let restored: ContinuationProof = @@ -2102,8 +2119,14 @@ mod tests { fn test_split_verify_rejects_dropped_last_epoch() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!(bundle.epochs.len() >= 3, "need multiple epochs"); bundle.epochs.pop(); assert!( @@ -2120,8 +2143,14 @@ mod tests { fn test_split_verify_rejects_reordered_epochs() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!(bundle.epochs.len() >= 3, "need multiple epochs"); bundle.epochs.swap(0, 1); assert!( @@ -2139,8 +2168,14 @@ mod tests { fn test_split_verify_rejects_tampered_register_fini() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( bundle.epochs.len() >= 2, "need a second epoch to chain into" @@ -2162,8 +2197,14 @@ mod tests { fn test_split_verify_rejects_malformed_register_fini_length() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!(!bundle.epochs.is_empty()); bundle.epochs[0].reg_fini.pop(); assert!( @@ -2179,8 +2220,14 @@ mod tests { fn test_split_verify_rejects_inflated_epoch_table_count() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 8, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 8, + &ProofOptions::default_test_options(), + ) + .unwrap(); bundle.epochs[0].table_counts.cpu += 1; assert!( verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()) @@ -2203,9 +2250,14 @@ mod tests { let expected = input[4..12].to_vec(); // Smallest epochs (2^2 = 4 cycles) so the short program splits across epochs. - let bundle = - prove_continuation(&elf_bytes, &input, &[], 2, &ProofOptions::default_test_options()) - .unwrap(); + let bundle = prove_continuation( + &elf_bytes, + &input, + &[], + 2, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( bundle.num_epochs() > 1, "4-cycle epochs must split the run into multiple epochs" @@ -2242,9 +2294,14 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let mut bundle = - prove_continuation(&elf_bytes, &input, &[], 2, &ProofOptions::default_test_options()) - .unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &input, + &[], + 2, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( bundle.num_private_input_pages > 0, "baseline must have a touched private-input page" @@ -2276,9 +2333,14 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("test_private_input_xpage"); let input: Vec = (0u8..16).collect(); - let mut bundle = - prove_continuation(&elf_bytes, &input, &[], 2, &ProofOptions::default_test_options()) - .unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &input, + &[], + 2, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert_eq!( bundle.num_private_input_pages, 1, "16 bytes of private input fits in one page" @@ -2439,8 +2501,14 @@ mod tests { fn test_split_verify_rejects_oversized_num_private_input_pages() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); bundle.num_private_input_pages = page::max_private_input_pages() + 1; assert!(matches!( verify_continuation(&elf_bytes, &bundle, &ProofOptions::default_test_options()), @@ -2469,9 +2537,14 @@ mod tests { let expected: [u8; 8] = [0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x07, 0x18]; input[commit_off..commit_off + 8].copy_from_slice(&expected); - let bundle = - prove_continuation(&elf_bytes, &input, &[], 4, &ProofOptions::default_test_options()) - .unwrap(); + let bundle = prove_continuation( + &elf_bytes, + &input, + &[], + 4, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( bundle.num_private_input_pages >= 2, "input spanning two pages must give >=2 private pages" @@ -2501,8 +2574,14 @@ mod tests { fn test_split_verify_tolerates_reordered_touched_page_bases() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( !bundle.touched_page_bases.is_empty(), "baseline must have touched pages" @@ -2533,8 +2612,14 @@ mod tests { fn test_split_verify_rejects_dropped_touched_page_base() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( !bundle.touched_page_bases.is_empty(), "baseline must have touched pages" @@ -2563,8 +2648,14 @@ mod tests { fn test_split_verify_rejects_non_page_aligned_touched_page_base() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( !bundle.touched_page_bases.is_empty(), "baseline must have touched pages" @@ -2594,8 +2685,14 @@ mod tests { fn test_split_verify_rejects_tampered_l2g_root() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &ProofOptions::default_test_options()).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &ProofOptions::default_test_options(), + ) + .unwrap(); assert!( bundle.epochs.len() >= 2, "need multiple epochs to exercise the binding" @@ -2616,8 +2713,14 @@ mod tests { fn test_continuation_blob_rejects_tampered_l2g_root() { let _ = env_logger::builder().is_test(true).try_init(); let elf_bytes = asm_elf_bytes("all_loadstore_32"); - let mut bundle = - prove_continuation(&elf_bytes, &[], &[], 3, &crate::recursion::MIN_PROOF_OPTIONS).unwrap(); + let mut bundle = prove_continuation( + &elf_bytes, + &[], + &[], + 3, + &crate::recursion::MIN_PROOF_OPTIONS, + ) + .unwrap(); assert!( bundle.epochs.len() >= 2, "need multiple epochs to exercise the binding" diff --git a/prover/src/lib.rs b/prover/src/lib.rs index 61ab82d0c..879a40036 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -53,8 +53,8 @@ use crate::tables::types::BusId; use crate::test_utils::{ E, F, VmAir, create_bitwise_air, create_branch_air, create_bytewise_air, create_commit_air, create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, create_ecdas_air, - create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, - create_keccak_rc_air, create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, + create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, create_keccak_rc_air, + create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, create_memw_aligned_air, create_memw_register_air, create_mul_air, create_page_air, create_register_air, create_shift_air, create_store_air, }; diff --git a/prover/src/tables/page.rs b/prover/src/tables/page.rs index a1784724a..d293cd742 100644 --- a/prover/src/tables/page.rs +++ b/prover/src/tables/page.rs @@ -183,7 +183,9 @@ impl PageConfig { /// /// [`Memory::store_private_inputs`]: executor::vm::memory::Memory::store_private_inputs pub(crate) fn private_input_page_count(private_inputs: &[u8], hints: &[[u8; 32]]) -> usize { - use executor::vm::memory::{HINT_ARENA_HEADER_BYTES, HINT_SLOT_BYTES, hint_arena_header_offset}; + use executor::vm::memory::{ + HINT_ARENA_HEADER_BYTES, HINT_SLOT_BYTES, hint_arena_header_offset, + }; if private_inputs.is_empty() && hints.is_empty() { return 0; } diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index 3dffde11f..7c4e8456c 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -3413,9 +3413,13 @@ fn build_traces( // Continuation epochs (l2g_memory_bookend) skip PAGE: the L2G table owns // every touched cell's Memory init/fini, and every untouched PAGE row // self-cancels (init==fini, ts=0), so PAGE contributes nothing here. - Some(image) if !l2g_memory_bookend => { - generate_page_tables(image, memory_state, private_input, hints, l2g_memory_bookend) - } + Some(image) if !l2g_memory_bookend => generate_page_tables( + image, + memory_state, + private_input, + hints, + l2g_memory_bookend, + ), _ => (Vec::new(), Vec::new()), }; let gen_register = || register::generate_register_trace(®ister_final_state, register_init); @@ -3671,8 +3675,7 @@ pub fn count_table_lengths( let decode_rows = (instructions.len() as u64 + 1).next_power_of_two().max(2); // Memory + register state for partition predicates that need timestamps. - let mut memory_state = - MemoryState::from_image(&build_initial_image(elf, private_input, hints)); + let mut memory_state = MemoryState::from_image(&build_initial_image(elf, private_input, hints)); let mut register_state = RegisterState::new(elf.entry_point); // Raw counts (pre-chunking + pre-padding). diff --git a/prover/src/tests/count_table_lengths_drift_tests.rs b/prover/src/tests/count_table_lengths_drift_tests.rs index 4fad8cdae..8605960f9 100644 --- a/prover/src/tests/count_table_lengths_drift_tests.rs +++ b/prover/src/tests/count_table_lengths_drift_tests.rs @@ -11,8 +11,8 @@ fn assert_count_table_lengths_matches(elf: &Elf, logs: &[Log]) { let predicted = count_table_lengths(elf, logs, &max_rows, &[], &[]).expect("count_table_lengths succeeds"); - let traces = - Traces::from_elf_and_logs_minimal(elf, logs, &max_rows, &[], &[]).expect("trace build succeeds"); + let traces = Traces::from_elf_and_logs_minimal(elf, logs, &max_rows, &[], &[]) + .expect("trace build succeeds"); let sum_heights = |tables: &[stark::trace::TraceTable<_, _>]| -> u64 { tables.iter().map(|t| t.main_table.height as u64).sum() diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index b80df3216..e3db3e2c4 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1076,8 +1076,8 @@ fn test_prove_elfs_keccak_multi_call() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_keccak_multi"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // The guest initializes lane[i] = i + 1 and applies keccak-f[1600] three times. @@ -1098,7 +1098,8 @@ fn test_prove_elfs_keccak_multi_call() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); assert_eq!( traces.public_output_bytes, result.return_values.memory_values @@ -1116,8 +1117,8 @@ fn test_prove_elfs_ecsm() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // The guest computes 5·G and commits the 32-byte x-coordinate; cross-check it against @@ -1138,7 +1139,8 @@ fn test_prove_elfs_ecsm() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "ECSM prove/verify failed" @@ -1151,8 +1153,8 @@ fn test_prove_elfs_ecsm_multi() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm_multi"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // Gx little-endian. @@ -1177,7 +1179,8 @@ fn test_prove_elfs_ecsm_multi() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); assert!( prove_and_verify_vm_minimal(&elf, &mut traces), "ECSM multi-call prove/verify failed" @@ -1321,8 +1324,8 @@ fn test_prove_ecrecover_hints_auto_records_arena() { // The recording pass answers the guest's requests (sqrt + batched field // inverse + scalar inverse per recovery, in that order). - let hints = executor::vm::execution::collect_hints(&program, input.clone()) - .expect("collect_hints"); + let hints = + executor::vm::execution::collect_hints(&program, input.clone()).expect("collect_hints"); assert_eq!(hints.len(), 6, "2 recoveries × 3 hint requests"); // The policy under test: no explicit arena ⇒ auto-record ⇒ the SAME @@ -1354,11 +1357,12 @@ fn test_prove_elfs_ecsm_forged_result_rejected() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); // Forge the low byte of xR on the (single) real ECSM row. let orig = *traces.ecsm.main_table.get(0, ecsm_cols::xr(0)); @@ -1382,11 +1386,12 @@ fn test_prove_elfs_ecsm_forged_ecdas_mu_rejected() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_ecsm"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); // Row 0 is a real ECDAS step (µ=1); forge µ to a non-boolean value. traces.ecdas.main_table.set( @@ -1418,11 +1423,12 @@ fn test_prove_elfs_keccak_unaligned_state_addr() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_keccak_multi"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); // Tamper the first real keccak row: replace addr(1) (a byte cell) with a // value outside [0, 256). The new ARE_BYTES bus sender will emit this @@ -1444,8 +1450,8 @@ fn test_prove_elfs_keccak_unaligned_state_addr() { fn test_prove_elfs_test_commit_4() { let elf_bytes = crate::test_utils::asm_elf_bytes("test_commit_4"); let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); // Verify public output matches the committed bytes [0xAA, 0xBB, 0xCC, 0xDD] @@ -1456,7 +1462,8 @@ fn test_prove_elfs_test_commit_4() { ); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); assert_eq!( traces.public_output_bytes, result.return_values.memory_values @@ -1478,11 +1485,12 @@ fn test_prove_elfs_test_commit_4_wrong_pages_rejected() { let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let proof_options = ProofOptions::default_test_options(); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); // Prover uses correct page configs let table_counts = traces.table_counts(); @@ -2233,11 +2241,12 @@ fn test_deep_stack_runtime_pages_roundtrip() { let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let proof_options = ProofOptions::default_test_options(); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -2315,11 +2324,12 @@ fn test_deep_stack_missing_pages_rejected() { let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let proof_options = ProofOptions::default_test_options(); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); // Prover uses correct page configs (auto-detected from MemoryState) let table_counts = traces.table_counts(); @@ -2417,11 +2427,12 @@ fn test_heap_alloc_runtime_pages_roundtrip() { let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); let proof_options = ProofOptions::default_test_options(); - let executor = - executor::vm::execution::Executor::new(&elf, vec![], &[]).expect("Failed to create executor"); + let executor = executor::vm::execution::Executor::new(&elf, vec![], &[]) + .expect("Failed to create executor"); let result = executor.run().expect("Failed to run program"); let mut traces = - Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]).unwrap(); + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[], &[]) + .unwrap(); let runtime_page_ranges = traces.runtime_page_ranges(); let table_counts = traces.table_counts(); @@ -2808,8 +2819,8 @@ fn test_pure_commit_rust() { #[test] fn test_prove_with_input_empty() { let elf_bytes = crate::test_utils::asm_elf_bytes("sub"); - let result = - crate::prove_with_inputs(&elf_bytes, &[], &[]).expect("prove_with_inputs should succeed on sub"); + let result = crate::prove_with_inputs(&elf_bytes, &[], &[]) + .expect("prove_with_inputs should succeed on sub"); assert!( crate::verify(&result, &elf_bytes).expect("verify should not error"), "prove_with_inputs(empty) proof should verify" diff --git a/prover/src/tests/trace_builder_tests.rs b/prover/src/tests/trace_builder_tests.rs index 3acdd8bf7..b2332b0bf 100644 --- a/prover/src/tests/trace_builder_tests.rs +++ b/prover/src/tests/trace_builder_tests.rs @@ -837,7 +837,11 @@ fn test_from_image_and_logs_matches_from_elf_and_logs() { let elf_bytes = asm_elf_bytes("basic_program"); let program = Elf::load(&elf_bytes).unwrap(); - let logs = Executor::new(&program, vec![], &[]).unwrap().run().unwrap().logs; + let logs = Executor::new(&program, vec![], &[]) + .unwrap() + .run() + .unwrap() + .logs; let max_rows = MaxRowsConfig::default(); let from_elf = Traces::from_elf_and_logs( diff --git a/prover/tests/calibration.rs b/prover/tests/calibration.rs index 310d162eb..d9789742b 100644 --- a/prover/tests/calibration.rs +++ b/prover/tests/calibration.rs @@ -32,8 +32,8 @@ fn peak_bytes_does_not_underestimate_measured_heap() { let elf_bytes = asm_elf_bytes("fib_iterative_372k"); let max_rows = MaxRowsConfig::default(); - let lengths = - count_table_lengths(&elf, &logs, &max_rows, &[], &[]).expect("count_table_lengths succeeds"); + let lengths = count_table_lengths(&elf, &logs, &max_rows, &[], &[]) + .expect("count_table_lengths succeeds"); let opts = GoldilocksCubicProofOptions::with_blowup(2).expect("blowup=2 is valid"); let predicted = @@ -56,8 +56,8 @@ fn peak_bytes_does_not_underestimate_measured_heap() { }) }; - let _proof = - prove_with_options_and_inputs(&elf_bytes, &[], &[], &opts, &max_rows).expect("proof succeeds"); + let _proof = prove_with_options_and_inputs(&elf_bytes, &[], &[], &opts, &max_rows) + .expect("proof succeeds"); stop.store(true, Ordering::Relaxed); sampler.join().expect("sampler joins"); diff --git a/prover/tests/ecrecover_hints_continuation.rs b/prover/tests/ecrecover_hints_continuation.rs index 4147f8ddc..5ad471982 100644 --- a/prover/tests/ecrecover_hints_continuation.rs +++ b/prover/tests/ecrecover_hints_continuation.rs @@ -31,7 +31,11 @@ fn ecrecover_arena_continuation_proof() { ); let hints_bin = std::fs::read(dir.join("ecrecover_hints.hints.bin")) .expect("ecrecover_hints.hints.bin missing — run the executor driver"); - assert_eq!(hints_bin.len() % 32, 0, "hints fixture must be 32-byte slots"); + assert_eq!( + hints_bin.len() % 32, + 0, + "hints fixture must be 32-byte slots" + ); let hints: Vec<[u8; 32]> = hints_bin .chunks_exact(32) .map(|c| c.try_into().unwrap()) diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index 54a2cb88e..697212b05 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -291,8 +291,6 @@ pub fn request_hint(_hint_id: usize, _input: &[u8; 32]) -> Option<[u8; 32]> { unimplemented!("syscalls are only implemented for riscv64 targets"); } - - #[cfg(target_arch = "riscv64")] pub fn sys_halt() -> ! { // NOTE: no print_string here — the Print ecall is unmatched on the Ecall bus