feat(p2p): require the Inbox prefix reference on every block proposal - #25445
Draft
spalladino wants to merge 1 commit into
Draft
feat(p2p): require the Inbox prefix reference on every block proposal#25445spalladino wants to merge 1 commit into
spalladino wants to merge 1 commit into
Conversation
spalladino
added this pull request to stack #25441
September 10, 2026 03:35
spalladino
requested review from
IlyasRidhuan,
LeilaWang,
iAmMichaelConnor,
iakovenkos and
ledwards2225
as code owners
September 10, 2026 04:04
spalladino
force-pushed
the
spl/fi2-required-inbox-prefix
branch
from
September 10, 2026 04:04
7341b54 to
230668b
Compare
spalladino
force-pushed
the
spl/fi2-required-inbox-prefix
branch
2 times, most recently
from
September 10, 2026 05:41
2f815c7 to
477b650
Compare
spalladino
removed this pull request from stack #25441
September 10, 2026 11:49
spalladino
added this pull request to stack #25448
September 10, 2026 11:49
spalladino
removed this pull request from stack #25448
September 10, 2026 12:09
spalladino
changed the base branch from
spl/fi2-f15-authenticate-recovery-anchor
to
spl/fi2-f14-l1-endpoint-check
September 10, 2026 12:10
spalladino
changed the base branch from
spl/fi2-f14-l1-endpoint-check
to
spl/fi2-f15-authenticate-recovery-anchor
September 10, 2026 12:10
spalladino
added this pull request to stack #25449
September 10, 2026 12:11
spalladino
force-pushed
the
spl/fi2-required-inbox-prefix
branch
from
September 10, 2026 12:51
477b650 to
e4bee5f
Compare
spalladino
removed this pull request from stack #25449
September 10, 2026 13:04
spalladino
changed the base branch from
spl/fi2-f15-authenticate-recovery-anchor
to
spl/fi2-f14-l1-endpoint-check
September 10, 2026 13:05
spalladino
changed the base branch from
spl/fi2-f14-l1-endpoint-check
to
spl/fi2-f15-authenticate-recovery-anchor
September 10, 2026 13:05
spalladino
added this pull request to stack #25451
September 10, 2026 13:05
spalladino
force-pushed
the
spl/fi2-required-inbox-prefix
branch
from
September 10, 2026 13:52
e4bee5f to
c46e794
Compare
spalladino
removed this pull request from stack #25451
September 10, 2026 13:53
spalladino
changed the base branch from
spl/fi2-f15-authenticate-recovery-anchor
to
spl/fi2-f14-l1-endpoint-check
September 10, 2026 13:54
spalladino
changed the base branch from
spl/fi2-f14-l1-endpoint-check
to
spl/fi2-f15-authenticate-recovery-anchor
September 10, 2026 13:54
spalladino
added this pull request to stack #25452
September 10, 2026 13:54
spalladino
removed request for
IlyasRidhuan,
LeilaWang,
iAmMichaelConnor,
iakovenkos and
ledwards2225
September 10, 2026 14:22
spalladino
marked this pull request as draft
September 10, 2026 14:22
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. Also links the three deferred backlog limitations from the code that still carries them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
spalladino
force-pushed
the
spl/fi2-required-inbox-prefix
branch
from
September 10, 2026 14:30
c46e794 to
e95e153
Compare
spalladino
removed this pull request from stack #25452
September 10, 2026 14:31
spalladino
changed the base branch from
spl/fi2-f15-authenticate-recovery-anchor
to
spl/fi2-f14-l1-endpoint-check
September 10, 2026 14:31
spalladino
changed the base branch from
spl/fi2-f14-l1-endpoint-check
to
spl/fi2-f15-authenticate-recovery-anchor
September 10, 2026 14:31
spalladino
added this pull request to stack #25453
September 10, 2026 14:32
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.
Background
The Inbox is the L1 contract that queues L1-to-L2 messages. An L2 block consumes a prefix of that queue: not a fixed
batch, but however many messages the proposer's node had observed by the time it built the block. A block therefore
has to state where in the message sequence it stopped. Its header carries the count, and its gossiped proposal carries
a second field,
InboxMessagePrefixRef— the rolling hash over exactly that many messages. Each message's rollinghash chains the previous one, so a validator holding the same hash at the same count holds the same messages, and can
read the block's message bundle out of its own store instead of trusting a list the proposer sent.
Both halves of that pair have to be signed, or a relay could move the block to a different set of messages. The count
is in the signed header. The hash is in the proposal.
The problem
The reference was encoded as an optional tail, after the optional transaction bundle, behind its own presence flag.
Take a proposal with two transaction hashes,
Fr(7)andFr(8), and no bundle. Its last bytes were:Cut the last 36 bytes and the buffer still decodes.
fromBufferreached the end after thehasSignedTxsflag,recorded no reference, and handed back a proposal that the type system said was perfectly well formed, because
inboxPrefixRefwasInboxMessagePrefixRef | undefined.That absent case then had to be handled everywhere downstream. The signing payload appended the reference only when
set, so an unset proposal signed a shorter payload. The validator's first streaming check existed only to reject a
proposal with no reference.
proposal.inboxPrefixRef!appeared in the re-execution classifier. The sequencer threw a"wiring bug" error before pushing a block to its own archiver. Every one of those is code guarding against a state the
protocol never wants a block to be in: even a block that consumes no new messages has a position, namely the one its
parent ended at.
This is not known to be exploited, and cutting the tail off someone else's proposal does not work as an attack: the
truncated proposal hashes a different signing payload, so signer recovery fails and the proposal is dropped. What
making the field required buys is that the absent case stops being representable at all. A proposal that omits or
truncates the reference is rejected by the decoder, before any of the checks that used to have to consider it.
What this changes
InboxMessagePrefixRefbecomes a required field on a standaloneBlockProposaland on the final block embedded in aCheckpointProposal, and it moves ahead of the optional transaction bundle. The presence flag is gone, so the tail ofthe same proposal is now:
A proposal that carries a reference is four bytes smaller than before, and
getSize()matchestoBuffer().lengthforevery combination. The reference is now always part of the signing payload rather than appended only when set, so
signer recovery, payload hashing, attestation-pool deduplication and the HA signer's duty protection all see it on
every proposal. Two attempts at the same duty with different prefix hashes are a slashing-protection conflict, not a
duplicate duty.
Where a final block exists, its reference is checked against the checkpoint header's
inboxRollingHash— atconstruction and, now that decoding builds the proposal through the constructor, on the wire too. A block that
consumes no new messages supplies the real unchanged prefix its parent ended at; the zero hash is only correct for the
empty prefix a chain starts from.
With the absent case gone, the guards against it go too: the validator's "reference present" check and its
inbox_prefix_unavailableverdict for a missing field, the non-null assertions in the re-execution classifier, theundefinedin the streaming check input type, and the sequencer's runtime check before pushing a proposed block tothe archiver.
What this does not do
This is a breaking wire-format change. A node on the old format cannot decode a proposal from a node on this one, and
the reverse fails too, since the old decoder reads the first four bytes of the rolling hash as a
hasSignedTxsflag.There is no mixed-version mode; the peer set has to be upgraded together. Both the operator changelog and the
developer migration notes say so.
The embedded last block itself stays optional. A
CheckpointProposalmay still carry no last block at all; the ruleis only that when there is one, it has a reference.
Historical blocks replayed from L1 blobs are untouched. The reference is peer-to-peer only and is not part of any
published block, so the archiver's L1 ingestion path takes no reference and is unchanged.
Nothing else moves: no L1 contract, no consensus policy, no proving variant, no recovery behaviour, no new slashing
offense. Local disagreement about a prefix stays non-punitive, since it can reflect an honest L1 fork.
Testing
New and updated unit tests cover both representations: round trips with and without a transaction bundle and with the
empty genesis prefix; the exact byte order, asserting the reference sits between the last transaction hash and the
hasSignedTxsflag; golden wire fixtures for the serialized proposal and the signing payload; rejection of a bufferwritten in the old encoding and of one whose reference is truncated; a tampered reference breaking signer recovery and
changing the payload hash; a well-formed checkpoint buffer whose last-block reference disagrees with the header being
rejected at decode; the embedded final block recovering the same signed block identity as a standalone one; and
getSize() === toBuffer().lengthacross bundle combinations. An HA test asserts that a second, different prefix hashfor the same block duty is refused as a slashing-protection conflict. The sequencer's existing streaming test already
asserts that a block consuming nothing re-signs its parent's prefix, and the test double for the checkpoint builder
now derives the header's
inboxRollingHashfrom the messages it was handed, so the header-versus-final-blockagreement is exercised by the whole sequencer suite rather than asserted in isolation.
Full-suite runs, from
yarn-project:@aztec/stdlib1100 passed,@aztec/validator-client336 passed,@aztec/sequencer-client293 passed,@aztec/aztec-node183 passed,@aztec/archiver639 passed (the archiverrunner exits 129 on SIGHUP with no failures, on untouched branches too).
@aztec/p2ppassed 1458 of 1458 on one run;a later run had
p2p_client.batch_tx_requester.bench.test.tstime out waiting for one of its 25 worker processes,which is load-related and unrelated to this change.
yarn formatandyarn lintboth exit 0.yarn buildexits 1, on one pre-existing error unrelated to this branch:the untracked
end-to-end/src/single-node/cross-chain/streaming_inbox_load.test.tsimportsgetInboxCutoffTimestamp,which no longer exists. That file is someone's in-progress work and was left alone. Every other package compiles. No
e2e tests were run.
Part of A-1928