From 36fc8dc3fac6cbab678b431198e6d678c1b6cc4a Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Tue, 4 Feb 2025 19:09:02 +0000 Subject: [PATCH 1/3] fix: handle sequencer building block mid-synch --- .../src/sequencer/sequencer.ts | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 0b7a475ed86..345cf70c061 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -19,6 +19,7 @@ import { GENESIS_ARCHIVE_ROOT, Gas, type GlobalVariables, + INITIAL_L2_BLOCK_NUM, StateReference, } from '@aztec/circuits.js'; import { AztecAddress } from '@aztec/foundation/aztec-address'; @@ -232,20 +233,18 @@ export class Sequencer { protected async doRealWork() { this.setState(SequencerState.SYNCHRONIZING, 0n); // Update state when the previous block has been synced - const prevBlockSynced = await this.isBlockSynced(); + const chainTip = await this.getChainTip(); // Do not go forward with new block if the previous one has not been mined and processed - if (!prevBlockSynced) { + if (!chainTip) { return; } this.setState(SequencerState.PROPOSER_CHECK, 0n); - const chainTip = await this.l2BlockSource.getBlock(-1); - - const newBlockNumber = (chainTip?.header.globalVariables.blockNumber.toNumber() ?? 0) + 1; + const newBlockNumber = chainTip.blockNumber + 1; // If we cannot find a tip archive, assume genesis. - const chainTipArchive = chainTip?.archive.root ?? new Fr(GENESIS_ARCHIVE_ROOT); + const chainTipArchive = chainTip.archive; const slot = await this.slotForProposal(chainTipArchive.toBuffer(), BigInt(newBlockNumber)); if (!slot) { @@ -763,14 +762,16 @@ export class Sequencer { * We don't check against the previous block submitted since it may have been reorg'd out. * @returns Boolean indicating if our dependencies are synced to the latest block. */ - protected async isBlockSynced() { + protected async getChainTip(): Promise<{ blockNumber: number; archive: Fr } | undefined> { const syncedBlocks = await Promise.all([ this.worldState.status().then((s: WorldStateSynchronizerStatus) => s.syncedToL2Block), this.l2BlockSource.getL2Tips().then(t => t.latest), - this.p2pClient.getStatus().then(s => s.syncedToL2Block.number), + this.p2pClient.getStatus(), this.l1ToL2MessageSource.getBlockNumber(), ] as const); + const [worldState, l2BlockSource, p2p, l1ToL2MessageSource] = syncedBlocks; + const result = // check that world state has caught up with archiver // note that the archiver reports undefined hash for the genesis block @@ -780,8 +781,8 @@ export class Sequencer { // this should change to hashes once p2p client handles reorgs // and once we stop pretending that the l1tol2message source is not // just the archiver under a different name - p2p >= l2BlockSource.number && - l1ToL2MessageSource >= l2BlockSource.number; + (!l2BlockSource.hash || p2p.syncedToL2Block.hash === l2BlockSource.hash) && + l1ToL2MessageSource === l2BlockSource.number; this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, { worldStateNumber: worldState.number, @@ -791,7 +792,21 @@ export class Sequencer { p2pNumber: p2p, l1ToL2MessageSourceNumber: l1ToL2MessageSource, }); - return result; + + if (!result) { + return undefined; + } + if (worldState.number >= INITIAL_L2_BLOCK_NUM) { + const block = await this.l2BlockSource.getBlock(worldState.number); + if (!block) { + // this shouldn't really happen because a moment ago we checked that all components were in synch + return undefined; + } + + return { blockNumber: block.number, archive: block.archive.root }; + } else { + return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive: new Fr(GENESIS_ARCHIVE_ROOT) }; + } } private getSlotStartTimestamp(slotNumber: number | bigint): number { From 2e01774c181919120d5bc08e056b0e29cf86d7ea Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 5 Feb 2025 08:51:28 +0000 Subject: [PATCH 2/3] fix: update mocks used by sequencer test --- .../sequencer-client/src/sequencer/sequencer.test.ts | 5 +++++ yarn-project/sequencer-client/src/sequencer/sequencer.ts | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index b7b58425128..1ae2f2149e5 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -560,6 +560,11 @@ describe('sequencer', () => { l2BlockSource.getBlock.mockResolvedValue(L2Block.random(blockNumber - 1)); l2BlockSource.getBlockNumber.mockResolvedValue(blockNumber - 1); + l2BlockSource.getL2Tips.mockResolvedValue({ + latest: { number: blockNumber - 1, hash }, + proven: { number: 0, hash: undefined }, + finalized: { number: 0, hash: undefined }, + }); l1ToL2MessageSource.getBlockNumber.mockResolvedValue(blockNumber - 1); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 345cf70c061..0d6d9b1333f 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -766,7 +766,7 @@ export class Sequencer { const syncedBlocks = await Promise.all([ this.worldState.status().then((s: WorldStateSynchronizerStatus) => s.syncedToL2Block), this.l2BlockSource.getL2Tips().then(t => t.latest), - this.p2pClient.getStatus(), + this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block), this.l1ToL2MessageSource.getBlockNumber(), ] as const); @@ -781,7 +781,7 @@ export class Sequencer { // this should change to hashes once p2p client handles reorgs // and once we stop pretending that the l1tol2message source is not // just the archiver under a different name - (!l2BlockSource.hash || p2p.syncedToL2Block.hash === l2BlockSource.hash) && + (!l2BlockSource.hash || p2p.hash === l2BlockSource.hash) && l1ToL2MessageSource === l2BlockSource.number; this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, { @@ -789,7 +789,8 @@ export class Sequencer { worldStateHash: worldState.hash, l2BlockSourceNumber: l2BlockSource.number, l2BlockSourceHash: l2BlockSource.hash, - p2pNumber: p2p, + p2pNumber: p2p.number, + p2pHash: p2p.hash, l1ToL2MessageSourceNumber: l1ToL2MessageSource, }); From ac43e517b7c2c357d27fb1152b10dc86af26e944 Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Wed, 5 Feb 2025 09:33:44 +0000 Subject: [PATCH 3/3] test: sequencer selects the chain tip when building a block --- .../src/sequencer/sequencer.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 1ae2f2149e5..4bfd14aeed0 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -488,6 +488,45 @@ describe('sequencer', () => { expectPublisherProposeL2Block(postFlushTxHashes); }); + it('settles on the chain tip before it starts building a block', async () => { + // this test simulates a synch happening right after the sequencer starts building a bloxk + // simulate every component being synched + const firstBlock = await L2Block.random(1); + const currentTip = firstBlock; + const syncedToL2Block = { number: currentTip.number, hash: (await currentTip.hash()).toString() }; + worldState.status.mockImplementation(() => + Promise.resolve({ state: WorldStateRunningState.IDLE, syncedToL2Block }), + ); + p2p.getStatus.mockImplementation(() => Promise.resolve({ state: P2PClientState.IDLE, syncedToL2Block })); + l2BlockSource.getL2Tips.mockImplementation(() => + Promise.resolve({ + latest: syncedToL2Block, + proven: { number: 0, hash: undefined }, + finalized: { number: 0, hash: undefined }, + }), + ); + l1ToL2MessageSource.getBlockNumber.mockImplementation(() => Promise.resolve(currentTip.number)); + + // simulate a synch happening right after + l2BlockSource.getBlockNumber.mockResolvedValueOnce(currentTip.number); + l2BlockSource.getBlockNumber.mockResolvedValueOnce(currentTip.number + 1); + // now the new tip is actually block 2 + l2BlockSource.getBlock.mockImplementation(n => + n === -1 + ? L2Block.random(currentTip.number + 1) + : n === currentTip.number + ? Promise.resolve(currentTip) + : Promise.resolve(undefined), + ); + + publisher.canProposeAtNextEthBlock.mockResolvedValueOnce(undefined); + await sequencer.doRealWork(); + expect(publisher.enqueueProposeL2Block).not.toHaveBeenCalled(); + // even though the chain tip moved, the sequencer should still have tried to build a block against the old archive + // this should get caught by the rollup + expect(publisher.canProposeAtNextEthBlock).toHaveBeenCalledWith(currentTip.archive.root.toBuffer()); + }); + it('aborts building a block if the chain moves underneath it', async () => { const tx = await makeTx(); mockPendingTxs([tx]);