fix(bb-prover): rebuild the batch verifier after its bb dies - #359
charlielye wants to merge 6 commits into
Conversation
|
4d9c4db to
0f8e6ea
Compare
0f8e6ea to
9178d30
Compare
5aef838 to
8620014
Compare
A dead batch verifier latched a fatal error and stayed dead for the life of the node. Every gossiped transaction then failed proof verification, and because verifyProof turned any failure into `valid: false`, and that result feeds gossip validation at LowToleranceError, the node worked through its peer set penalising honest nodes for its own dead helper process. Two changes, and the second matters even if the first never fires. The verifier rebuilds itself on the next verification after a fatal error. Everything the session holds comes from configuration — the protocol circuit verification keys, the batch size, the core count — so nothing is lost by replacing the bb process and starting a fresh session on it; only the proofs already in flight were lost, and those were rejected when it failed. Concurrent callers share one rebuild, and attempts are paced at one a second, indefinitely rather than a bounded number of times and flat rather than backing off, which is the convention the AVM simulator pool settled on so a node recovers as soon as bb is healthy rather than waiting out a backoff it has already outlived. A verifier that could not check a proof no longer reports it as invalid. It has not judged the proof, and saying otherwise is what turns a dead helper into a punished peer. That is the same distinction the RPC verifier draws. Not run: the labs tests need stdlib, which needs constants generated from the Noir sources in aztec-packages. The behaviour wants an integration test that kills the bb the verifier spawned and verifies again, which needs the pid exposed; happy to add it if that is wanted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
Awaiting the rebuild check before queueing let a stop() issued after the proof was submitted close the queue first, so the proof was rejected instead of drained. Only defer when a rebuild is actually needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
…thout penalty Propagating ProofVerifierUnavailableError was not enough: gossip validation turns any thrown error into a rejection with a penalty, so a dead local bb still had the node penalising honest peers. Stage 2 now ignores the tx instead. The error moves to stdlib so p2p can recognise it. In the batch verifier, proofs in flight when the session fails, and proofs bb fails to take, now fail as unavailable rather than invalid. stop() waits out a rebuild in progress so it cannot leave a bb process running after teardown, and each session gets its own FIFO reader, so a replaced reader's late 'end' cannot fail the session that replaced it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
8620014 to
8766b94
Compare
…ilding it in place Rebuilding swapped a new bb session into the fields of the one that died, so every piece of session state needed a guard against the old one: a stale FIFO reader's events, a shutdown racing the rebuild, proofs submitted during it. The last of those, stop() waiting for a rebuild, could hang shutdown on a bb that never finished starting. BatchChonkVerifier goes back to being one session that never rebuilds; it only reports the proofs it could not check as unavailable, and whether it has failed. SelfHealingChonkVerifier owns the current one and, on the next verification after a failure, retires it and creates a replacement, paced as before. stop() stops the verifier in use and never waits for one still being created, which stops itself when it is ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
| const verifier = await this.liveVerifier(); | ||
| return await verifier.verifyProof(tx); |
There was a problem hiding this comment.
Shutdown can penalize peers If node shutdown or a
realProofs configuration change occurs after liveVerifier() selects a healthy verifier but before this call reaches it, stop() closes the verifier first. BatchChonkVerifier converts its “stopped” error into valid: false, so gossip can reject a valid transaction and penalize its sender instead of ignoring a proof it could not check.
Knowledge Base Used: P2P network implementation
There was a problem hiding this comment.
(Written by Claude on behalf of Facundo)
Partly fixed in 440939d. A proof sent to a stopped BatchChonkVerifier now rejects with ProofVerifierUnavailableError, so it's ignored rather than penalised. Proofs already in flight when it stops can still become valid: false, through the stop drain timing out or erroring. I've raised that in the review, and #383 converts those paths. Leaving this open until then.
| this.started = undefined; | ||
| if (verifier) { | ||
| void this.retire(verifier); | ||
| } | ||
| this.current = this.startReplacement(); |
There was a problem hiding this comment.
Shutdown skips retiring verifier During replacement, this clears
started and begins stopping the failed verifier without waiting for it. If node shutdown starts then, the wrapper’s stop() returns without waiting for that verifier’s FIFO reader and backend teardown. Shutdown can proceed while those handles are still being released.
Knowledge Base Used: Aztec node service and APIs
There was a problem hiding this comment.
(Written by Claude on behalf of Facundo)
Not fixed yet. stop() still doesn't wait for a verifier that liveVerifier() is retiring. That teardown is bounded by the 5 s stop timeouts, so I've listed it as a nit in the review. Leaving this open.
…available A verification that reached the verifier just as it stopped was reported as an invalid proof, so gossip penalised the peer that sent it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
…oofs #359 was rebased onto main after #354 was squash-merged, and its batch verifier no longer rebuilds in place: BatchChonkVerifier is one session again and SelfHealingChonkVerifier replaces it when it fails. This branch's own commits are carried over unchanged in intent. The conflicts were: - batch_chonk_verifier.ts: the rebuild code this branch's verifyProof sat next to is gone; verifyProof, isBatchVerifierInternalError and the internal-error handling in handleResult are kept as they were. - libp2p_service.ts: main's #286 made the gossip validation outcome generic over the failure consequence (IgnoreWithoutPenalty). The outcome keeps that and this branch's passed/failed/unverifiable status; the stage-2 catch is still dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NuUzj3qpkpJor6GMpWB4T4
fcarreiro
left a comment
There was a problem hiding this comment.
(Written by Claude on behalf of Facundo)
I reasoned through this; I didn't run it. CI is green. Replacing the whole verifier is the right shape: a bb death closes the results FIFO and the verifier latches as unavailable. The next verification then gets a fresh one, and a replacement that finishes after stop() stops itself, so shutdown never waits on one. Gossip ignoring ProofVerifierUnavailableError without a penalty is the important fix.
Two inline comments: a replacement that hangs has no deadline, and some paths still report an unavailable verifier as a verdict (#383 covers those).
Nits:
stop()during a replacement doesn't wait for the failed verifier's teardown, whichliveVerifier()starts withvoid this.retire(verifier)(Greptile's comment on line 84). It's bounded by the 5 s stop timeouts, and the exit handler cleans up the FIFO, so this is tidiness: keep the promise and await it, bounded, instop().- Recovery after a real bb death isn't tested. The description says why: the bb's pid isn't exposed.
| } | ||
|
|
||
| private async startReplacement(): Promise<FailingChonkVerifier> { | ||
| const verifier = await this.create(); |
There was a problem hiding this comment.
(Written by Claude on behalf of Facundo)
A replacement that hangs here stops peer-tx verification for good. create() is BatchChonkVerifier.new, which runs Barretenberg.new, then initSRSChonk(), then chonkBatchVerifierStart() (batch_chonk_verifier.ts:122-140). bb.js bounds only the first, with its 60 s startup timeout. The other two are calls on a running bb, with no timeout.
If either hangs, this.current never settles, and every later gossiped tx waits on it in liveVerifier() forever. It isn't rejected as unavailable, and no further replacement is tried. That's plausible right after a death caused by memory pressure.
Suggest a deadline on creation. Past it, treat the creation as failed, so the once-per-interval retry takes over, and stop the late verifier if it ever finishes.
| if (this.stopped) { | ||
| return Promise.reject(new Error('BatchChonkVerifier stopped')); | ||
| // A stopped verifier has not checked the proof, so this is not a verdict on it either. | ||
| return Promise.reject(new ProofVerifierUnavailableError('BatchChonkVerifier stopped')); |
There was a problem hiding this comment.
(Written by Claude on behalf of Facundo)
This turns the stopped path into unavailable, but three paths still reject in-flight proofs with plain Errors. verifyProof turns those into valid: false, so gossip still penalises the peer:
Callers other than gossip also see the new rejection, where they used to get valid: false:
- The req/resp batch requester counts a rejected validation as invalid and penalises the peer (
batch_tx_requester.ts:517-533). - Block-proposal tx validation (
validateTxsReceivedInBlockProposal) throws instead of reporting invalid txs. TxValidationCachekeeps the rejected promise until the LRU evicts it, so the tx stays failed after the verifier recovers.
#383 covers all of these, so it's fine if the two land together. If #383 lags, the three conversions above are worth folding in here.
Follows #354 (merged), which introduces
ProofVerifierUnavailableError.The batch verifier at the p2p layer holds a long-lived
bbprocess with a session on it: the protocol circuit verification keys, a worker pool, and a FIFO for results. When that process dies, the FIFO closes, the verifier latches a fatal error, and it stays dead for the life of the node. Nothing clears the latch and nothing rebuilds the verifier, which is created once at startup.That is bad on its own, and worse than the equivalent on the RPC path.
verifyProofturned any failure intovalid: false, and this verifier's result feeds gossip validation asproofValidatoratPeerErrorSeverity.LowToleranceError. So a dead helper process made every gossiped transaction look like a bad proof, and the node then worked through its peer set penalising honest peers for its own fault.A failed verifier is replaced
SelfHealingChonkVerifierwraps the batch verifier and is what the node now creates. On the next verification after the verifier fails, it stops the failed one and creates a fresh one. Everything the verifier's session holds comes from configuration (ProtocolCircuitVks, the batch size, the core count), so a fresh one restores it exactly. Only proofs in flight are lost, and those were already rejected as unavailable.This is safe here in a way it would not be for Chonk accumulation, where the state being lost is the half-built proof itself. Here the session is really just process initialisation:
chonkBatchVerifierStartruns once insidenewand is never restarted, so a new verifier restores exactly what was there.Concurrent callers share one replacement. Attempts are paced at one a second, indefinitely rather than a bounded number of times, and flat rather than backing off; between attempts, verifications are rejected as unavailable. That is the convention
avm_simulator_poolsettled on, so a node recovers as soon asbbis healthy instead of waiting out a backoff it has already outlived.BatchChonkVerifieritself stays one session that never rebuilds, as on main. It changes only to report the proofs it could not check as unavailable, and to say whether it has failed. An earlier version rebuilt it in place, swapping a newbbsession into the fields of the dead one. That needed guards against the old session's FIFO reader, and a shutdown that waited for the rebuild, which could hang on abbthat never finished starting. Replacing the whole verifier needs neither.It stops calling an unavailable verifier an invalid proof
A verifier that could not check a proof has not judged it.
ProofVerifierUnavailableErrornow propagates rather than being flattened tovalid: false, and that covers proofs in flight when the session fails and proofsbbfails to take, not only calls made while it is down.Propagating it is not enough on its own, because gossip validation turns any thrown error into a rejection with a penalty. So the second (proof) stage of gossip validation catches it and returns
Ignore: the tx is dropped without penalising the peer, as the pool pre-check already does. The error moves tostdlib/errorsso p2p can recognise it. This is the same distinction the RPC verifier draws in #354, and here it is the difference between degraded verification and a self-inflicted partition.Shutdown
stop()stops the verifier in use and never waits for one that is still being created: that one stops itself as soon as it is ready. So abbthat is slow to start cannot hold up shutdown.Not done
The FIFO side channel stays. It exists because responses come back out of order, and bb.js's socket backend matches responses strictly in order by a callback queue with no ids, so there is nowhere to put them.
ipc-runtime's clients do carry request ids, on both the socket and shared-memory paths, so this becomes removable once bb.js moves onto them in aztec-packages#25362 — but not before, and not cheaply.Testing
self_healing_chonk_verifier.test.ts, against a fake verifier, covers:stop()stopping the verifier in use;stop()not waiting for a replacement still being created, which then stops itself.libp2p_service.test.tscovers an unavailable proof verifier: the gossiped tx is ignored, the peer is not penalised and nothing reaches the pool.BatchChonkVerifier, including draining proofs accepted beforestop().Not covered: a replacement after a real
bbdeath. That wants an integration test that kills thebbthe verifier spawned and verifies again, which needs the process id exposed.