Skip to content

Commit 4251f2a

Browse files
committed
chore(fast-inbox): delete legacy per-checkpoint and 128-bit message paths in the archiver (A-1388)
Removes the legacy getL1ToL2Messages(checkpointNumber) flow, the padded per-checkpoint index invariants, the inboxTreeInProgress readiness gate, and the L1ToL2MessagesNotReadyError. InboxMessage drops the 128-bit keccak rollingHash and the vacuous derived checkpointNumber, so messages carry only the compact global index and the full-width consensus rolling hash; the store serialization changes and ARCHIVER_DB_VERSION bumps to 9 (nodes resync, no migration). Reorg detection now compares the local consensus rolling hash and total against the Inbox's current rolling-hash bucket via new getBucket/getCurrentBucketSeq/getCurrentBucket wrappers, replacing the 128-bit getState comparison. Test fakes/mocks move to compact indexing.
1 parent 074d122 commit 4251f2a

17 files changed

Lines changed: 225 additions & 505 deletions

yarn-project/archiver/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ Two independent syncpoints track progress on L1:
4444

4545
### L1-to-L2 Messages
4646

47-
Messages are synced from the Inbox contract. The sync compares local state (message count and rolling hash) against the Inbox contract state on L1, downloads any missing messages, and verifies consistency afterwards. On success, the syncpoint advances to the current L1 block. On failure (L1 reorg or inconsistency), the syncpoint rolls back to the last known-good message and the operation retries (up to 3 times within the same sync iteration).
47+
Messages are synced from the Inbox contract. The sync compares local state (message count and consensus rolling hash) against the Inbox's current rolling-hash bucket on L1, downloads any missing messages, and verifies consistency afterwards. On success, the syncpoint advances to the current L1 block. On failure (L1 reorg or inconsistency), the syncpoint rolls back to the last known-good message and the operation retries (up to 3 times within the same sync iteration).
4848

49-
1. Query Inbox state at the current L1 block (message count + rolling hash)
49+
1. Query the Inbox's current bucket at the current L1 block (cumulative message count + consensus rolling hash)
5050
2. Compare local state against remote
5151
3. If they match, advance syncpoint and return
5252
4. If mismatch, fetch `MessageSent` events in batches and store them
@@ -55,7 +55,7 @@ Messages are synced from the Inbox contract. The sync compares local state (mess
5555
- If still mismatched (e.g., messages missed due to a concurrent L1 reorg), rollback and retry
5656
6. On success, advance the syncpoint
5757

58-
The syncpoint and the `inboxTreeInProgress` marker (which tracks which checkpoint's messages are currently being filled on L1) are updated atomically. The marker is only advanced after messages are stored, so concurrent reads don't see an unsealed checkpoint as readable before its messages are available.
58+
Messages are stored with compact (unpadded) global indices matching the Inbox's insertion order, and each carries the consensus rolling hash (a truncated sha256 chain) and the sequence of the Inbox bucket it was absorbed into (AZIP-22 Fast Inbox). Bucket snapshots let the sequencer and validator resolve message bundles per block.
5959

6060
### Checkpoints
6161

@@ -70,13 +70,12 @@ Checkpoints are synced from the Rollup contract via `handleCheckpoints()`:
7070
- Verify archive matches (checkpoint still in chain)
7171
- Validate attestations (2/3 + 1 committee signatures required)
7272
- Skip invalid checkpoints (see "Invalid Checkpoints" in Edge Cases)
73-
- Verify `inHash` matches expected value (see below)
7473
- Store valid checkpoints with their blocks
7574
6. Update proven checkpoint again (may have advanced after storing new checkpoints)
7675
7. Handle epoch prune if applicable
7776
8. Check for checkpoints behind syncpoint (L1 reorg case)
7877

79-
The `inHash` is a hash of all L1-to-L2 messages consumed by a checkpoint. The archiver computes the expected `inHash` from locally stored messages and compares it against the checkpoint header. A mismatch indicates a bug (messages out of sync with checkpoints) and causes a fatal error.
78+
L1 enforces at propose time that a checkpoint header's consensus rolling hash matches the Inbox bucket the checkpoint consumes through (AZIP-22 Fast Inbox), so the archiver does not re-derive or cross-check a per-checkpoint message hash while syncing.
8079

8180
The `blocksSynchedTo` syncpoint is updated:
8281
- When checkpoints are stored: set to the L1 block of the last stored checkpoint

yarn-project/archiver/src/archiver-sync.test.ts

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { sum, times } from '@aztec/foundation/collection';
1616
import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer';
1717
import { Fr } from '@aztec/foundation/curves/bn254';
1818
import { EthAddress } from '@aztec/foundation/eth-address';
19+
import { toArray } from '@aztec/foundation/iterable';
1920
import { type Logger, createLogger } from '@aztec/foundation/log';
2021
import { retryFastUntil } from '@aztec/foundation/retry';
2122
import { TestDateProvider } from '@aztec/foundation/timer';
@@ -36,7 +37,7 @@ import { type MockProxy, mock } from 'jest-mock-extended';
3637
import type { GetBlockReturnType } from 'viem';
3738

3839
import { Archiver, type ArchiverEmitter } from './archiver.js';
39-
import { BlockOrCheckpointSlotExpiredError, L1ToL2MessagesNotReadyError } from './errors.js';
40+
import { BlockOrCheckpointSlotExpiredError } from './errors.js';
4041
import type { ArchiverInstrumentation } from './modules/instrumentation.js';
4142
import { ArchiverL1Synchronizer } from './modules/l1_synchronizer.js';
4243
import { type ArchiverDataStores, createArchiverDataStores } from './store/data_stores.js';
@@ -186,6 +187,11 @@ describe('Archiver Sync', () => {
186187
await archiver?.stop();
187188
});
188189

190+
// Returns every stored L1-to-L2 message leaf (as hex), in insertion order (compact indexing, AZIP-22 Fast Inbox).
191+
const getStoredLeaves = async () =>
192+
(await toArray(archiverStore.messages.iterateL1ToL2Messages())).map(m => m.leaf.toString());
193+
const asHex = (leaves: Fr[]) => leaves.map(l => l.toString());
194+
189195
describe('basic sync', () => {
190196
it('syncs l1 to l2 messages and checkpoints', async () => {
191197
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(0));
@@ -220,7 +226,7 @@ describe('Archiver Sync', () => {
220226
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(1));
221227

222228
// Verify messages for checkpoint 1
223-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toEqual(msgs1);
229+
expect(await getStoredLeaves()).toEqual(asHex(msgs1));
224230

225231
// Mark checkpoint 1 as proven
226232
fake.markCheckpointAsProven(CheckpointNumber(1));
@@ -233,11 +239,8 @@ describe('Archiver Sync', () => {
233239
await archiver.syncImmediate();
234240
expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(3));
235241

236-
// Verify messages for all checkpoints
237-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toEqual(msgs1);
238-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(2))).toEqual(msgs2);
239-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toEqual(msgs3);
240-
await expect(archiver.getL1ToL2Messages(CheckpointNumber(4))).rejects.toThrow(L1ToL2MessagesNotReadyError);
242+
// Verify messages for all checkpoints, stored contiguously in insertion order.
243+
expect(await getStoredLeaves()).toEqual(asHex([...msgs1, ...msgs2, ...msgs3]));
241244

242245
// Verify private logs are surfaced through the block body.
243246
for (const checkpoint of [cp1, cp2, cp3]) {
@@ -440,29 +443,29 @@ describe('Archiver Sync', () => {
440443
logger.warn('Initial sync');
441444
await archiver.syncImmediate();
442445

443-
expect(inboxContract.getState).toHaveBeenCalledTimes(1);
446+
expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(1);
444447
expect(rollupContract.status).toHaveBeenCalledTimes(1);
445-
inboxContract.getState.mockClear();
448+
inboxContract.getCurrentBucket.mockClear();
446449
rollupContract.status.mockClear();
447450

448451
// We sync again, but since chain didn't move, no new calls should be expected
449452
logger.warn('Sync with no L1 advancement');
450453
await archiver.syncImmediate();
451-
expect(inboxContract.getState).toHaveBeenCalledTimes(0);
454+
expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(0);
452455
expect(rollupContract.status).toHaveBeenCalledTimes(0);
453456

454457
// Advance the chain and we should see calls again
455458
fake.setL1BlockNumber(150n);
456459
logger.warn('Sync after L1 advancement');
457460
await archiver.syncImmediate();
458-
expect(inboxContract.getState).toHaveBeenCalledTimes(1);
461+
expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(1);
459462
expect(rollupContract.status).toHaveBeenCalledTimes(1);
460463
});
461464

462465
it('does not fetch messages when local and remote state both have zero messages', async () => {
463-
// When there are no messages on L1, the remote inbox state has messagesRollingHash = Buffer16.ZERO
464-
// and totalMessagesInserted = 0. The local store also returns 0 messages and undefined lastMessage.
465-
// The fallback for the local rolling hash must use Buffer16.ZERO (not Buffer32.ZERO) to match.
466+
// When there are no messages on L1, the remote Inbox current bucket is genesis (rolling hash Fr.ZERO,
467+
// total 0). The local store also returns 0 messages and undefined lastMessage, whose rolling-hash fallback
468+
// is Fr.ZERO — so local and remote state match and no message fetch is attempted.
466469
fake.setL1BlockNumber(100n);
467470

468471
// Add a checkpoint with zero messages so the sync has something to process
@@ -1094,10 +1097,7 @@ describe('Archiver Sync', () => {
10941097
// Sync
10951098
await archiver.syncImmediate();
10961099

1097-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1098-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(2))).toHaveLength(0);
1099-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(4);
1100-
await expect(archiver.getL1ToL2Messages(CheckpointNumber(4))).rejects.toThrow(L1ToL2MessagesNotReadyError);
1100+
expect(await getStoredLeaves()).toEqual(asHex([...msgs1, ...msgs3]));
11011101

11021102
// Simulate L1 reorg: remove last 2 messages from checkpoint 3, add new messages for checkpoints 4 and 5
11031103
logger.warn('Reorging L1 to L2 messages');
@@ -1114,18 +1114,8 @@ describe('Archiver Sync', () => {
11141114
fake.setL1BlockNumber(111n);
11151115
await archiver.syncImmediate();
11161116

1117-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1118-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(2))).toHaveLength(0);
1119-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(2); // Reduced from 4 to 2
1120-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(1);
1121-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(5))).toHaveLength(2);
1122-
1123-
expect((await archiver.getL1ToL2Messages(CheckpointNumber(4))).map(leaf => leaf.toString())).toEqual(
1124-
[msg40].map(leaf => leaf.toString()),
1125-
);
1126-
expect((await archiver.getL1ToL2Messages(CheckpointNumber(5))).map(leaf => leaf.toString())).toEqual(
1127-
[msg50, msg51].map(leaf => leaf.toString()),
1128-
);
1117+
// The reorg kept the first 4 messages (2 from CP1, 2 from CP3) and appended the new ones.
1118+
expect(await getStoredLeaves()).toEqual(asHex([msgs1[0], msgs1[1], msgs3[0], msgs3[1], msg40, msg50, msg51]));
11291119
});
11301120

11311121
it('short-circuits rollback at the finalized L1 block', async () => {
@@ -1141,8 +1131,7 @@ describe('Archiver Sync', () => {
11411131
fake.setL1BlockNumber(110n);
11421132
await archiver.syncImmediate();
11431133

1144-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1145-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(3))).toHaveLength(4);
1134+
expect(await getStoredLeaves()).toEqual(asHex([...msgs1, ...msgs3]));
11461135

11471136
// Simulate L1 reorg: remove the last 2 messages from checkpoint 3 and add new ones.
11481137
fake.removeMessagesAfter(4);
@@ -1163,8 +1152,7 @@ describe('Archiver Sync', () => {
11631152
);
11641153
expect(callsAtFinalizedOrBelow).toHaveLength(0);
11651154

1166-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1167-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(1);
1155+
expect(await getStoredLeaves()).toEqual(asHex([msgs1[0], msgs1[1], msgs3[0], msgs3[1], msg40]));
11681156
});
11691157

11701158
it('falls back to per-message log queries when finalized block is undefined', async () => {
@@ -1193,8 +1181,7 @@ describe('Archiver Sync', () => {
11931181
// 2 messages mismatch on remote (msgs3[2], msgs3[3]) and one matches (msgs3[1]) before we break.
11941182
expect(eventByHashSpy).toHaveBeenCalledTimes(3);
11951183

1196-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(1))).toHaveLength(2);
1197-
expect(await archiver.getL1ToL2Messages(CheckpointNumber(4))).toHaveLength(1);
1184+
expect(await getStoredLeaves()).toEqual(asHex([msgs1[0], msgs1[1], msgs3[0], msgs3[1], msg40]));
11981185
});
11991186

12001187
it('persists the finalized L1 block monotonically after message sync', async () => {

yarn-project/archiver/src/archiver.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -764,14 +764,14 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
764764
`Removing checkpoints after checkpoint ${targetCheckpointNumber} (target block ${targetL2BlockNumber})`,
765765
);
766766
await this.updater.removeCheckpointsAfter(targetCheckpointNumber);
767-
this.log.info(`Rolling back L1 to L2 messages to checkpoint ${targetCheckpointNumber}`);
768-
await this.stores.messages.rollbackL1ToL2MessagesToCheckpoint(targetCheckpointNumber);
767+
this.log.info(`Rolling back L1 to L2 messages inserted after L1 block ${targetL1BlockNumber}`);
768+
await this.stores.messages.rollbackL1ToL2MessagesAfterL1Block(targetL1BlockNumber);
769769
this.log.info(`Setting L1 syncpoints to ${targetL1BlockNumber}`);
770770
await this.stores.blocks.setSynchedL1BlockNumber(targetL1BlockNumber);
771-
await this.stores.messages.setMessageSyncState(
772-
{ l1BlockNumber: targetL1BlockNumber, l1BlockHash: targetL1BlockHash },
773-
undefined,
774-
);
771+
await this.stores.messages.setMessageSyncState({
772+
l1BlockNumber: targetL1BlockNumber,
773+
l1BlockHash: targetL1BlockHash,
774+
});
775775
if (targetL2BlockNumber < currentProvenBlock) {
776776
this.log.info(`Rolling back proven L2 checkpoint to ${targetCheckpointNumber}`);
777777
await this.updater.setProvenCheckpointNumber(targetCheckpointNumber);

yarn-project/archiver/src/errors.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -102,20 +102,6 @@ export class BlockAlreadyCheckpointedError extends Error {
102102
}
103103
}
104104

105-
/** Thrown when L1 to L2 messages are requested for a checkpoint whose message tree hasn't been sealed yet. */
106-
export class L1ToL2MessagesNotReadyError extends Error {
107-
constructor(
108-
public readonly checkpointNumber: number,
109-
public readonly inboxTreeInProgress: bigint,
110-
) {
111-
super(
112-
`Cannot get L1 to L2 messages for checkpoint ${checkpointNumber}: ` +
113-
`inbox tree in progress is ${inboxTreeInProgress}, messages not yet sealed`,
114-
);
115-
this.name = 'L1ToL2MessagesNotReadyError';
116-
}
117-
}
118-
119105
/** Thrown when a proposed checkpoint number is stale (already processed). */
120106
export class ProposedCheckpointStaleError extends Error {
121107
constructor(

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { type Logger, createLogger } from '@aztec/foundation/log';
2424
import { RollupAbi } from '@aztec/l1-artifacts';
2525
import { Body, CommitteeAttestation, L2Block } from '@aztec/stdlib/block';
2626
import { Checkpoint, L1PublishedData, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
27-
import { InboxLeaf } from '@aztec/stdlib/messaging';
2827
import { Proof } from '@aztec/stdlib/proofs';
2928
import { CheckpointHeader } from '@aztec/stdlib/rollup';
3029
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
@@ -386,10 +385,6 @@ function mapLogInboxMessage(log: MessageSentLog): InboxMessage {
386385
leaf: log.args.leaf,
387386
l1BlockNumber: log.l1BlockNumber,
388387
l1BlockHash: log.l1BlockHash,
389-
// The Inbox no longer emits a checkpoint number (AZIP-22 Fast Inbox). Derive it from the compact index for the
390-
// legacy per-checkpoint message store, which the node still keeps until it drops the per-checkpoint flow.
391-
checkpointNumber: InboxLeaf.checkpointNumberFromIndex(log.args.index),
392-
rollingHash: log.args.rollingHash,
393388
inboxRollingHash: log.args.inboxRollingHash,
394389
bucketSeq: log.args.bucketSeq,
395390
bucketTimestamp: log.l1BlockTimestamp,

yarn-project/archiver/src/modules/data_source_base.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,6 @@ export abstract class ArchiverDataSourceBase
316316
return this.stores.functionNames.register(signatures);
317317
}
318318

319-
public getL1ToL2Messages(checkpointNumber: CheckpointNumber): Promise<Fr[]> {
320-
return this.stores.messages.getL1ToL2Messages(checkpointNumber);
321-
}
322-
323319
public getL1ToL2MessageIndex(l1ToL2Message: Fr): Promise<bigint | undefined> {
324320
return this.stores.messages.getL1ToL2MessageIndex(l1ToL2Message);
325321
}

0 commit comments

Comments
 (0)