feat(pxe): hash-pinned node read cache (backport #24969) - #25114
Merged
Merged
Conversation
…bmission-window expiry Decouple "a checkpoint prover failed" (a fact) from "the epoch failed" (a decision). A proving or L1-submission fault now settles the EpochSession in the non-declaring terminal 'stopped' instead of 'failed'; the reconciler rebuilds the epoch over current canonical content each tick (retry-to-converge), cheap because the broker reuses already-completed sub-proofs. An epoch is declared terminally failed — with its post-mortem upload — only when its L1 proof-submission window closes with the proven tip settled and the epoch still unproven, from ProverNode.expireEpoch. This removes the racy, lagging-replica "was this a prune?" classification entirely. Deletes lastTickEpoch (the epoch-keyed anti-retry gate), the checkpointsMatch upload-suppression in SessionManager.runSession, and the onSessionFailed callback. The post-mortem upload moves to tryUploadEpochFailure(epoch, checkpoints), built from the store's last-known canonical provers.
…fault path directly Address review feedback on the retry-to-converge change: - checkEpochExpiry was called from both handleBlockStreamEvent and the periodic ticker, so two sweeps could interleave and both upload a post-mortem for the same epoch before either advanced lastExpiredEpoch. Drop the inline block-stream call: the ticker (a RunningPromise, which never overlaps its own runs) is now the sole driver, so the high-water mark advances — and each epoch uploads — exactly once. Expiry is a background sweep keyed off the archiver's synced slot; it never needed to be on the event path. The A-1041 tips-unadvanced guard now covers only the registration/prune handling that genuinely needs it. - Add a checkpoint-prover test for the actual data-plane race: dbProvider.fork rejecting mid-proof rejects whenBlockProofsReady(), which the EpochSession maps to 'stopped'. Point the expiry unit tests at checkEpochExpiry directly rather than through a block-stream event.
The expiry sweep no longer runs from handleBlockStreamEvent. Rename "Per-event expiry sweep" to "Periodic expiry sweep", redraw the diagram around the expiryTicker (RunningPromise) as the sole driver, and fix the prose: the high-water mark advances per sweep (not per event) and is seeded from resolveLastFullyProvenEpoch. Drop the stale getCheckpointsData and computeStartupState references (expireEpoch uses getBlocks; there is no computeStartupState).
… re-proved every tick Retry-to-converge was naively per-tick: for an epoch that keeps failing, every tick cleared the stopped session and re-created a fresh one, re-running proving work until the deadline for no benefit. Key the retry off content instead, per the original design: record the content key of a full session that ends in 'stopped', and have the tick skip an epoch whose current canonical content matches an already-failed attempt. Recovery is unaffected — it flows through the ungated checkpoint/prune triggers, which fire on a genuine change (a re-add or reorg, including an identical-content re-add whose world-state has resettled) and reopen the epoch regardless. The gate resets when the epoch is proven, expires, or the proven frontier passes it. Only full sessions are affected: partials are opened solely by an explicit startProof and are never reopened by the tick or by events, so they never entered the re-spin loop.
…er mark Replace the content-keyed retry gate (a per-epoch content-key map plus two helpers and record/clear bookkeeping) with the monotonic lastTickEpoch high-water mark: the tick opens an epoch once and does not re-create a session for it every tick. Recovery from a genuine change still flows through the ungated checkpoint/prune triggers, so a pruned-then-re-added epoch recovers exactly as before. The tradeoff — a transient failure on an already-complete epoch waits for the deadline rather than being auto-retried by the tick — is unchanged from the content-keyed version, at a fraction of the machinery.
…nt prover Replace the lastTickEpoch high-water mark with a check at the point of construction: a CheckpointProver whose block proofs rejected for a non-cancel reason (a sub-tree fault or a prune-induced fork fault) now records isFailed(), and the SessionManager refuses to open (or rebuild) an EpochSession over any set that contains a failed prover. A stuck epoch is therefore skipped cheaply each tick — no session, no re-proving — rather than being gated by per-epoch bookkeeping. This keeps the resiliency and drops the tick gate: a pruned/re-added epoch recovers because the re-add installs a fresh (non-failed) prover, and a session that stops with healthy provers (a transient top-tree/submit error) is still retried by the next tick. The failure lives on the prover, where it happened, with the store as the single source of truth.
…upload eagerly, not at expiry
The expiry-time post-mortem upload could never fire for a persistently-failing epoch:
by the time its window closes, its checkpoint provers have been pruned, so there was
nothing to upload (the upload_failed_proof e2e test hung as a result).
Give EpochSession a genuine-failure state, told apart by the checkpoint provers'
isFailed() flag:
- a fault while a prover under it failed → 'stopped' (maybe a prune): not uploaded,
not retried over the failed prover, recovered on re-add.
- the session's own top-tree/submit work failed while every prover was healthy →
'failed' (hasFailed()): definitively not a prune, so it is race-free. The reconciler
retains such a full session (so the tick doesn't re-prove a deterministic failure)
and uploads a post-mortem once, eagerly, from the session's checkpoints.
Removes the fail-at-expiry upload (expireEpoch is back to chonk-release + reap only);
reinstates the onSessionFailed → tryUploadEpochFailure wiring on the genuine-failure
path. Reverts the e2e test to warp-to-epoch-1 + the eager upload trigger.
…st per failed session A checkpoint prover that fails to produce its block proofs (a sub-tree fault or a prune-induced fork fault) now fires an onFailed callback, and ProverNode uploads a snapshot for that single checkpoint via tryUploadCheckpointFailure. This captures a genuine checkpoint proving failure that ends its session in 'stopped' — which the session-level upload (only on a session's own 'failed') deliberately does not cover. The checkpoint upload fires for prune-induced faults too, on purpose: a prune-caused checkpoint snapshot is harmless, and not trying to tell prune from genuine failure is what keeps it race-free. A cancelled prover (control-plane prune / shutdown) is not a failure and does not upload.
…kpoint upload Add rerunCheckpointProvingJob: reuses the epoch rerun's offline setup (world state + archiver snapshot, local broker/prover, replaying tx provider) but rebuilds just the one checkpoint's sub-tree prover and awaits its block proofs — no epoch top-tree or L1 submit. Extract the shared setup into createRerunContext / buildCheckpointProver. Add a test-only checkpointProveOverride hook (CheckpointProverDeps → CheckpointStore setTestHooks → ProverNode.setCheckpointHooks) so a test can force a sub-tree failure, mirroring the existing session topTreeProveOverride hook. Extend upload_failed_proof.e2e with a second test: force a checkpoint sub-tree failure, capture the eager per-checkpoint upload URL via tryUploadCheckpointFailure, download, and re-prove that single checkpoint with rerunCheckpointProvingJob.
Merges the current `v5-next` tree (`c11bc68`) back into `v5` and bumps the release-please manifest to `5.0.1` to cut the v5.0.1 patch release. - `.release-please-manifest.json`: `5.0.0` → `5.0.1` - Brings all commits on `v5-next` since `v5.0.0` onto `v5` (companion change: `v5-next` manifest bumped to `5.1.0` in `ee3716277a`). **Merge strategy:** this is a release-branch sync — merge so `v5` remains a true superset of the history (merge commit or fast-forward). A squash merge would collapse the whole tree into a single commit and rewrite the SHAs, so avoid it here. Tagging `v5.0.1` on the resulting release commit is a follow-up step.
…cle flags; drop public isCompleted completed/failed/cancelled are three orthogonal facts, not a single status — a prover can be completed+cancelled (routine teardown) or completed+failed (enqueued then the sub-tree faulted); only failed+cancelled is excluded. Add a comment explaining why they aren't one enum, with per-field docs. isCompleted() had no callers outside tests (internally the `completed` field is used directly), so remove the public getter and the two secondary test assertions that used it.
…ke-epoch-proving-robust-to-prune-induced-fork
…l/a-1418-prover-node-make-epoch-proving-robust-to-prune-induced-fork
…s executed A mainnet sequencer kept signalling a governance payload whose proposal had already been executed, wasting ~100k gas per slot on a signal the canonical rollup rejected. Three fixes: - Fix the `ProposalState` enum, which was missing `Droppable` (Solidity `IGovernance` has 9 states). The mismatch made Solidity `Expired` decode as out-of-range and throw in `asProposalState`, so the publisher failed open and signalled anyway. Add an explicit `LIVE_PROPOSAL_STATES` set for the sweep. - Replace `hasActiveProposalWithPayload` (boolean) with `getPayloadProposalStatus` returning `'live' | 'executed' | 'none'`, matching a proposal by its stored payload directly or via its GSEPayload wrapper. The publisher now stops signalling an executed payload (memoized in-process, live takes precedence over executed) unless `GOVERNANCE_PROPOSER_FORCE_PAYLOAD_VOTE` is set, for payloads designed to be re-executed. - Add a canonicality guard: resolve the canonical rollup instance once and skip the signal when the configured rollup is not canonical, reusing that instance for round accounting and the EIP-712 digest so the read cannot race a canonical-rollup change.
…ser API Resolve the canonical rollup via getRollupAddress() instead of the getInstance() wrapper, dropping the now-unused instance parameter threaded through createSignalRequestWithSignature. Since the guard returns early when the configured rollup is not canonical, round accounting and the EIP-712 digest can just use the configured rollup address. Also drop the redundant sawExecuted flag from getPayloadProposalStatus (the executed verdict is already memoized in the set).
getL2ToL1MembershipWitness assembled its witness from several non-atomic archiver reads (getTxEffect, then the epoch blocks, target block and checkpoint metadata read inside computeL2ToL1MembershipWitness). A store commit landing between those reads could splice together two different chain states and surface a spurious "message does not exist" error even when the tx-effect index was healthy. Wrap the witness assembly in a single store.transactionAsync so every archiver read binds to one write transaction and observes a consistent snapshot (the store serializes writers, so no commit can interleave). The L1 Outbox roots fetch stays outside the transaction to avoid holding the archiver's writer lock across a network round-trip, and the tx effect is re-read inside the snapshot so the receipt's block number and tx index stay consistent with the block data. Add a kv-store test asserting reads inside a transaction see a consistent snapshot while a concurrent write is queued behind it.
…ed elsewhere BlockStore.removeBlocksAfter had two defects that corrupt the tx-effect index (txHash -> block position) when the same tx exists in two stored blocks, e.g. re-included after its original proposal expired: - deleteBlock removed #txEffects entries blindly by txHash, destroying the entry of a tx whose index already points at another stored block, which makes that block unreadable. - removeBlocksAfter skipped cleanup entirely for blocks it could not reconstruct, leaking their row, tx effects, and indices; a later insert at the same number then overwrites the row in place, leaving stale tx-effect entries pointing into the new chain. A stale entry makes getL2ToL1MembershipWitness resolve the tx at wrong coordinates and throw 'The L2ToL1Message you are trying to prove inclusion of does not exist' for a message in a proven block, and lets getTxReceipt report a proven position the chain no longer has. Cleanup now works from the raw storage row (so unreadable blocks are still fully released) and only deletes tx-effect entries still owned by the block being removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oval The ownership-checked delete silently skipped entries owned by another block. That state (two stored blocks sharing a tx) should be unreachable for honest chains, so skipping it silently hides direct evidence of an upstream bug. Warn on both anomalies (foreign-owned entry and missing entry) and correct the comment that attributed the duplication to routine proposal expiry, which cannot produce it.
The lazy KZG singleton (getKzg) builds its precomputation tables synchronously on first use, blocking the event loop for ~2s locally and 12-15s under production CPU limits. On an RPC node the first use is the archiver reconstructing blobs or the proposal handler uploading them, right after a checkpoint arrives, so the stalled loop overruns the gossipsub mcache window and attestation forwarding is skipped. Warm it in the node factory alongside the bb.js singleton, before any subsystem runs, keeping the cost off the gossip path.
Move the trusted-setup load timing and logging out of the node factory and into getKzg itself, gated on an optional logger argument, using elapsedSync for the measurement. Keeps the factory call site to a plain getKzg(log) and makes the timing available to any warm-up caller.
… jobId uploadEpochProofFailure already prefixes the upload path with the epoch number, so the epoch in the jobId was redundant — and the epoch-only string dropped the per-upload uniqueness the original session.getId() UUID gave. Use each entity's own id instead: the session's id for a session (epoch) failure, and the prover's content-addressed id for a checkpoint failure. Drop the now-unused epoch param from tryUploadEpochFailure.
…25028) ## Context `SiblingPath.deserialize` read a 32-bit length prefix and then both pre-allocated an array of that size and looped that many times, with nothing tying the declared length to the bytes actually present. Its element reader was built on `Buffer.slice`, which clamps past the end of a buffer instead of throwing — so the loop never ran out of input and ran the full declared count regardless of how much data arrived. A 4-byte field was therefore enough to build a million-element path, or to walk a client's V8 heap into a fatal out-of-memory abort. That abort is not catchable, so a `try`/`catch` around RPC decoding does not contain it. This matters because sibling paths reach clients as RPC *outputs* (witness lookups, which the PXE fetches automatically during private execution), so it is the node that can crash the client, not the other way round. ## Approach Sibling paths now deserialize through `BufferReader.readVector`, which bounds the declared length three ways: an explicit `maxSize` (`MAX_SIBLING_PATH_LENGTH`, 128 — the deepest protocol tree is 42 levels and the stacked path for L2-to-L1 message inclusion spans four unbalanced trees, so real paths are well under it), a new check that the size does not exceed the bytes left after the prefix, and `readBytes`' own per-element range check. The last of those also closes the truncation half of the problem, where a path declaring more elements than its payload holds used to deserialize into undersized buffers instead of failing. The cap has to live in `foundation`, which cannot import `@aztec/constants` since `constants` depends on it, so a test in `stdlib` asserts the cap stays above every generated `*_HEIGHT` constant. Enumerating them rather than listing the deep ones means a future deeper tree fails that test instead of silently failing to deserialize. `deserializeArrayFromVector`, the primitive behind the old code, had exactly one caller and duplicated `readVector` minus all of those checks, so it is deleted rather than fixed. The remaining-bytes bound lands in `readVector` itself, which covers its other callers too: every element consumes at least one byte, so any size beyond the bytes remaining is unsatisfiable and can be rejected before allocating or looping. It was not exploitable there in the same way (its item deserializers all range-check as they read), but the guard makes the failure explicit and cheap rather than dependent on every element reader. That bound assumes a minimum of one byte per element, so I checked for anything that deserializes a count of sub-byte items: `BitVector` is the only bit-packed reader, and it converts its bit count to `ceil(length / 8)` bytes itself and reads through the range-checked `readBytes`, so it never reaches the bound with a bit-denominated length. Every other `readVector` element reader in the tree advances by whole bytes. One existing `readVector` test changed: it asserted that a 32-byte buffer declaring 65537 elements yields 65537 elements, using a deserializer that consumed no bytes. That amplification is what the bound removes, so the happy path is now exercised against a well-formed vector and the oversized case asserts the throw. Fixes A-1522
Two public node RPC inputs reached expensive work with no length cap.
`simulatePublicCalls` accepted an unbounded `overrides.publicStorage`
array (and an unbounded
`overrides.contracts` record). Each storage override becomes a leaf
inserted into a forked public
data tree before the simulated transaction runs, so a small request
could force a large amount of
Merkle work and memory that the transaction's gas limit does not meter.
`getCheckpointsData` range queries validated `limit` with only `min(1)`,
while the sibling
`getCheckpoints` capped it. The effective upper bound was the node's
contiguous checkpoint history
rather than a page size, so one request could ask the archiver to read
and serialize the whole chain.
## Approach
Both caps live in `stdlib/src/interfaces/api_limit.ts` next to the
existing `MAX_RPC_*` limits and are
enforced in the zod schemas, so they reject at the RPC boundary before
any handler runs:
- `MAX_RPC_PUBLIC_STORAGE_OVERRIDES_LEN` (200) and
`MAX_RPC_CONTRACT_OVERRIDES_LEN` (50) on
`SimulationOverrides.schema`. Internal callers of the override path use
a handful of entries at
most (`fastforwardContractUpdate` writes one delayed-public-mutable
value), so there is ample
headroom.
- `MAX_RPC_CHECKPOINTS_DATA_LEN` (200) on both range variants of
`CheckpointsQuerySchema`
(`{ from, limit }` and `{ fromSlot, limit }`). Checkpoint data carries
no attestations or block
bodies, hence a larger page than a full checkpoint response. The widest
internal caller is the
prover node's catch-up fetch, bounded by `(proofSubmissionEpochs + 1) *
epochDuration`.
The existing `MAX_RPC_TXS_LEN`, `MAX_RPC_BLOCKS_LEN` and
`MAX_RPC_CHECKPOINTS_LEN` page caps stay at
50 and are unchanged. Two ideas from earlier revisions were dropped:
raising those caps to 100 while
adding a tighter `MAX_RPC_HEAVY_LEN` for reads whose `include*` options
attach a tx body or proof to
every entry, which needed a refine on four schemas plus a divergent
default for `getPendingTxs`; and
capping the per-slot attestation arrays at the committee size, which is
not actually an upper bound
since equivocation can produce more than one attestation per validator
per slot.
Tests cover each cap at the schema level (`SimulationOverrides`,
`CheckpointsQuerySchema`) and as
round-trips through the JSON-RPC client and server in
`aztec-node.test.ts`.
Fixes A-1524
Fixes A-1523
## Summary - Backport the current `origin/next` docs baseline onto `v5-next`, replacing the stale `v4.3.0` docs config/snapshots with the current `v5.0.1` docs baseline. - Bring over generated API docs, versioned developer/operator snapshots, docs site tooling/assets, and source docs so a `v5.1.0` docs release starts from the current docs tree instead of the old release-line state. - Backport the `release-docs` and `release-network-docs` skill updates, including the explicit post-cut reconcile step against `origin/next`. - Rebase the PR onto the current `origin/v5-next` and backfill the docs changes that landed on `origin/next` after the initial PR snapshot. ## Notes - This intentionally does not change protocol/source files outside `docs/` and the two release-docs skill files. - The PR branch now matches current `origin/next` for `docs/`, `.claude/skills/release-docs/SKILL.md`, and `.claude/skills/release-network-docs/SKILL.md`, except for two deliberate v5 compatibility fixes: - `docs/examples/ts/tsconfig.template.json` uses `yarn-project/l1-artifacts`, because `l1-contracts/l1-artifacts` does not exist on `v5-next`. - `docs/examples/ts/recursive_verification/config.yaml` uses `barretenberg/ts`, because `barretenberg/ts/bb.js` does not exist on `v5-next`. - After this lands, the actual `v5.1.0` release still needs the normal release-docs flow: cut the `v5.1.0` snapshot from the release tag, then run the documented reconcile step for anything newer on `origin/next`. ## Testing - `cd docs && yarn install --immutable` - Completed in the original PR validation with non-fatal Yarn peer/native warnings, including an optional `unix-dgram` native build warning. - `cd docs && yarn build` - Passed after the latest docs backfill. - Docusaurus emitted the existing non-fatal SSG/broken-anchor warnings for copied operator pages and generated CLI references; the build completed successfully. - Verified the effective docs/release-skill tree against `origin/next`: only the two intentional v5 path compatibility fixes differ. - Verified merge-tree against current `origin/v5-next`: no conflicts. --- *Created by [claudebox](https://claudebox.work/v2/sessions/08b91d8285e61921/jobs/2) · updated by [claudebox](https://claudebox.work/v2/sessions/08b91d8285e61921/jobs/5) · group: `slackbot` · requested by Alejo Amiras · [Slack thread](https://aztecfoundation.slack.com/archives/C0B24G1GFGB/p1785179401873069?thread_ts=1785179401.873069&cid=C0B24G1GFGB)*
BEGIN_COMMIT_OVERRIDE feat(prover-client): stop duplicating broker job inputs/results in memory (A-1215) (#24990) fix(prover-client): don't retain inline job inputs in the facade without a failed-proof store (A-1517) (#25027) feat(prover-node): tear down checkpoint sub-tree once block proofs are ready (A-1213) (#24982) fix(stdlib): cap the unbounded arrays in Tx and HashedValues schemas (#25029) fix(foundation): bound deserialized sibling path and vector lengths (#25028) fix(rpc): cap simulation overrides and checkpoint queries (#25026) END_COMMIT_OVERRIDE
…ures (#25031) ## Context A selected proposer can attach signed txs to a block proposal that pass the proposal-level checks (each tx hash is listed in `txHashes` and each tx recomputes to its own hash) but fail minimum tx integrity validation — bad proof, metadata, size, data, or contract instances. Tx collection threw a bare `Error('Invalid tx detected')` out of the validator's block-received callback before `handleBlockProposal` could return a typed result, so `validateBlockProposal` never reached the branch that emits `BROADCASTED_INVALID_BLOCK_PROPOSAL` and marks the slot invalid. The proposer avoided invalid-block accountability, and observers missed the invalid slot that the delayed attested-to-invalid-proposal watcher relies on. The same callback rejection also leaked tx-pool protections: `processValidBlockProposal` protects the proposal's txs before awaiting the callback but only unprotected them when the callback *returned* false. ## Approach - `validateTxsReceivedInBlockProposal` now throws a typed `InvalidBlockProposalTxsError` carrying each offending tx hash and its validation reasons, instead of a bare error. - The proposal handler catches that error around tx collection (in a `collectProposalTxs` helper) and returns a new `invalid_embedded_txs` failure reason, which is in `SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT`, so both the validator and non-validator handlers emit the offense and mark the slot invalid — still gated on the escape hatch being closed. Any other collection error keeps propagating: only proposer misbehavior gets a typed, slashable reason, so a local pool or network failure is not mistaken for an invalid proposal. The error crosses a package boundary, so it is matched with `isErrorClass` rather than `instanceof`, and sets its `name` accordingly. - `processValidBlockProposal` releases the protections it created when the callback rejects, not just when it returns false. A `tryBlockReceivedCallback` helper reports a throwing callback as a rejection — the proposal is no more usable than one explicitly rejected, and the error would otherwise escape with the protections still held — leaving one release path for both outcomes. The outer gossip handler only logged such errors anyway; one visible consequence is that an unexpected error while processing a checkpoint's embedded block no longer skips the checkpoint proposal itself. - A proposal that lists the same tx hash twice is rejected with a new slashable `duplicate_txs` reason. The repeated tx makes the block unbuildable (it would emit nullifiers the first copy already emitted), and tx collection reconciles a deduplicated hash set against the full list, so a duplicate that is not available locally threw the invariant error `Error collecting txs for proposal with N txs: found X and flagged Y as missing` out of `getTxsForBlockProposal` — the same unattributable-exception shape this PR removes. The check lives in the proposal handler rather than in gossip validation: a duplicate hash is proposer misbehavior, not a relaying-peer fault, so rejecting it at ingress would penalize the forwarding peer and short-circuit before the block-received callback, leaving the offense unrecorded. This mirrors how an over-limit `indexWithinCheckpoint` is handled. The handler runs the check before tx collection, so the invariant error is unreachable, and the terminal block carried in a checkpoint proposal goes through the same handler. Slashing here is attributable to the proposer. The proposal is validated for the right signing domain, slot window, and expected-proposer signature before any tx is looked at, and `validateTxs` requires every embedded tx to be listed in the signed `txHashes` with a self-consistent hash. `BlockProposal.getSender()` additionally returns undefined unless the embedded `SignedTxs` bundle shares the outer proposal's signature context and recovers to the same signer. A relaying peer can strip txs from the body, which surfaces as the non-slashable `txs_not_available`, but cannot graft foreign txs into an honest proposer's proposal. Two limitations worth noting, both pre-existing and left as-is: - `TxProvider.extractFromProposal` only considers hashes missing from the local pool, so a tx carried in the proposal whose hash is already pooled is never integrity-checked. Detection is therefore mempool-dependent. Closing that gap means proof-verifying every carried tx on the attestation path, which is a separate performance call. - `SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT`, `badProposalReasons` in `validator.ts`, and the metric label seeds in `metrics.ts` are three hand-maintained lists of the same taxonomy and have already drifted (`global_variables_mismatch` is slashable but counted as a node issue). The new reason is added to all three; consolidating them into one exhaustive record would be a good follow-up. Fixes A-1525
…targets (#25058) Closes A-1443. Reworked version of #24923 targeting the v5 line, addressing the review feedback there. Operators currently get no signal when the network starts voting to slash their validators — during the v5 inactivity-slashing incident, operators found out only after stake was lost. This PR makes the node detect when its own validators are targeted by onchain slashing, at two points in the lifecycle: - **Vote time (early warning)**: a new `OwnValidatorSlashMonitor` (following the `SlashRoundMonitor`/`SlashOffensesCollector` decomposition) subscribes to `VoteCast` events — only when the node runs validators — and warns on every vote that names one of them, with the running tally against the quorum: `Own validator 0x… targeted by slashing vote (7 of 65 votes needed to slash)`. Quorum needs a majority of a round's slots, so warnings start well inside the window an operator has to react. - **Round execution**: `handleRoundExecuted` filters the already-fetched `Slashed` events to own validators and emits a WARN with the exact slashed amount. No additional L1 reads. Five node-level metrics (no per-validator labels, so one series per node regardless of attester count): `aztec.slasher.own_validator.targeted_count`, `.current_round_votes_max` (highest tally against any committee position held by an own validator, reset each round), `aztec.slasher.quorum_size` to compare it against, and `.slashed_count` / `.slashed_amount` for executed slashes. The alert is `current_round_votes_max` approaching `quorum_size`; the WARN identifies which validator via a structured log field. Design notes: - **Votes are read by index, events are only triggers.** `VoteCast` carries no vote index, so the monitor keeps a per-round cursor: on each event it reads the round's `voteCount` and processes votes `[cursor, voteCount)` via a new `getVoteAt(round, index)`. Duplicate or batched deliveries are no-ops, backlogs are caught up by the next event, and a failed read is retried because the cursor only advances after a vote is successfully processed. - **All processing is serialized** through a single queue, with the round revalidated after every await, so concurrent event handlers cannot double-count, drop votes into the wrong round, or emit out-of-order tallies. `stop()` drains the queue and suppresses any late warns/metrics. - **Startup baseline instead of replay.** On start the monitor reads the current `voteCount` (before subscribing) and skips everything before it: replaying old votes would double-count cumulative counters across restarts. Votes cast while the node is offline or starting up are not counted, and L1 reorgs can cause small drift within a round — both documented on the metrics. - **The tally is per flattened committee position** because that is the unit the contract tallies quorum by: a validator sitting in several of the round's committees is named once per position by a single vote, each position racing quorum independently. Warnings and `targeted_count` remain per (vote, validator), reporting the validator at its highest position tally. - Own validator addresses were already passed to `createSlasherFacade` for `slashValidatorsNever`; they are now also threaded as `ownValidators` into `SlasherClient`, independent of `slashSelfAllowed`. `getSlashingAmounts` is memoized (the amounts are Solidity `immutable`), and `SlashVote` is renamed to the singular `SlashVoteTarget`. Testing: 22 unit tests for the monitor (per-vote warning with pinned message/context, per-position tallying, multi-validator nodes, round rollover in both directions and quiet-round reset, closed-round and foreign-validator votes, cursor semantics under duplicate/batched/missed events, failed-read retry, drain serialization, mid-drain rollover, startup baseline and its failure fallback, stop/restart lifecycle, subscription gating), 3 client wiring tests, and an anvil-backed `getVoteAt` decoding test in the ethereum package.
This happened due to a Noir version bump.
See [merge-train-readme.md](https://github.com/AztecProtocol/aztec-packages/blob/next/.github/workflows/merge-train-readme.md). This is a merge-train.
AztecBot
force-pushed
the
cb/backport-24969-v5-next
branch
from
August 5, 2026 14:06
ed6ca41 to
f1dd556
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
nchamo
marked this pull request as ready for review
August 5, 2026 14:11
nchamo
approved these changes
Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Backport of #24969 (
feat(pxe): hash-pinned node read cache) tov5-next, via thebackport-to-v5-next-stagingqueue as per.claude/claudebox/backport.md.Opened by hand after the automatic backport failed: run 31012053465.
Restructured to sync-first — the cherry-pick is now clean and there is no conflict resolution at all.
Why the automatic backport failed
The bot cherry-picks
1b345627e46031772edfe8edc383a9054f5fc097ontobackport-to-v5-next-staging, not ontov5-nextitself. It hit exactly one conflict:That is not a real code conflict — it is branch drift. #24969 edits that bench test, but the file did not exist on the staging branch: it was added to
v5-nextby #24275 (feat(pxe): constrained tag sync optimization and recipient logs sync benchmarks), which is inv5-nextbut not in staging.backport-to-v5-next-stagingwas 104 commits behindv5-next(merge basebb922d71857, with only #25089 unique to staging), so the cherry-pick was being applied to a tree that predated the file.Commit structure
chore: sync backport-to-v5-next-staging with v5-next— mergeorigin/v5-nextinto the staging branch. Clean merge, no conflicts.feat(pxe): hash-pinned node read cache (#24969)—git cherry-pick -xof the merge commit. Applies cleanly: all 16 files, +1205 / −487, byte-identical to the upstream commit's diff.There is no third commit: once the branch is synced with
v5-next, the cause of the conflict is gone.This also removes the follow-up hazard from the earlier version of this PR.
sync_tagged_private_logs.bench.test.tsis present here and carries the #24969 update, so it points atnode/benchmarked_node.js/withRecordingrather than the deletedcontract_function_simulator/benchmarked_node.js.Note that merging this PR also brings
v5-nextintobackport-to-v5-next-staging, which is the point of commit 1 — the staging branch stops being 104 commits stale.Verification
1b345627e46: identical content, differing only in blob hashes and hunk line offsets.contract_function_simulator/benchmarked_node,aztec_node_read_cache,BenchmarkedNodeFactory) anywhere inyarn-project.yarn-project/node_modulesabsent). Type-checking is left to CI.Created by claudebox · group:
slackbot· requested by Nico Chamo · Slack thread