Skip to content

Commit ed302f1

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 a319e00 commit ed302f1

17 files changed

Lines changed: 215 additions & 349 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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -443,22 +443,22 @@ describe('Archiver Sync', () => {
443443
logger.warn('Initial sync');
444444
await archiver.syncImmediate();
445445

446-
expect(inboxContract.getState).toHaveBeenCalledTimes(1);
446+
expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(1);
447447
expect(rollupContract.status).toHaveBeenCalledTimes(1);
448-
inboxContract.getState.mockClear();
448+
inboxContract.getCurrentBucket.mockClear();
449449
rollupContract.status.mockClear();
450450

451451
// We sync again, but since chain didn't move, no new calls should be expected
452452
logger.warn('Sync with no L1 advancement');
453453
await archiver.syncImmediate();
454-
expect(inboxContract.getState).toHaveBeenCalledTimes(0);
454+
expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(0);
455455
expect(rollupContract.status).toHaveBeenCalledTimes(0);
456456

457457
// Advance the chain and we should see calls again
458458
fake.setL1BlockNumber(150n);
459459
logger.warn('Sync after L1 advancement');
460460
await archiver.syncImmediate();
461-
expect(inboxContract.getState).toHaveBeenCalledTimes(1);
461+
expect(inboxContract.getCurrentBucket).toHaveBeenCalledTimes(1);
462462
expect(rollupContract.status).toHaveBeenCalledTimes(1);
463463
});
464464

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
}

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

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { BlobClientInterface } from '@aztec/blob-client/client';
22
import { EpochCache } from '@aztec/epoch-cache';
3-
import { InboxContract, type InboxContractState, RollupContract } from '@aztec/ethereum/contracts';
3+
import { InboxContract, type InboxContractBucket, RollupContract } from '@aztec/ethereum/contracts';
44
import type { L1BlockId } from '@aztec/ethereum/l1-types';
55
import { getFinalizedL1Block } from '@aztec/ethereum/queries';
66
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
77
import { asyncPool } from '@aztec/foundation/async-pool';
88
import { maxBigint } from '@aztec/foundation/bigint';
99
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
10-
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
10+
import { Buffer32 } from '@aztec/foundation/buffer';
1111
import { compactArray, partition, pick } from '@aztec/foundation/collection';
1212
import { Fr } from '@aztec/foundation/curves/bn254';
1313
import { EthAddress } from '@aztec/foundation/eth-address';
@@ -430,16 +430,14 @@ export class ArchiverL1Synchronizer implements Traceable {
430430
return true;
431431
}
432432

433-
// Compare local message store state with the remote. If they match, we just advance the match pointer.
434-
const remoteMessagesState = await this.inbox.getState({ blockNumber: currentL1BlockNumber });
433+
// Compare local message store state with the remote. If they match, we just advance the match pointer. The
434+
// remote state is the Inbox's current bucket (AZIP-22 Fast Inbox): its cumulative total and consensus rolling
435+
// hash are the Inbox's live chain position.
436+
const remoteBucket = await this.inbox.getCurrentBucket({ blockNumber: currentL1BlockNumber });
435437
const localLastMessage = await this.stores.messages.getLastMessage();
436-
if (await this.localStateMatches(localLastMessage, remoteMessagesState)) {
438+
if (await this.localStateMatches(localLastMessage, remoteBucket)) {
437439
this.log.trace(`Local L1 to L2 messages are already in sync with remote at L1 block ${currentL1BlockNumber}`);
438-
await this.stores.messages.setMessageSyncState(
439-
currentL1Block,
440-
remoteMessagesState.treeInProgress,
441-
finalizedL1Block,
442-
);
440+
await this.stores.messages.setMessageSyncState(currentL1Block, finalizedL1Block);
443441
return true;
444442
}
445443

@@ -455,7 +453,7 @@ export class ArchiverL1Synchronizer implements Traceable {
455453
`Failed to store L1 to L2 messages retrieved from L1: ${error.message}. Rolling back syncpoint to retry.`,
456454
{ inboxMessage: error.inboxMessage },
457455
);
458-
await this.rollbackL1ToL2Messages(remoteMessagesState);
456+
await this.rollbackL1ToL2Messages(remoteBucket);
459457
return false;
460458
}
461459
throw error;
@@ -465,32 +463,28 @@ export class ArchiverL1Synchronizer implements Traceable {
465463
// we'd notice by comparing our local state with the remote one again, and seeing they don't match even after
466464
// our sync attempt. In this case, we also rollback our syncpoint, and trigger a retry.
467465
const localLastMessageAfterSync = await this.stores.messages.getLastMessage();
468-
if (!(await this.localStateMatches(localLastMessageAfterSync, remoteMessagesState))) {
466+
if (!(await this.localStateMatches(localLastMessageAfterSync, remoteBucket))) {
469467
this.log.warn(
470468
`Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`,
471-
{ localLastMessageAfterSync, remoteMessagesState },
469+
{ localLastMessageAfterSync, remoteBucket },
472470
);
473-
await this.rollbackL1ToL2Messages(remoteMessagesState);
471+
await this.rollbackL1ToL2Messages(remoteBucket);
474472
return false;
475473
}
476474

477475
// Advance the syncpoint after a successful sync
478-
await this.stores.messages.setMessageSyncState(
479-
currentL1Block,
480-
remoteMessagesState.treeInProgress,
481-
finalizedL1Block,
482-
);
476+
await this.stores.messages.setMessageSyncState(currentL1Block, finalizedL1Block);
483477
return true;
484478
}
485479

486-
/** Checks if the local rolling hash and message count matches the remote state */
487-
private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteState: InboxContractState) {
480+
/** Checks if the local consensus rolling hash and message count match the remote Inbox current bucket. */
481+
private async localStateMatches(localLastMessage: InboxMessage | undefined, remoteBucket: InboxContractBucket) {
488482
const localMessageCount = await this.stores.messages.getTotalL1ToL2MessageCount();
489-
this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteState });
483+
this.log.trace(`Comparing local and remote inbox state`, { localMessageCount, localLastMessage, remoteBucket });
490484

491485
return (
492-
remoteState.totalMessagesInserted === localMessageCount &&
493-
remoteState.messagesRollingHash.equals(localLastMessage?.rollingHash ?? Buffer16.ZERO)
486+
remoteBucket.totalMsgCount === localMessageCount &&
487+
remoteBucket.rollingHash.equals(localLastMessage?.inboxRollingHash ?? Fr.ZERO)
494488
);
495489
}
496490

@@ -518,19 +512,19 @@ export class ArchiverL1Synchronizer implements Traceable {
518512
} while (searchEndBlock < toL1Block);
519513

520514
if (messageCount > 0) {
521-
this.log.info(
522-
`Retrieved ${messageCount} new L1 to L2 messages up to message with index ${lastMessage?.index} for checkpoint ${lastMessage?.checkpointNumber}`,
523-
{ lastMessage, messageCount },
524-
);
515+
this.log.info(`Retrieved ${messageCount} new L1 to L2 messages up to message with index ${lastMessage?.index}`, {
516+
lastMessage,
517+
messageCount,
518+
});
525519
}
526520
}
527521

528522
/**
529523
* Rolls back local L1 to L2 messages to the last common message with L1, and updates the syncpoint to the L1 block of that message.
530524
* If no common message is found, rolls back all messages and sets the syncpoint to the start block.
531525
*/
532-
private async rollbackL1ToL2Messages(remoteMessagesState: InboxContractState): Promise<L1BlockId> {
533-
const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState;
526+
private async rollbackL1ToL2Messages(remoteBucket: InboxContractBucket): Promise<L1BlockId> {
527+
const remoteRollingHash = remoteBucket.rollingHash;
534528

535529
const messagesFinalizedL1Block = await this.stores.messages.getMessagesFinalizedL1Block();
536530
const finalizedL1BlockNumber = messagesFinalizedL1Block?.l1BlockNumber;
@@ -542,12 +536,12 @@ export class ArchiverL1Synchronizer implements Traceable {
542536
let messagesToDelete = 0;
543537
this.log.verbose(`Searching most recent common L1 to L2 message`);
544538
for await (const localMsg of this.stores.messages.iterateL1ToL2Messages({ reverse: true })) {
545-
const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteMessagesState };
539+
const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteBucket };
546540

547541
// First check if the local message rolling hash matches the current rolling hash of the inbox contract,
548542
// which means we just need to rollback some local messages and we should be back in sync. This means there
549543
// was an L1 reorg that removed some of the messages we had, but no new messages were added compared.
550-
if (localMsg.rollingHash.equals(remoteRollingHash)) {
544+
if (localMsg.inboxRollingHash.equals(remoteRollingHash)) {
551545
this.log.info(
552546
`Found common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber} matching current remote state`,
553547
logCtx,
@@ -570,7 +564,7 @@ export class ArchiverL1Synchronizer implements Traceable {
570564
// an archival rpc node, since the message could be from a long time ago if we're catching up with syncing.
571565
const remoteMsg = await retrieveL1ToL2Message(this.inbox, localMsg);
572566
logCtx.remoteMsg = remoteMsg;
573-
if (remoteMsg && remoteMsg.rollingHash.equals(localMsg.rollingHash)) {
567+
if (remoteMsg && remoteMsg.inboxRollingHash.equals(localMsg.inboxRollingHash)) {
574568
this.log.info(
575569
`Found most recent common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber}`,
576570
logCtx,
@@ -612,10 +606,9 @@ export class ArchiverL1Synchronizer implements Traceable {
612606
: await this.getL1BlockHash(syncPointL1BlockNumber);
613607

614608
const messagesSyncPoint = { l1BlockNumber: syncPointL1BlockNumber, l1BlockHash: syncPointL1BlockHash };
615-
await this.stores.messages.setMessageSyncState(messagesSyncPoint, remoteTreeInProgress);
609+
await this.stores.messages.setMessageSyncState(messagesSyncPoint);
616610
this.log.verbose(`Updated messages syncpoint to L1 block ${messagesSyncPoint.l1BlockNumber}`, {
617611
...messagesSyncPoint,
618-
remoteTreeInProgress,
619612
});
620613
return messagesSyncPoint;
621614
}

yarn-project/archiver/src/store/data_stores.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { FunctionNamesCache } from './function_names_cache.js';
1313
import { LogStore } from './log_store.js';
1414
import { MessageStore } from './message_store.js';
1515

16-
export const ARCHIVER_DB_VERSION = 8;
16+
export const ARCHIVER_DB_VERSION = 9;
1717

1818
/**
1919
* Represents the latest L1 block processed by the archiver for various objects in L2.

0 commit comments

Comments
 (0)