Skip to content

Commit 0898f4a

Browse files
committed
fix(fast-inbox): reconstruct each block's own L1-to-L2 message root from the blob (A-1384)
Post-flip the checkpoint blob carries a per-block L1-to-L2 message tree root, since any block within a checkpoint can insert messages. The archiver reconstruction still read the root once from the checkpoint's first block and applied it to every block, a stale pre-flip per-checkpoint assumption. Follower nodes (prover / sync-only) that rebuild a non-first message-inserting block via retrievedToPublishedCheckpoint therefore got the first block's root, so the world-state synchronizer inserted the correct message, recomputed the real root, and mismatched the reconstructed header (block state does not match world state), forking the node from the sequencer on the first message-consuming block. Reconstruct each block's tree root from its own blob data instead.
1 parent c32de25 commit 0898f4a

2 files changed

Lines changed: 26 additions & 7 deletions

File tree

yarn-project/archiver/src/l1/data_retrieval.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ describe('data_retrieval', () => {
7979
expect(reconstructedBlock1.body.txEffects.map(tx => tx.txHash.toString())).not.toEqual(
8080
reconstructedBlock3.body.txEffects.map(tx => tx.txHash.toString()),
8181
);
82+
83+
// Each block's L1-to-L2 message tree root must be reconstructed from its own blob data, not the
84+
// checkpoint's first block. Post-flip (AZIP-22 streaming inbox) any block can insert messages, so
85+
// intra-checkpoint blocks carry distinct roots; using the first block's root forks follower nodes.
86+
expect(reconstructedBlock1.header.state.l1ToL2MessageTree.root.toString()).toEqual(
87+
block1BlobData.l1ToL2MessageRoot.toString(),
88+
);
89+
expect(reconstructedBlock2.header.state.l1ToL2MessageTree.root.toString()).toEqual(
90+
block2BlobData.l1ToL2MessageRoot.toString(),
91+
);
92+
expect(reconstructedBlock3.header.state.l1ToL2MessageTree.root.toString()).toEqual(
93+
block3BlobData.l1ToL2MessageRoot.toString(),
94+
);
8295
});
8396

8497
it('handles single-block checkpoint', async () => {

yarn-project/archiver/src/l1/data_retrieval.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,22 @@ export async function retrievedToPublishedCheckpoint({
8383
.slice(1)
8484
.concat([archiveRoot]);
8585

86-
// An error will be thrown from `decodeCheckpointBlobDataFromBlobs` if it can't read a field for the
87-
// `l1ToL2MessageRoot` of the first block. So below we can safely assume it exists:
88-
const l1toL2MessageTreeRoot = blocksBlobData[0].l1ToL2MessageRoot!;
89-
9086
const spongeBlob = SpongeBlob.init();
9187
const l2Blocks: L2Block[] = [];
9288
for (let i = 0; i < blocksBlobData.length; i++) {
9389
const blockBlobData = blocksBlobData[i];
94-
const { blockEndMarker, blockEndStateField, lastArchiveRoot, noteHashRoot, nullifierRoot, publicDataRoot } =
95-
blockBlobData;
90+
// Post-flip (AZIP-22 streaming inbox) the blob carries a per-block L1-to-L2 message tree root: any block
91+
// within a checkpoint can insert messages, so reconstruction must use each block's own root rather than
92+
// the checkpoint's first block.
93+
const {
94+
blockEndMarker,
95+
blockEndStateField,
96+
lastArchiveRoot,
97+
noteHashRoot,
98+
nullifierRoot,
99+
publicDataRoot,
100+
l1ToL2MessageRoot,
101+
} = blockBlobData;
96102

97103
const l2BlockNumber = blockEndMarker.blockNumber;
98104

@@ -109,7 +115,7 @@ export async function retrievedToPublishedCheckpoint({
109115

110116
const state = StateReference.from({
111117
l1ToL2MessageTree: new AppendOnlyTreeSnapshot(
112-
l1toL2MessageTreeRoot,
118+
l1ToL2MessageRoot,
113119
blockEndStateField.l1ToL2MessageNextAvailableLeafIndex,
114120
),
115121
partial: PartialStateReference.from({

0 commit comments

Comments
 (0)