Skip to content

Commit 62f14ef

Browse files
spalladinoclaude
andcommitted
fix(archiver): reject proposed blocks for slots that have already passed
Adds validation in the archiver to reject blocks submitted via addBlock() when the block's slot has already passed according to the L1 timestamp. This prevents stale blocks from being stored in the archiver when a checkpoint proposal fails after the slot boundary crosses. - Add getSlotAtNextL1Block helper to epoch-helpers - Validate block slot against current L1 timestamp in processQueuedBlocks - Add unit test for rejecting past slot blocks Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent a4fc873 commit 62f14ef

File tree

4 files changed

+63
-3
lines changed

4 files changed

+63
-3
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,42 @@ describe('Archiver Sync', () => {
10801080
await expect(archiver.addBlock(blockAlreadySyncedFromCheckpoint)).rejects.toThrow();
10811081
}, 10_000);
10821082

1083+
it('rejects adding blocks for past slots', async () => {
1084+
// L1 constants from setup: slotDuration=24, ethereumSlotDuration=12
1085+
// L1 blocks per L2 slot = 24/12 = 2
1086+
// L2 slot for L1 block N = floor((N * 12) / 24) = floor(N / 2)
1087+
1088+
// Sync checkpoint 1 from L1 to establish a baseline
1089+
const { checkpoint: cp1 } = await fake.addCheckpoint(CheckpointNumber(1), {
1090+
l1BlockNumber: 4n, // This is in L2 slot 2
1091+
messagesL1BlockNumber: 2n,
1092+
numL1ToL2Messages: 3,
1093+
slotNumber: SlotNumber(2),
1094+
});
1095+
const cp1Archive = cp1.blocks[cp1.blocks.length - 1].archive;
1096+
1097+
fake.setL1BlockNumber(4n);
1098+
await archiver.syncImmediate();
1099+
1100+
expect(await archiver.getSynchedCheckpointNumber()).toEqual(CheckpointNumber(1));
1101+
1102+
// Now advance L1 significantly to slot 10 (L1 block 20)
1103+
// Current slot = floor(20 / 2) = 10
1104+
// Slot at next L1 block = floor(21 / 2) = 10
1105+
fake.setL1BlockNumber(20n);
1106+
await archiver.syncImmediate();
1107+
1108+
// Create a block for slot 5 (which has already passed)
1109+
const pastSlotBlocks = await fake.makeBlocks(CheckpointNumber(2), {
1110+
l1BlockNumber: 10n, // Would be slot 5
1111+
previousArchive: cp1Archive,
1112+
slotNumber: SlotNumber(5), // Explicitly set past slot
1113+
});
1114+
1115+
// Try to add the block for the past slot - should be rejected
1116+
await expect(archiver.addBlock(pastSlotBlocks[0])).rejects.toThrow(/past slot/);
1117+
}, 10_000);
1118+
10831119
it('adds missing blocks when checkpoint has more blocks than local', async () => {
10841120
// Sync checkpoint 1 from L1
10851121
await fake.addCheckpoint(CheckpointNumber(1), {

yarn-project/archiver/src/archiver.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
2626
import {
2727
type L1RollupConstants,
2828
getEpochNumberAtTimestamp,
29+
getSlotAtNextL1Block,
2930
getSlotAtTimestamp,
3031
getSlotRangeForEpoch,
3132
getTimestampRangeForEpoch,
@@ -212,8 +213,23 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
212213
const queuedItems = this.blockQueue.splice(0, this.blockQueue.length);
213214
this.log.debug(`Processing ${queuedItems.length} queued block(s)`);
214215

216+
// Calculate slot threshold for validation
217+
const l1Timestamp = this.synchronizer.getL1Timestamp();
218+
const slotAtNextL1Block =
219+
l1Timestamp === undefined ? undefined : getSlotAtNextL1Block(l1Timestamp, this.l1Constants);
220+
215221
// Process each block individually to properly resolve/reject each promise
216222
for (const { block, resolve, reject } of queuedItems) {
223+
const blockSlot = block.header.globalVariables.slotNumber;
224+
if (slotAtNextL1Block !== undefined && blockSlot < slotAtNextL1Block) {
225+
this.log.warn(
226+
`Rejecting proposed block ${block.number} for past slot ${blockSlot} (current is ${slotAtNextL1Block})`,
227+
{ block: block.toBlockInfo(), l1Timestamp, slotAtNextL1Block },
228+
);
229+
reject(new Error(`Block ${block.number} is for past slot ${blockSlot} (current is ${slotAtNextL1Block})`));
230+
continue;
231+
}
232+
217233
try {
218234
await this.updater.addProposedBlocks([block]);
219235
this.log.debug(`Added block ${block.number} to store`);

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
1616
import { isDefined } from '@aztec/foundation/types';
1717
import { type ArchiverEmitter, L2BlockSourceEvents, type ValidateCheckpointResult } from '@aztec/stdlib/block';
1818
import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
19-
import { type L1RollupConstants, getEpochAtSlot, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
19+
import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
2020
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
2121
import { type Traceable, type Tracer, execInSpan, trackSpan } from '@aztec/telemetry-client';
2222

@@ -249,8 +249,7 @@ export class ArchiverL1Synchronizer implements Traceable {
249249
const firstUncheckpointedBlockSlot = firstUncheckpointedBlockHeader?.getSlot();
250250

251251
// What's the slot at the next L1 block? All blocks for slots strictly before this one should've been checkpointed by now.
252-
const nextL1BlockTimestamp = currentL1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration);
253-
const slotAtNextL1Block = getSlotAtTimestamp(nextL1BlockTimestamp, this.l1Constants);
252+
const slotAtNextL1Block = getSlotAtNextL1Block(currentL1Timestamp, this.l1Constants);
254253

255254
// Prune provisional blocks from slots that have ended without being checkpointed
256255
if (firstUncheckpointedBlockSlot !== undefined && firstUncheckpointedBlockSlot < slotAtNextL1Block) {

yarn-project/stdlib/src/epoch-helpers/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export function getSlotAtTimestamp(
5151
: SlotNumber.fromBigInt((ts - constants.l1GenesisTime) / BigInt(constants.slotDuration));
5252
}
5353

54+
/** Returns the L2 slot number at the next L1 block based on the current timestamp. */
55+
export function getSlotAtNextL1Block(
56+
currentL1Timestamp: bigint,
57+
constants: Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration' | 'ethereumSlotDuration'>,
58+
): SlotNumber {
59+
const nextL1BlockTimestamp = currentL1Timestamp + BigInt(constants.ethereumSlotDuration);
60+
return getSlotAtTimestamp(nextL1BlockTimestamp, constants);
61+
}
62+
5463
/** Returns the epoch number for a given timestamp. */
5564
export function getEpochNumberAtTimestamp(
5665
ts: bigint,

0 commit comments

Comments
 (0)