feat(p2p): require the Inbox prefix reference on every block proposal - #184
Draft
spalladino wants to merge 2 commits into
Draft
spalladino wants to merge 2 commits into
spalladino wants to merge 2 commits into
Conversation
The signed Inbox message-prefix reference is now a required field on a standalone BlockProposal and on the final block embedded in a CheckpointProposal, serialized without a presence flag ahead of the optional SignedTxs bundle and always included in the signing payload. The end-of-buffer fallback is gone: a proposal that omits or truncates the reference is malformed input rather than a valid zero-message proposal. The final block's reference is checked against the checkpoint header's inboxRollingHash whenever a final block is present, and a zero-new-message block re-states the prefix its parent ended at instead of leaving the field unset. This is a coordinated P2P wire break: peers on the old optional-tail encoding write a buffer 32 bytes short of a valid proposal. Historical L1 blob replay is unaffected, since it never reads this encoding.
…t carries them Automine cannot spread a censorship-deadline backlog over more than one block, node-side public simulation predicts the next block's consumed messages from the local log alone, and the archiver's no-new-checkpoint short-circuit only compares the pending archive root. Each is explained where it lives, as a self-contained constraint rather than a tracker link.
spalladino
added this pull request to stack #188
September 12, 2026 04:52
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.
This is a coordinated P2P wire break. Every node on the network has to move together: a peer still writing the
previous optional-tail encoding produces a buffer that is 32 bytes short of a valid proposal, and a peer still
reading it decodes the new bytes as garbage. There is no negotiated fallback and none is wanted — the whole point is
that a proposal without an authenticated Inbox position is not a proposal.
Rung N8 of the Fast Inbox node port stack, on top of
spl/fi-n6-endpoint-gate. It reconstructs old PR #25445,commit
e95e15327482b5ad6edda9142f2ac398813145ce, which fully implements section 1 of the guide-review code-changeplan of 2026-09-09.
What changes
The signed Inbox message-prefix reference (
InboxMessagePrefixRef) becomes a required field on a standaloneBlockProposaland on the final block embedded in aCheckpointProposal. N3a introduced it as an optional tail withan end-of-buffer-means-unset decode, explicitly as a staging shape; this rung finishes the job.
The presence flag and the EOF fallback are gone. A proposal that omits or truncates the reference is now malformed
input, rejected by the decoder, rather than a proposal that happens to have consumed no messages. Those two readings
were never distinguishable on the wire, which is exactly why the optional shape could not survive: silently treating
a stripped reference as "consumed nothing" hands a relay a way to turn an authenticated block into an unauthenticated
one.
A block that consumes no new messages now carries its real inherited prefix — it re-states the prefix its parent
ended at. The zero hash is a genuine value only for the empty global prefix a chain starts from.
Whenever a checkpoint proposal carries a final block, that block's reference is checked against the checkpoint
header's
inboxRollingHashat both construction and decode, so the checkpoint's consumed position is exactly itslast block's.
The validator's per-block streaming checks lose their old "check 1: reference present" step entirely. The wire format
now guarantees it, so a proposal missing the reference never reaches the check at all; the remaining three checks
renumber.
New byte layout
BlockProposal.toBuffer():CheckpointProposal.toBuffer(), whenhasLastBlockis 1:The reference sits immediately after the transaction hashes and immediately before the
hasSignedTxsflag, with nopresence flag of its own. The signing payload changes the same way:
The reference is unconditionally part of the payload, so a relay can neither strip it nor substitute it without
breaking signer recovery — and two attempts at the same block duty against different message prefixes are now
conflicting signatures, not duplicate ones.
Historical L1 blob replay is unaffected. Blocks reconstructed from L1 blob data never go through this encoding;
nothing about replaying past checkpoints is coupled to the P2P wire format, and nothing in this PR touches that path.
Source commits reconstructed
e95e153274— the whole of it, minus the exclusions below.Stack position
This is rung 21 of 25 in the Fast Inbox node stack, stacked on
spl/fi-n6-endpoint-gate.Tests
Run locally from
yarn-project, all green:yarn workspace @aztec-labs/stdlib test src/p2p/— 6 suites, 65 passed (base 59).yarn workspace @aztec-labs/stdlib test(full) — 132 suites, 1120 passed, 1 skipped.yarn workspace @aztec-labs/validator-client test— 11 suites, 317 passed, 3 skipped(base 319/3; three "no prefix reference" cases deleted because the state they describe is now undecodable, one HA
conflict case added).
yarn workspace @aztec-labs/sequencer-client test— 12 suites, 305 passed, 1 skipped (base parity).yarn workspace @aztec-labs/p2p test— 75 suites, 1459 passed.yarn workspace @aztec-labs/archiver test— 23 suites, 682 passed.yarn build,yarn format,yarn lint— all clean.Red/green was taken on the wire cases first: with the new tests in place against the old encoding, nine cases failed
across the two p2p suites — the pinned block and checkpoint bytes, both byte-layout assertions placing the reference
before the
hasSignedTxsflag, rejection of a buffer written without the reference for both types, rejection atdecode of an embedded reference that disagrees with the checkpoint header, and the two constructor-order cases
including the tampered-reference signature check. All nine pass after the change.
Two further cases are regression pins, not red/green proof. The generic truncated-buffer rejection already passed
under the old encoding because its tail decoder also ran off the end. The zero-new-message round trip also passed when
the optional reference was set explicitly. They remain useful coverage, but they do not add to the nine observed
failures above.
E2E suites were not run locally and are left to CI. The only e2e file touched is
e2e_multi_validator_node_key_store.test.ts, whosecreateBlockProposaloverride needed the new parameter.Deliberately left out
CheckpointProposal.getSize()withthis.toBuffer().lengthand added a "reports the actual serialized size" testmatrix; this repository deliberately has neither, because
getSize()still accounts 8 bytes for afeeAssetPriceModifierthatserializeSignedBigIntwrites as 32. That under-reports a checkpoint proposal byexactly 24 bytes (measured: 1132 vs 1156). Left exactly as found — only the prefix-reference term of the sum was
touched.
BlockProposal.getSize()is exact and is asserted againsttoBuffer().lengthin the ported tests.comments instead explain the constraints directly: automine can stall on a censorship-deadline backlog larger than
the per-block cap; node-side public simulation predicts the next block's consumed messages from the local log
alone; and the archiver's no-new-checkpoint short-circuit compares only the pending archive root.
docs/docs-developers/.../migration_notes.mdanddocs/docs-operate/.../changelog/v6.md, owned by theaztec-packagesdocumentation follow-up.validator-client/README.md, which the source commit edits but which does not exist in this repository.DutyBudget, nounverifiablestatus or sentinel encoding 9,no
R − Psend cutoff, no generic Rollup artifact library map, no override-type cleanup, no proof-lifetime joins.Audited by grep over the full branch diff.
Notes for review
The wire fixtures in
stdlib/src/p2p/wire_compat_fixtures.tswere regenerated for this repository rather than copiedfrom the source: the destination's block header carries a
tx_effects_tree_rootfield the source's did not, so thesource's hex is the wrong length here. The previous
LEGACY_*constants are kept under newPRE_REQUIRED_PREFIX_*names, with their bytes unchanged — they are now the negative fixtures proving the oldencoding is rejected, which is a stronger use than the compatibility assertion they used to serve.