Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Do not report epoch as complete until blocks have synced #12638

Merged
merged 1 commit into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions yarn-project/archiver/src/archiver/archiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,40 @@ describe('Archiver', () => {
expect(await archiver.isEpochComplete(0n)).toBe(true);
});

// Regression for https://github.com/AztecProtocol/aztec-packages/issues/12631
it('reports an epoch as complete due to timestamp only once all its blocks have been synced', async () => {
const { l1StartBlock, slotDuration, ethereumSlotDuration, epochDuration } = l1Constants;
const l2Slot = 1;
const l1BlockForL2Block = l1StartBlock + BigInt((l2Slot * slotDuration) / ethereumSlotDuration);
const lastL1BlockForEpoch = l1StartBlock + BigInt((epochDuration * slotDuration) / ethereumSlotDuration) - 1n;

logger.info(`Syncing epoch 0 with L2 block on slot ${l2Slot} mined in L1 block ${l1BlockForL2Block}`);
const l2Block = blocks[0];
l2Block.header.globalVariables.slotNumber = new Fr(l2Slot);
blocks = [l2Block];
const blobHashes = await makeVersionedBlobHashes(l2Block);

const rollupTxs = await Promise.all(blocks.map(makeRollupTx));
publicClient.getBlockNumber.mockResolvedValueOnce(lastL1BlockForEpoch);
mockRollup.read.status.mockResolvedValueOnce([0n, GENESIS_ROOT, 1n, l2Block.archive.root.toString(), GENESIS_ROOT]);
makeL2BlockProposedEvent(l1BlockForL2Block, 1n, l2Block.archive.root.toString(), blobHashes);

rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx));
const blobsFromBlocks = await Promise.all(blocks.map(b => makeBlobsFromBlock(b)));
blobsFromBlocks.forEach(blobs => blobSinkClient.getBlobSidecar.mockResolvedValueOnce(blobs));

await archiver.start(false);

expect(await archiver.isEpochComplete(0n)).toBe(false);
while (!(await archiver.isEpochComplete(0n))) {
// No sleep, we want to know exactly when the epoch completes
}

// Once epoch is flagged as complete, block number must be 1
expect(await archiver.getBlockNumber()).toEqual(1);
expect(await archiver.isEpochComplete(0n)).toBe(true);
});

// TODO(palla/reorg): Add a unit test for the archiver handleEpochPrune
xit('handles an upcoming L2 prune', () => {});

Expand Down
34 changes: 20 additions & 14 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {

private store: ArchiverStoreHelper;

public l1BlockNumber: bigint | undefined;
public l1Timestamp: bigint | undefined;
private l1BlockNumber: bigint | undefined;
private l1Timestamp: bigint | undefined;

public readonly tracer: Tracer;

Expand Down Expand Up @@ -270,11 +270,11 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
// ********** Events that are processed per L1 block **********
await this.handleL1ToL2Messages(messagesSynchedTo, currentL1BlockNumber);

// Store latest l1 block number and timestamp seen. Used for epoch and slots calculations.
if (!this.l1BlockNumber || this.l1BlockNumber < currentL1BlockNumber) {
this.l1Timestamp = (await this.publicClient.getBlock({ blockNumber: currentL1BlockNumber })).timestamp;
this.l1BlockNumber = currentL1BlockNumber;
}
// Get L1 timestamp for the current block
const currentL1Timestamp =
!this.l1Timestamp || !this.l1BlockNumber || this.l1BlockNumber !== currentL1BlockNumber
? (await this.publicClient.getBlock({ blockNumber: currentL1BlockNumber })).timestamp
: this.l1Timestamp;

// ********** Events that are processed per L2 block **********
if (currentL1BlockNumber > blocksSynchedTo) {
Expand All @@ -285,11 +285,16 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
// blocks from more than 2 epochs ago, so we want to make sure we have the latest view of
// the chain locally before we start unwinding stuff. This can be optimized by figuring out
// up to which point we're pruning, and then requesting L2 blocks up to that point only.
await this.handleEpochPrune(provenBlockNumber, currentL1BlockNumber);

await this.handleEpochPrune(provenBlockNumber, currentL1BlockNumber, currentL1Timestamp);
this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
}

// After syncing has completed, update the current l1 block number and timestamp,
// otherwise we risk announcing to the world that we've synced to a given point,
// but the corresponding blocks have not been processed (see #12631).
this.l1Timestamp = currentL1Timestamp;
this.l1BlockNumber = currentL1BlockNumber;

if (initialRun) {
this.log.info(`Initial archiver sync to L1 block ${currentL1BlockNumber} complete.`, {
l1BlockNumber: currentL1BlockNumber,
Expand All @@ -300,18 +305,19 @@ export class Archiver extends EventEmitter implements ArchiveSource, Traceable {
}

/** Queries the rollup contract on whether a prune can be executed on the immediatenext L1 block. */
private async canPrune(currentL1BlockNumber: bigint) {
const time = (this.l1Timestamp ?? 0n) + BigInt(this.l1constants.ethereumSlotDuration);
private async canPrune(currentL1BlockNumber: bigint, currentL1Timestamp: bigint) {
const time = (currentL1Timestamp ?? 0n) + BigInt(this.l1constants.ethereumSlotDuration);
return await this.rollup.read.canPruneAtTime([time], { blockNumber: currentL1BlockNumber });
}

/** Checks if there'd be a reorg for the next block submission and start pruning now. */
private async handleEpochPrune(provenBlockNumber: bigint, currentL1BlockNumber: bigint) {
private async handleEpochPrune(provenBlockNumber: bigint, currentL1BlockNumber: bigint, currentL1Timestamp: bigint) {
const localPendingBlockNumber = BigInt(await this.getBlockNumber());
const canPrune = localPendingBlockNumber > provenBlockNumber && (await this.canPrune(currentL1BlockNumber));
const canPrune =
localPendingBlockNumber > provenBlockNumber && (await this.canPrune(currentL1BlockNumber, currentL1Timestamp));

if (canPrune) {
const localPendingSlotNumber = await this.getL2SlotNumber();
const localPendingSlotNumber = getSlotAtTimestamp(currentL1Timestamp, this.l1constants);
const localPendingEpochNumber = getEpochAtSlot(localPendingSlotNumber, this.l1constants);

// Emit an event for listening services to react to the chain prune
Expand Down