Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cfdbb69
feat(hints): replace the hint ecall with a private-input hint arena
diegokingston Aug 19, 2026
ee8ee23
Merge branch 'chore/bump-ethrex-rev-finish' into feat/hint-arena
diegokingston Aug 19, 2026
7e519fd
fix(guests): bump ecrecover_hints' ethrex-crypto pin to 4f658c2b
diegokingston Aug 19, 2026
cd1d611
perf(hints): auto-record the hint arena in prove/count entry points
diegokingston Aug 19, 2026
5781ff5
chore(hints): satisfy the lint gate (fmt + clippy)
diegokingston Aug 19, 2026
521b84c
Merge remote-tracking branch 'origin/chore/bump-ethrex-rev' into feat…
jotabulacios Aug 26, 2026
3ad5f77
Update gpu_force_downgrade to the three-argument prove_with_inputs
jotabulacios Aug 26, 2026
3b77a6f
Unify ecrecover_hints' ethrex pin at 797df554
jotabulacios Aug 26, 2026
3db5708
Update the ethrex-tests call site to the three-argument Executor::new
jotabulacios Aug 27, 2026
4dcab54
Answer hint requests during the run instead of requiring a recording …
jotabulacios Aug 25, 2026
8dbfc7c
Add a driver that measures what the hint mechanism costs on real ethr…
jotabulacios Aug 25, 2026
37f4d38
Answer hint requests during a continuation too, dropping its pre-pass.
jotabulacios Aug 25, 2026
d1320f1
Drop the doc reference to the deleted resolve_hints from prove_with_i…
jotabulacios Aug 25, 2026
4ea8f08
Hook the hint request log on the guest's store instruction, not in Me…
jotabulacios Aug 25, 2026
1a61b19
Fix the gpu_force_downgrade comment for one-pass
jotabulacios Aug 27, 2026
85b0cf8
Fix the hint request order in ecrecover_hints doc
jotabulacios Aug 27, 2026
44c68ac
Fix the stale hint comment in run_with_flamegraph
jotabulacios Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 149 additions & 23 deletions bin/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ 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<PathBuf>,

/// Run the guest once and write the hint arena it produced
/// (concatenated 32-byte slots) to this file, for later use with
/// --hints. Requests are answered during that run, so this is the
/// cheap hinted path, not the software-fallback one; the file is only
/// needed to reuse an arena across runs.
#[arg(
long,
value_hint = ValueHint::FilePath,
conflicts_with_all = ["flamegraph", "cycle_budget", "hints", "cycles"]
)]
record_hints: Option<PathBuf>,
},

/// Generate a proof for an ELF program
Expand Down Expand Up @@ -193,6 +211,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<u32>,

/// 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<PathBuf>,
},

/// Verify a proof bundle
Expand Down Expand Up @@ -227,6 +250,11 @@ enum Commands {
/// Path to the private input file
#[arg(long, value_hint = ValueHint::FilePath)]
private_input: Option<PathBuf>,

/// 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<PathBuf>,
},
}

Expand All @@ -243,6 +271,8 @@ fn main() -> ExitCode {
flamegraph_checkpoint_cycles,
cycle_budget,
cycles,
hints,
record_hints,
} => cmd_execute(
elf,
private_input,
Expand All @@ -253,6 +283,8 @@ fn main() -> ExitCode {
},
cycle_budget,
cycles,
hints,
record_hints,
),
Commands::Prove {
elf,
Expand All @@ -264,19 +296,16 @@ fn main() -> ExitCode {
elements,
continuations,
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,
)
cmd_prove_continuation(elf, output, inputs, epoch_size_log2, blowup, time, cycles)
} else {
cmd_prove(elf, output, private_input, blowup, time, cycles, elements)
cmd_prove(elf, output, inputs, blowup, time, cycles, elements)
}
}
Commands::Verify {
Expand All @@ -292,7 +321,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),
}
}

Expand All @@ -306,10 +339,33 @@ fn read_private_input(path: Option<&PathBuf>) -> Result<Vec<u8>, String> {
}
}

fn count_cycles(elf_data: &[u8], private_inputs: &[u8]) -> Result<u64, String> {
/// 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<Vec<[u8; 32]>, 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<u64, String> {
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()
Expand Down Expand Up @@ -383,6 +439,8 @@ fn cmd_execute(
flamegraph: FlamegraphCliOptions,
cycle_budget: Option<u64>,
cycles: bool,
hints_path: Option<PathBuf>,
record_hints_path: Option<PathBuf>,
) -> ExitCode {
let elf_data = match std::fs::read(&elf_path) {
Ok(data) => data,
Expand All @@ -408,6 +466,36 @@ fn cmd_execute(
}
};

// Run once and dump the arena the executor built answering the guest's
// requests, for --hints runs that want to reuse it.
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
Expand Down Expand Up @@ -469,7 +557,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);
Expand Down Expand Up @@ -542,10 +630,18 @@ 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<PathBuf>,
hints: Option<PathBuf>,
}

fn cmd_prove(
elf_path: PathBuf,
output_path: PathBuf,
private_input_path: Option<PathBuf>,
inputs: ProveInputPaths,
blowup: u8,
time: bool,
cycles: bool,
Expand All @@ -560,19 +656,27 @@ 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}");
return ExitCode::FAILURE;
}
};

let hints = match read_hints(inputs.hints.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}");
Expand All @@ -585,7 +689,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);
Expand Down Expand Up @@ -620,6 +724,7 @@ fn cmd_prove(
let proof = prover::prove_with_options_and_inputs(
&elf_data,
&private_inputs,
&hints,
&opts,
&Default::default(),
);
Expand Down Expand Up @@ -735,7 +840,7 @@ 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<PathBuf>,
inputs: ProveInputPaths,
epoch_size_log2: Option<u32>,
blowup: u8,
time: bool,
Expand All @@ -750,16 +855,24 @@ 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}");
return ExitCode::FAILURE;
}
};

let hints = match read_hints(inputs.hints.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}");
Expand Down Expand Up @@ -800,6 +913,7 @@ fn cmd_prove_continuation(
let bundle = match prover::continuation::prove_continuation(
&elf_data,
&private_inputs,
&hints,
epoch_size_log2,
&opts,
) {
Expand Down Expand Up @@ -916,7 +1030,11 @@ fn cmd_verify_continuation(
}
}

fn cmd_count_elements(elf_path: PathBuf, private_input_path: Option<PathBuf>) -> ExitCode {
fn cmd_count_elements(
elf_path: PathBuf,
private_input_path: Option<PathBuf>,
hints_path: Option<PathBuf>,
) -> ExitCode {
let elf_data = match std::fs::read(&elf_path) {
Ok(data) => data,
Err(e) => {
Expand All @@ -933,7 +1051,15 @@ fn cmd_count_elements(elf_path: PathBuf, private_input_path: Option<PathBuf>) ->
}
};

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);
Expand Down
4 changes: 2 additions & 2 deletions crypto/ethrex-crypto/Cargo.lock

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

Loading
Loading