fix(crypto): run every proof on one dedicated prover thread - #641
Conversation
With --prover-arena, leanVM's arena gives each thread that drives a proof its own slab and never takes it back, so its faulted pages stay resident for the life of the process. ethlambda proved on whichever thread asked: the aggregation worker's spawn_blocking thread, which tokio replaces once it idles, and the actor for block building and re-aggregation. Each new thread pinned up to one proof's peak, so devnet-5 aggregators grew past their memory limit and were OOM-killed about every 40 minutes. Proofs now go through a single long-lived `leanvm-prover` thread, which also keeps the one-proof-at-a-time rule the mutex enforced. The new arena_slabs test proves from fresh threads and reads the arena's slab count: on main it grows by one per thread (10 -> 14), here it stays at 10.
🤖 Kimi Code ReviewI'll review this PR which replaces a mutex-based prover permit with a dedicated single prover thread to prevent OOM issues from arena slab accumulation. SummaryThe change is architecturally sound: moving from Critical Issues1. Unbounded channel allows unbounded memory growth under loadFile: let (tx, rx) = mpsc::channel::<ProverJob>();
Fix: Use 2.
|
| Category | Verdict |
|---|---|
| Correctness | Acceptable with fixes for items 1, 2, 3 |
| Security | No direct vulnerabilities; resource exhaustion risk (item 1) |
| Performance | Neutral to slightly positive (thread creation amortized) |
| Maintainability | Improved with better comments on AssertUnwindSafe |
Required before merge: Address item 1 (unbounded channel) or justify why it's safe. Item 2 (silent send error) should at minimum have corrected comment or logging. Item 3 (thread spawn panic) should be considered for production hardening.
Automated review by Kimi (Moonshot AI) · kimi-k2.6 · custom prompt
🤖 Claude Code ReviewReview of PR 641: run every proof on one dedicated prover threadVerdict: The change is sound. I read the diff only and did not build or run the tests. I have no blocking concerns. What it does:
Correct:
Minor points:
The Automated review by Claude (Anthropic) · sonnet · custom prompt |
🤖 Codex Code ReviewLooks good overall. The main change — serializing all
One thing I’d still flag:
Minor maintainability note:
If you want, I can also turn this into a ready-to-post GitHub review comment. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
| match reply_rx | ||
| .recv() | ||
| .expect("the prover thread replies to every job") | ||
| { | ||
| Ok(value) => value, | ||
| Err(payload) => panic::resume_unwind(payload), | ||
| } |
There was a problem hiding this comment.
| match reply_rx | |
| .recv() | |
| .expect("the prover thread replies to every job") | |
| { | |
| Ok(value) => value, | |
| Err(payload) => panic::resume_unwind(payload), | |
| } | |
| match reply_rx | |
| .recv() | |
| .expect("the prover thread replies to every job") | |
| .unwrap_or_else(|_| panic::resume_unwind(payload)) |
| static QUEUE: OnceLock<mpsc::Sender<ProverJob>> = OnceLock::new(); | ||
| QUEUE.get_or_init(|| { | ||
| let (tx, rx) = mpsc::channel::<ProverJob>(); | ||
| thread::Builder::new() |
There was a problem hiding this comment.
Small note on the stack: 2 MiB matches the tokio threads the node proved on, but not every caller. ethlambda benchmark runs synchronously on the main thread (bin/ethlambda/src/main.rs:72, 8 MiB stack), so its proofs (benchmark/corpus.rs:155 plus the block building it drives) now drop to 2 MiB. Same for the actor's proofs under shadow-integration, where the runtime is current_thread on main.
Probably fine since production already proves on 2 MiB, but since aggregation has hit stack overflows before (the reason tests run under release-fast), maybe set an explicit .stack_size(...) here, or at least soften the "the size tokio gives its own threads" wording in the doc comment and PR body?
The reply is already a Result whose error arm only resumes the unwind, so the combinator says the same thing as the match in fewer lines. Addresses review feedback on #641.
🗒️ Description / Motivation
devnet-5 aggregators running
--prover-arenawere OOM-killed about every 40 minutes (77-98 restarts in ~65 h, each at ~29 GB anon RSS against a 28 GiB memory limit). Non-aggregators, and aggregators without the arena, stayed flat.The cause is how leanVM's arena (
zk_alloc) interacts with the threads ethlambda proves from:/proc/PID/pagemap; pool-worker slabs stayed near 0).spawn_blocking, a thread tokio replaces once it idles) and the actor (block building, re-aggregation). Every new calling thread pinned another proof peak, up to the 24-slab cap.What Changed
crates/common/crypto/src/lib.rs:prove()replaces theacquire_prover()mutex. It sends the proving closure to one long-livedleanvm-proverthread and blocks the caller until the proof returns. Each of the five proving sites wraps only itsaggregatecall; decoding and argument conversion stay on the caller.crates/common/crypto/tests/arena_slabs.rs(new): with the arena on, proves once from each of 5 fresh threads and checks thatzk_alloc::stats().threads(slabs handed out) does not grow after the first.zk_allocis a new dev-dependency, pinned to the same leanVM rev asleanvm, because the facade does not re-export it. The comment inCargo.tomlsays the two revs must stay equal: only then is it the same crate instance, with the same statics.crates/common/crypto/tests/common/mod.rs: the key helperarena.rshad, now shared by both arena tests.CLAUDE.md: a note on why all proving goes through one thread.Correctness / Behavior Guarantees
error!) and re-raised on the caller withresume_unwind, so callers see the same panic as before and later proofs still run. The mutex version recovered from poisoning for the same reason.release-fast.prove()again (the prover thread would wait on itself). No caller does this; the doc comment says so.--prover-arena, apart from the thread the proof runs on.Tests Added / Run
mainarena_slabs(ignored, slow): slab count after 4 more proofs from fresh threadsprove_runs_every_job_on_one_thread(unit)prove_reraises_a_panic_and_keeps_proving(unit)test_aggregate_single_signature,test_type_2_merge_verify_split_round_trip,aggregates_on_the_arena_when_enabled(ignored, real proofs)test_setup_is_idempotentno longer acquires the permit twice, since the permit is gone.Related Issues / PRs
riscv-exploration, notmain, and does not cover long-lived threads that take turns proving.verifyreachesmle_eval_par→eq_table_arenaon the calling thread, which uses the arena whenever another thread has a proof's phase open. Each such slab touches only KiB, so it is not an OOM source.✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(cargo test --workspace --profile release-fast) — ran--lib --binsplus the crypto tests above; leanSpec spec tests not run