Skip to content

fix(validator): remove duplicate time-sensitive validation of block proposals - #25207

Merged
alexghr merged 2 commits into
merge-train/spartan-v5from
spl/a-1703-remove-duplicate-block-proposal-validation
Aug 14, 2026
Merged

fix(validator): remove duplicate time-sensitive validation of block proposals#25207
alexghr merged 2 commits into
merge-train/spartan-v5from
spl/a-1703-remove-duplicate-block-proposal-validation

Conversation

@spalladino

Copy link
Copy Markdown
Contributor

Removes the duplicate block-proposal validation that ran again downstream of p2p ingress, whose repeated wall-clock check could turn an honest, on-time proposal into a false slashable offense.

Context

On mainnet a validator node classified a canonical block proposal as a slashable invalid_proposal offense. Block proposals are validated at p2p ingress, where the receive-window check decides whether they arrived in time; ProposalHandler.handleBlockProposal then re-ran that same validation "out of caution" before re-execution. When node-local processing stalled (~50s in the incident), the repeated wall-clock check failed an on-time proposal, and invalid_proposal is in SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT — so local latency was converted into slashing evidence against an honest proposer.

Approach

Delete the downstream blockProposalValidator.validate(proposal) call and, with it, the now-unused BlockProposalValidator dependency of ProposalHandler (and its construction in ValidatorClient.new and createProposalHandler).

This is safe because every path into handleBlockProposal is post-ingress — gossiped block proposals and checkpoint-embedded blocks both pass ingress validation first, and there is no replay path — so the second pass added no coverage. The checks that remain downstream are deterministic properties of the signed payload rather than of the clock: the signature via getSender(), duplicate tx hashes, checkpoint/index consistency, in-hash agreement, embedded-tx integrity via tx collection, and full re-execution. The checkpoint-proposal path already operates without such a re-check. invalid_proposal stays in the slashable list, since it is still produced by the structural computeCheckpointNumber checks. The p2p package is untouched: ingress keeps its own validator.

Note that dropping the whole call also stops re-deriving the expected proposer downstream; that identity check likewise belongs to ingress, and the two validator.test.ts cases that pinned the old repeated-gate behavior were rewritten to assert the new contract (validates, no WANT_TO_SLASH_EVENT, slot not marked invalid).

This is the simpler alternative to #25201, which instead splits the validator into validate() / validateStableFields() and adds a non-slashable reason; the two are open in parallel for comparison.

Fixes A-1703

@github-actions github-actions Bot added the port-to-next Forward-port this merged PR into next label Aug 12, 2026
…roposals

Block proposals were validated twice: once at p2p ingress, and again at the
top of `handleBlockProposal` "out of caution". That second pass re-ran the
wall-clock receive-window check, whose outcome depends on when processing
starts rather than on anything the proposer did. On a node whose processing
stalled, an on-time canonical proposal was rejected as `invalid_proposal` —
a reason in the slashable list — manufacturing a false invalid-block offense
against an honest proposer.

Ingress is the arrival gate, and every path into the handler is post-ingress
(gossiped block proposals and checkpoint-embedded blocks alike), so the
duplicate pass added no coverage. What remains downstream are deterministic
properties of the signed payload: the signature via `getSender()`, duplicate
tx hashes, checkpoint/index consistency, in-hash agreement, embedded-tx
integrity from tx collection, and full re-execution.

Dropping the check makes the `BlockProposalValidator` dependency of
`ProposalHandler` unused, so it is removed along with its two construction
sites.
@alexghr
alexghr merged commit a5eeac4 into merge-train/spartan-v5 Aug 14, 2026
12 checks passed
@alexghr
alexghr deleted the spl/a-1703-remove-duplicate-block-proposal-validation branch August 14, 2026 09:47
AztecBot pushed a commit that referenced this pull request Aug 14, 2026
@AztecBot

Copy link
Copy Markdown
Collaborator

❌ Failed to cherry-pick to next due to conflicts. (🤖) View backport run.

spalladino added a commit that referenced this pull request Aug 14, 2026
…osal type (#25222)

Follow-up to #25207. That PR removed the duplicate downstream
re-validation of inbound block and checkpoint proposals, leaving p2p
ingress (gossipsub topic validation) as the single place proposals are
validated. The downstream handlers document that precondition in prose;
this PR enforces it with the type system.

- Adds branded `ValidatedBlockProposal` and
`ValidatedCheckpointProposalCore` types (plus their minting functions)
in `stdlib/src/p2p/validated_proposal.ts`, following the existing
`Branded<T, Brand>` convention used by `BlockNumber` and friends.
- The p2p received-proposal callbacks (`P2PBlockReceivedCallback`,
`P2PCheckpointReceivedCallback`) and the downstream consumers
(`ValidatorClient.validateBlockProposal` / `attestToCheckpointProposal`,
`ProposalHandler.handleBlockProposal` / `handleCheckpointProposal`, and
the `Validator` interface) now take the branded types, so a raw inbound
`BlockProposal` / `CheckpointProposalCore` cannot reach them.
- The brands are minted only in `libp2p_service.ts`, at the three points
where the topic validator has already returned `Accept`: the
block-proposal topic path, the checkpoint-embedded block path
(`processBlock` is only set after Accept), and the checkpoint path.
- Purely a compile-time marker: no runtime validation is added and there
is no behavior change. The only non-type edits are in tests, which mint
validated proposals from constructed ones.

Related to A-1703.
aminsammara added a commit that referenced this pull request Aug 17, 2026
Promotes `v5-next` onto `v5` for the **v5.2.0** release.

Frozen at `ee5d2d367e` — the `v5-next` tip at cut time. Both
`merge-train/spartan-v5` and `merge-train/fairies-v5` are drained to
that same commit, so nothing staged is left behind.

### Testnet validation

This branch was cut at `a4db216abf`, which is byte-identical to the
`v5.2.0-nightly.20260815` tag — same commit, same tree
(`0b22572eace2419ede6bbab173514d6965d4e0d0`). That nightly's CI3 run is
green, its artifacts are published to npm and Docker Hub, and it has
been **running healthily on testnet since 2026-08-15**:

| Component | Image / reported version |
|---|---|
| validators, prover node, prover broker, prover agents (`testnet` ns) |
`aztecprotocol/aztec:5.2.0-nightly.20260815` |
| `v5.testnet.rpc.aztec-labs.com` | `nodeVersion=5.2.0-nightly.20260815`
|
| `canonical.testnet.rpc.aztec-labs.com` |
`nodeVersion=5.2.0-nightly.20260815` |

Chain advancing normally, no prunes or reorgs, no WARN/ERROR across node
pods since rollout.

The branch has since been fast-forwarded to `ee5d2d367e` to pick up
#25242 (configurable RPC server HTTP timeouts and CORS allowed-headers).
That is the only delta from the soaked tree — 8 files, +131/−10, no
nightly covers it yet. Its config defaults were checked against Node's
built-ins (`keepAliveTimeout` 5000 ms, `headersTimeout` 60000 ms) and
match exactly, and the CORS default path resolves to the same `cors()`
call as before, so a node that sets none of the new env vars behaves
identically. It touches no circuits, protocol contracts, or generated
constants.

### Manifest

`.release-please-manifest.json` reads `5.2.0` on this branch, which is
the released version — correct as-is, no change needed in this PR.
`v5-next` moves to `5.3.0` separately in #25240; this branch is frozen
and cannot pick that up, so the two can merge in either order.

`v5` is a strict ancestor of `v5-next` this cycle, so there was no
manifest conflict to pre-resolve.

### Scope

139 commits (86 non-merge, 23 PR-level) spanning 2026-07-14 to
2026-08-17.

| Area | Theme |
|---|---|
| `prover-node` / `prover-client` | Epoch-proving robustness:
retry-to-converge with failure declared only at submission-window
expiry, ticker-driven expiry, per-checkpoint post-mortem upload,
checkpoint-only re-proving, prune-induced fault handling (#24678,
#24982, #24983, #24990, #25027) |
| `pxe` | Sync performance: hash-pinned node read cache (#24969),
anchor-bounded tag log caching (#25074), note/event validation from
cached tx data (#25076), constrained tag sync (#24275), sender tagging
finalization from log blocks (#25045) |
| `p2p` / `validator-client` | Gossip tx validation no longer stalls
behind tx-pool finalization (#25148); startup fails when p2p fails to
start (#25177); slashing/proposing/health/`sendTx` gated on p2p
connectivity (#25185); duplicate time-sensitive proposal validation
removed (#25207); `ValidatedProposal` branding (#25222) |
| `ethereum` / `aztec-node` | L1 watchers poll `getLogs` instead of
`eth_newFilter` (#25176); block stream sync no longer resolves against
an earlier pass (#25206) |
| `stdlib` / `foundation` | Deserialization bounds hardening (#25026,
#25028, #25029, #25109); checkpoint block-shape and block-count
validation (#25229); JSON-RPC cookies (#25231) |
| `archiver` | Removed-block cleanup and ownership-checked tx-effect
deletes (#24765); L2→L1 witness from a single store snapshot (#24754) |
| `aztec` | Declarative deployment framework at `@aztec/aztec/deploy`
(#24685) |
| `slasher` | Own-validator slash-target warnings and metrics (#25058) |
| telemetry | JSON-RPC metrics (#25159) |
| JSON-RPC server | Configurable HTTP keep-alive / headers timeouts and
CORS allowed-headers, defaults preserving current behaviour (#25242) |
| toolchain | Noir bumped to `v1.0.0-beta.25` (#24907) |

**One breaking change**, inherited from the Noir bump: note types
declared directly inside a `contract` module must now be `pub`.
Everything else is additive or internal.

### Protocol constants

Built from source and compared against `v5` — a cache-free rebuild of
every protocol circuit with each ref's own nargo and bb, then
regeneration of `vk_tree.ts` and `protocol_contract_data.ts`:

- `vkTreeRoot` =
`0x2b3b6ea4412b9c8f6457a37f91a2870306f8641e07e16a49b68bda6f8bc02892` —
unchanged from v5.1.0
- `protocolContractsHash` =
`0x2c075866eafc88a1f6f9addc7e337c6e64e45d1cb7fd7c0d612ebcec72aab2ca` —
unchanged from v5.1.0

The Noir beta.24 → beta.25 bump does not reach the circuits: the release
build consumes the committed `pinned-build.tar.gz`, which is
bit-identical between v5.1.0 and this commit (blob `3bedcb1fd1…`), so
the protocol-circuit bytecode is frozen rather than recompiled. The 47
verification keys were recomputed locally from that pinned bytecode with
the cache disabled, and all 47 `check_pinned_vk` checks passed.
`protocolContractsHash` is likewise backed by the new
`pinned-protocol-contracts.tar.gz`, whose three artifacts were
byte-compared against the build. Both values were also confirmed inside
the published `@aztec/protocol-contracts` and
`@aztec/noir-protocol-circuits-types` packages for
`5.2.0-nightly.20260815`.

That makes v5.2.0 a drop-in upgrade against the current rollup rather
than a coordinated one.

✅ **Gate re-run against `ee5d2d367e` (the current head) and passed** —
both values reproduced exactly from a build at this commit, with
`vkTreeRoot` numerically evaluated rather than inferred.
`29556326ce..ee5d2d3` touches no `noir-projects/**`,
`l1-contracts/src/**`, `ConstantsGen.sol`, `constants.gen.ts` or
`constants.nr`. Scope of the check: it verifies that the pinned circuit
bytecode plus locally recomputed VKs agree with the pin — not that a
from-source recompile reproduces v5.1.0's bytecode.

<details>
<summary>Commits added after the original constants check at
2955632</summary>

`#25207` `#25222` `#25206` `#25185` `#25176` `#25163` `#25229` `#25159`
`#25162` `#25231` `#25224` `#25228` `#25230` `#25242` plus their merge
commits.
</details>

### Known gap

Migration notes carry entries under `## TBD` that arrived with the docs
baseline backport (#25017) and describe `next`-line changes not present
on the v5 line — protocol contracts removed from
`@aztec/noir-contracts.js`, and the `at(wallet)` → `withWallet(wallet)`
deprecation. Neither exists on this branch. There is also no `## 5.1.0`
heading, and the entries that ship in v5.2.0 sit under `## TBD` rather
than a version heading.

This ships in the release docs, so it is worth correcting on `v5-next`
and backporting to `v5` before the tag, rather than after.
spalladino added a commit that referenced this pull request Aug 18, 2026
Forward-ports the fixes merged into `merge-train/spartan-v5` over the
last two weeks onto the `next` line. One commit per original PR, each
carrying a `cherry picked from` trailer.

## Ported

- #25177 — fix(p2p): fail node startup when p2p service fails to start
- #25183 — feat(p2p): warn periodically while node has zero connected
peers
- #25202 — fix: spurious insufficient-funds rejection when simulating L1
calls
- #25207 — fix(validator): remove duplicate time-sensitive validation of
block proposals
- #25222 — refactor(p2p): brand ingress-validated proposals with a
ValidatedProposal type
- #25229 — fix: checkpoint block-shape and block-count validation gaps
- #25206 — fix: block stream sync can resolve against a pass that
predates it
- #25185 — feat(p2p): gate slashing, proposing, health, and sendTx on
p2p connectivity
- #25176 — fix: poll getLogs instead of eth_newFilter for L1 event
watchers

Commits are ordered as they merged into the v5 line, since several build
on each other (#25177#25183#25185 and #25207#25222#25229).

## Not ported

- #25148 (gossip tx validation stalls behind tx pool finalization) —
already on this line via #25189, which covers the same ground. The
cherry-pick came out empty.
- #25155 (standard-contracts historical artifacts in release image) —
the whitelist entry is already present in
`release-image/Dockerfile.dockerignore`.

## Conflict resolutions worth a look

Four commits conflicted; all others applied clean.

- **`ethereum/src/l1_tx_utils/l1_tx_utils.ts`** (#25202) — the rename
#25202 called out in its own description: `getGasPrice` is
`getFeesPerGas` on this line. Took the fix's side (fee fields dropped
from the simulated call), so the local `feesPerGas` read is gone along
with them.
- **`archiver/src/modules/data_store_updater.ts`** (#25229) — the v5
version of this method also passes `maxBlocksPerCheckpoint:
MAX_CAPACITY_BLOCKS_PER_CHECKPOINT` on the ingest path, which is a
separate v5-only change that has not been ported. Only #25229's actual
change is taken here: the shared `validateOpts` object plus
`allowEmptyNonFirstBlocks: true`. Ingest on this line therefore still
validates against the attestable limit; raising it to the capacity limit
is a separate port.
- **`ethereum/src/contracts/slashing_proposer.ts`** (#25176) —
`listenToVoteCast` carries a `voteIndex` argument on this line (from
#25068), which the v5 version predates. Combined both: the new
`watchContractEvent` polling mechanism with the `voteIndex` callback
argument retained.
- **Import blocks** (#25185) — `safe_json_rpc_server.test.ts` keeps
`import http` as a value import, since #25243 added a runtime
`http.createServer()` call to that file.
`stdlib/src/interfaces/p2p.test.ts` takes only `P2PConnectivity`; the
`TxHash` / `GetTxByHashOptions` imports on the v5 side belong to v5-only
API methods and would be unused here.

## Verification

`yarn build` produces a byte-identical error set to
`origin/merge-train/spartan` built in the same working tree (46 errors,
all from stale cross-line generated artifacts locally — Noir circuit
artifacts, verification-key lengths, `withWallet` on contract types). No
new compile errors from the port.

Unit tests for every package with a hand-resolved conflict pass:
`stdlib` (52), `ethereum` (61), `foundation` (47), `slasher` (140),
`validator-client` (39). Two suites
(`archiver/data_store_updater.test.ts`,
`sequencer-client/checkpoint_proposal_job.test.ts`) fail to *load* in
this working tree on stale cross-line artifacts; the archiver one was
confirmed to fail identically with the base version of the file, so CI
is the first real run for those two.

Labeled `ci-no-squash` to preserve one commit per ported PR.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

port-to-next Forward-port this merged PR into next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants