Skip to content

fix(bb-prover): rebuild the batch verifier after its bb dies - #359

Open
charlielye wants to merge 6 commits into
mainfrom
cl/batch-verifier-selfheal
Open

charlielye wants to merge 6 commits into
mainfrom
cl/batch-verifier-selfheal

Conversation

@charlielye

@charlielye charlielye commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #354 (merged), which introduces ProofVerifierUnavailableError.

The batch verifier at the p2p layer holds a long-lived bb process 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. verifyProof turned any failure into valid: false, and this verifier's result feeds gossip validation as proofValidator at PeerErrorSeverity.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

SelfHealingChonkVerifier wraps 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: chonkBatchVerifierStart runs once inside new and 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_pool settled on, so a node recovers as soon as bb is healthy instead of waiting out a backoff it has already outlived.

BatchChonkVerifier itself 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 new bb session 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 a bb that 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. ProofVerifierUnavailableError now propagates rather than being flattened to valid: false, and that covers proofs in flight when the session fails and proofs bb fails 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 to stdlib/errors so 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 a bb that 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:
    • passing verifications through while the verifier is healthy;
    • replacing a failed verifier and stopping the failed one;
    • one replacement shared by concurrent verifications;
    • reporting unavailable, and pacing retries, when a replacement cannot start;
    • stop() stopping the verifier in use;
    • stop() not waiting for a replacement still being created, which then stops itself.
  • libp2p_service.test.ts covers an unavailable proof verifier: the gossiped tx is ignored, the peer is not penalised and nothing reaches the pool.
  • The existing batch verifier integration tests run against the main version of BatchChonkVerifier, including draining proofs accepted before stop().

Not covered: a replacement after a real bb death. That wants an integration test that kills the bb the verifier spawned and verifies again, which needs the process id exposed.

@greptile-apps

greptile-apps Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[High risk] Replaces batch verifier with self-healing wrapper that recreates it on failure.

The PR is not yet safe to merge because shutdown can turn an unverified gossiped proof into a peer-penalizing invalid verdict.

Fix All in Claude CodeFindings

  1. P1 Shutdown can penalize peers ▶
  2. P1 Shutdown can hang during rebuild ▶
  3. P2 Shutdown skips retiring verifier ▶
  4. P2 Recovery lacks regression coverage ▶
  5. P2 Rebuild uses promise callbacks ▶
  6. P2 Rebuild log lacks context ▶

Summary

The PR replaces the node’s peer batch verifier with a wrapper that recreates a failed bb session and distinguishes unavailable verification from an invalid proof.

  • Failed verifications can now trigger a shared, paced replacement.
  • Gossip ignores unavailable proof checks without penalizing the sender.
  • Shutdown avoids waiting for a replacement that is still starting.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Gossiped transaction] --> B[Self-healing verifier]
  B --> C{Current batch verifier failed?}
  C -->|No| D[Verify proof]
  C -->|Yes| E[Create shared replacement]
  E --> D
  E -->|Unavailable| F[Ignore without peer penalty]
  D -->|Invalid proof| G[Reject and penalize]
  D -->|Unavailable| F
Loading

Reviews (3) · Last reviewed commit: "fix(txe): stub SelfHealingChonkVerifier ..."

Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
@charlielye
charlielye force-pushed the cl/batch-verifier-selfheal branch 3 times, most recently from 4d9c4db to 0f8e6ea Compare October 1, 2026 14:30
@spalladino
spalladino added this pull request to stack #384 October 2, 2026 20:19
@charlielye
charlielye force-pushed the cl/batch-verifier-selfheal branch from 0f8e6ea to 9178d30 Compare October 5, 2026 08:23
@charlielye
charlielye force-pushed the cl/batch-verifier-selfheal branch 3 times, most recently from 5aef838 to 8620014 Compare October 5, 2026 09:19
Comment thread yarn-project/bb-prover/src/verifier/batch_chonk_verifier.ts Outdated
Base automatically changed from cl/bb-verifier-retryable to main October 5, 2026 10:50
charlielye and others added 3 commits October 5, 2026 11:50
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
@charlielye
charlielye force-pushed the cl/batch-verifier-selfheal branch from 8620014 to 8766b94 Compare October 5, 2026 10:50
charlielye and others added 2 commits October 5, 2026 10:58
…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
Comment on lines +55 to +56
const verifier = await this.liveVerifier();
return await verifier.verifyProof(tx);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

Fix in Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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.

Comment on lines +80 to +84
this.started = undefined;
if (verifier) {
void this.retire(verifier);
}
this.current = this.startReplacement();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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

Fix in Claude Code

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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
charlielye added a commit that referenced this pull request Oct 5, 2026
…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 fcarreiro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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, which liveVerifier() starts with void 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, in stop().
  • 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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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:

  • the per-request result timeout (:194)
  • the stop drain timing out (:258)
  • an error during stop (:262)

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.
  • TxValidationCache keeps 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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants