diff --git a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts index bc766de90ad9..470f49d7d1cb 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts @@ -1,5 +1,6 @@ import type { AztecNodeService } from '@aztec/aztec-node'; -import { retryUntil } from '@aztec/aztec.js'; +import { retryUntil, sleep } from '@aztec/aztec.js'; +import type { ValidatorsStats } from '@aztec/stdlib/validators'; import { jest } from '@jest/globals'; import fs from 'fs'; @@ -7,12 +8,13 @@ import 'jest-extended'; import os from 'os'; import path from 'path'; -import { createNodes } from '../fixtures/setup_p2p_test.js'; +import { createNode, createNodes } from '../fixtures/setup_p2p_test.js'; import { P2PNetworkTest, SHORTENED_BLOCK_TIME_CONFIG } from './p2p_network.js'; const NUM_NODES = 4; const NUM_VALIDATORS = NUM_NODES + 1; // We create an extra validator, who will not have a running node const BOOT_NODE_UDP_PORT = 40900; +const BLOCK_COUNT = 3; const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'validators-sentinel-')); @@ -22,7 +24,7 @@ describe('e2e_p2p_validators_sentinel', () => { let t: P2PNetworkTest; let nodes: AztecNodeService[]; - beforeEach(async () => { + beforeAll(async () => { t = await P2PNetworkTest.create({ testName: 'e2e_p2p_network', numberOfNodes: NUM_VALIDATORS, @@ -39,9 +41,11 @@ describe('e2e_p2p_validators_sentinel', () => { await t.applyBaseSnapshots(); await t.setup(); await t.removeInitialNode(); + + t.logger.info(`Setup complete`, { validators: t.validators }); }); - afterEach(async () => { + afterAll(async () => { await t.stopNodes(nodes); await t.teardown(); for (let i = 0; i < NUM_NODES; i++) { @@ -49,59 +53,89 @@ describe('e2e_p2p_validators_sentinel', () => { } }); - it('collects stats for offline validator', async () => { - if (!t.bootstrapNodeEnr) { - throw new Error('Bootstrap node ENR is not available'); - } + describe('with an offline validator', () => { + let stats: ValidatorsStats; + beforeAll(async () => { + t.logger.info('Creating nodes'); + nodes = await createNodes( + t.ctx.aztecNodeConfig, + t.ctx.dateProvider, + t.bootstrapNodeEnr, + NUM_NODES, // Note we do not create the last validator yet, so it shows as offline + BOOT_NODE_UDP_PORT, + t.prefilledPublicData, + DATA_DIR, + ); + + const currentBlock = t.monitor.l2BlockNumber; + const blockCount = BLOCK_COUNT; + const timeout = SHORTENED_BLOCK_TIME_CONFIG.aztecSlotDuration * blockCount * 8; + t.logger.info(`Waiting until L2 block ${currentBlock + blockCount}`, { currentBlock, blockCount, timeout }); + await retryUntil(() => t.monitor.l2BlockNumber >= currentBlock + blockCount, 'blocks mined', timeout); + + stats = await nodes[0].getValidatorsStats(); + t.logger.info(`Collected validator stats at block ${t.monitor.l2BlockNumber}`, { stats }); + }); + + it('collects stats on offline validator', () => { + const offlineValidator = t.validators.at(-1)!.attester.toLowerCase(); + t.logger.info(`Asserting stats for offline validator ${offlineValidator}`); + const offlineStats = stats.stats[offlineValidator]; + const historyLength = offlineStats.history.length; + expect(offlineStats.history.length).toBeGreaterThanOrEqual(BLOCK_COUNT - 1); + expect(offlineStats.history.every(h => h.status.endsWith('-missed'))).toBeTrue(); + expect(offlineStats.missedAttestations.count + offlineStats.missedProposals.count).toEqual(historyLength); + expect(offlineStats.missedAttestations.rate).toEqual(1); + expect(offlineStats.missedProposals.rate).toBeOneOf([1, NaN]); + }); - t.logger.info('Creating nodes'); - nodes = await createNodes( - t.ctx.aztecNodeConfig, - t.ctx.dateProvider, - t.bootstrapNodeEnr, - NUM_NODES, - BOOT_NODE_UDP_PORT, - t.prefilledPublicData, - DATA_DIR, - ); - - // Wait for a few blocks to be mined - const currentBlock = t.monitor.l2BlockNumber; - const blockCount = 3; - const timeout = SHORTENED_BLOCK_TIME_CONFIG.aztecSlotDuration * blockCount * 8; - t.logger.info(`Waiting until L2 block ${currentBlock + blockCount}`, { currentBlock, blockCount, timeout }); - await retryUntil(() => t.monitor.l2BlockNumber >= currentBlock + blockCount, 'blocks mined', timeout); - - const stats = await nodes[0].getValidatorsStats(); - t.logger.info(`Collected validator stats at block ${t.monitor.l2BlockNumber}`, { stats, validators: t.validators }); - - // Check stats for the offline validator - const offlineValidator = t.validators.at(-1)!.attester.toLowerCase(); - t.logger.info(`Asserting stats for offline validator ${offlineValidator}`); - const offlineStats = stats.stats[offlineValidator]; - const historyLength = offlineStats.history.length; - expect(offlineStats.history.length).toBeGreaterThanOrEqual(blockCount - 1); - expect(offlineStats.history.every(h => h.status.endsWith('-missed'))).toBeTrue(); - expect(offlineStats.missedAttestations.count + offlineStats.missedProposals.count).toEqual(historyLength); - expect(offlineStats.missedAttestations.rate).toEqual(1); - expect(offlineStats.missedProposals.rate).toBeOneOf([1, NaN]); - - // Check stats for a validator that mined a block - const [proposerValidator, proposerStats] = Object.entries(stats.stats).find(([_, v]) => - v?.history?.some(h => h.status === 'block-mined'), - )!; - t.logger.info(`Asserting stats for proposer validator ${proposerValidator}`); - expect(t.validators.map(v => v.attester.toLowerCase())).toContain(proposerValidator); - expect(proposerStats.history.length).toBeGreaterThanOrEqual(blockCount - 1); - expect(proposerStats.missedProposals.rate).toBeLessThan(1); - - // Check stats for a validator that attested to a block - const [attestorValidator, attestorStats] = Object.entries(stats.stats).find(([_, v]) => - v?.history?.some(h => h.status === 'attestation-sent'), - )!; - t.logger.info(`Asserting stats for attestor validator ${attestorValidator}`); - expect(t.validators.map(v => v.attester.toLowerCase())).toContain(attestorValidator); - expect(attestorStats.history.length).toBeGreaterThanOrEqual(blockCount - 1); - expect(attestorStats.missedAttestations.rate).toBeLessThan(1); + it('collects stats on a block builder', () => { + const [proposerValidator, proposerStats] = Object.entries(stats.stats).find(([_, v]) => + v?.history?.some(h => h.status === 'block-mined'), + )!; + t.logger.info(`Asserting stats for proposer validator ${proposerValidator}`); + expect(proposerStats).toBeDefined(); + expect(t.validators.map(v => v.attester.toLowerCase())).toContain(proposerValidator); + expect(proposerStats.history.length).toBeGreaterThanOrEqual(BLOCK_COUNT - 1); + expect(proposerStats.missedProposals.rate).toBeLessThan(1); + }); + + it('collects stats on an attestor', () => { + const [attestorValidator, attestorStats] = Object.entries(stats.stats).find(([_, v]) => + v?.history?.some(h => h.status === 'attestation-sent'), + )!; + t.logger.info(`Asserting stats for attestor validator ${attestorValidator}`); + expect(attestorStats).toBeDefined(); + expect(t.validators.map(v => v.attester.toLowerCase())).toContain(attestorValidator); + expect(attestorStats.history.length).toBeGreaterThanOrEqual(BLOCK_COUNT - 1); + expect(attestorStats.missedAttestations.rate).toBeLessThan(1); + }); + + // Regression test for #13142 + it('starts a sentinel on a fresh node', async () => { + const l2BlockNumber = t.monitor.l2BlockNumber; + const nodeIndex = NUM_NODES + 1; + const newNode = await createNode( + t.ctx.aztecNodeConfig, + t.ctx.dateProvider, + BOOT_NODE_UDP_PORT + nodeIndex + 1, + t.bootstrapNodeEnr!, + nodeIndex, + t.prefilledPublicData, + `${DATA_DIR}-i`, + ); + + t.logger.info(`Waiting for a few more blocks to be mined`); + const timeout = SHORTENED_BLOCK_TIME_CONFIG.aztecSlotDuration * 4 * 8; + await retryUntil(() => t.monitor.l2BlockNumber > l2BlockNumber + 3, 'more blocks mined', timeout); + await sleep(1000); + + const stats = await newNode.getValidatorsStats(); + t.logger.info(`Collected validator stats from new node at block ${t.monitor.l2BlockNumber}`, { stats }); + const newNodeValidator = t.validators.at(-1)!.attester.toLowerCase(); + expect(stats.stats[newNodeValidator]).toBeDefined(); + expect(stats.stats[newNodeValidator].history.length).toBeGreaterThanOrEqual(1); + expect(Object.keys(stats.stats).length).toBeGreaterThan(1); + }); }); }); diff --git a/yarn-project/kv-store/src/stores/l2_tips_memory_store.ts b/yarn-project/kv-store/src/stores/l2_tips_memory_store.ts index 40640fa4a362..5ff442297f7b 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_memory_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_memory_store.ts @@ -48,19 +48,26 @@ export class L2TipsMemoryStore implements L2BlockStreamEventHandler, L2BlockStre break; } case 'chain-pruned': - this.l2TipsStore.set('latest', event.blockNumber); + this.saveTag('latest', event.block); break; case 'chain-proven': - this.l2TipsStore.set('proven', event.blockNumber); + this.saveTag('proven', event.block); break; case 'chain-finalized': - this.l2TipsStore.set('finalized', event.blockNumber); + this.saveTag('finalized', event.block); for (const key of this.l2BlockHashesStore.keys()) { - if (key < event.blockNumber) { + if (key < event.block.number) { this.l2BlockHashesStore.delete(key); } } break; } } + + private saveTag(name: L2BlockTag, block: L2BlockId) { + this.l2TipsStore.set(name, block.number); + if (block.hash) { + this.l2BlockHashesStore.set(block.number, block.hash); + } + } } diff --git a/yarn-project/kv-store/src/stores/l2_tips_store.ts b/yarn-project/kv-store/src/stores/l2_tips_store.ts index 0a0e399cb994..b07f52acc574 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store.ts @@ -56,17 +56,24 @@ export class L2TipsKVStore implements L2BlockStreamEventHandler, L2BlockStreamLo break; } case 'chain-pruned': - await this.l2TipsStore.set('latest', event.blockNumber); + await this.saveTag('latest', event.block); break; case 'chain-proven': - await this.l2TipsStore.set('proven', event.blockNumber); + await this.saveTag('proven', event.block); break; case 'chain-finalized': - await this.l2TipsStore.set('finalized', event.blockNumber); - for await (const key of this.l2BlockHashesStore.keysAsync({ end: event.blockNumber })) { + await this.saveTag('finalized', event.block); + for await (const key of this.l2BlockHashesStore.keysAsync({ end: event.block.number })) { await this.l2BlockHashesStore.delete(key); } break; } } + + private async saveTag(name: L2BlockTag, block: L2BlockId) { + await this.l2TipsStore.set(name, block.number); + if (block.hash) { + await this.l2BlockHashesStore.set(block.number, block.hash); + } + } } diff --git a/yarn-project/kv-store/src/stores/l2_tips_store_suite.test.ts b/yarn-project/kv-store/src/stores/l2_tips_store_suite.test.ts index 7562da39e594..8294e80c9279 100644 --- a/yarn-project/kv-store/src/stores/l2_tips_store_suite.test.ts +++ b/yarn-project/kv-store/src/stores/l2_tips_store_suite.test.ts @@ -1,6 +1,6 @@ import { times } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/fields'; -import type { L2Block, PublishedL2Block } from '@aztec/stdlib/block'; +import type { L2Block, L2BlockId, PublishedL2Block } from '@aztec/stdlib/block'; import type { BlockHeader } from '@aztec/stdlib/tx'; import { expect } from 'chai'; @@ -20,6 +20,11 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { signatures: [], }); + const makeBlockId = (number: number): L2BlockId => ({ + number, + hash: new Fr(number).toString(), + }); + const makeTip = (number: number) => ({ number, hash: number === 0 ? undefined : new Fr(number).toString() }); const makeTips = (latest: number, proven: number, finalized: number) => ({ @@ -36,9 +41,9 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { it('stores chain tips', async () => { await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(20, i => makeBlock(i + 1)) }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', blockNumber: 5 }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', blockNumber: 8 }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', blockNumber: 10 }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(5) }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(8) }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-pruned', block: makeBlockId(10) }); const tips = await tipsStore.getL2Tips(); expect(tips).to.deep.equal(makeTips(10, 8, 5)); @@ -57,8 +62,8 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { it('clears block hashes when setting finalized chain', async () => { await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: times(5, i => makeBlock(i + 1)) }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', blockNumber: 3 }); - await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', blockNumber: 3 }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-finalized', block: makeBlockId(3) }); const tips = await tipsStore.getL2Tips(); expect(tips).to.deep.equal(makeTips(5, 3, 3)); @@ -70,4 +75,13 @@ export function testL2TipsStore(makeTipsStore: () => Promise) { expect(await tipsStore.getL2BlockHash(4)).to.deep.equal(new Fr(4).toString()); expect(await tipsStore.getL2BlockHash(5)).to.deep.equal(new Fr(5).toString()); }); + + // Regression test for #13142 + it('does not blow up when setting proven chain on an unseen block number', async () => { + await tipsStore.handleBlockStreamEvent({ type: 'blocks-added', blocks: [makeBlock(5)] }); + await tipsStore.handleBlockStreamEvent({ type: 'chain-proven', block: makeBlockId(3) }); + + const tips = await tipsStore.getL2Tips(); + expect(tips).to.deep.equal(makeTips(5, 3, 0)); + }); } diff --git a/yarn-project/p2p/src/client/p2p_client.ts b/yarn-project/p2p/src/client/p2p_client.ts index 0b5c3d3d0dce..d1b0747cccb1 100644 --- a/yarn-project/p2p/src/client/p2p_client.ts +++ b/yarn-project/p2p/src/client/p2p_client.ts @@ -295,12 +295,12 @@ export class P2PClient break; case 'chain-proven': { const from = (await this.getSyncedProvenBlockNum()) + 1; - const limit = event.blockNumber - from + 1; + const limit = event.block.number - from + 1; await this.handleProvenL2Blocks(await this.l2BlockSource.getBlocks(from, limit)); break; } case 'chain-pruned': - await this.handlePruneL2Blocks(event.blockNumber); + await this.handlePruneL2Blocks(event.block.number); break; default: { const _: never = event; diff --git a/yarn-project/pxe/src/synchronizer/synchronizer.test.ts b/yarn-project/pxe/src/synchronizer/synchronizer.test.ts index 0439b24e44e1..51c4815b30d0 100644 --- a/yarn-project/pxe/src/synchronizer/synchronizer.test.ts +++ b/yarn-project/pxe/src/synchronizer/synchronizer.test.ts @@ -64,7 +64,7 @@ describe('Synchronizer', () => { type: 'blocks-added', blocks: await timesParallel(5, randomPublishedL2Block), }); - await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', blockNumber: 3 }); + await synchronizer.handleBlockStreamEvent({ type: 'chain-pruned', block: { number: 3, hash: '0x3' } }); expect(removeNotesAfter).toHaveBeenCalledWith(3); expect(unnullifyNotesAfter).toHaveBeenCalledWith(3, 4); diff --git a/yarn-project/pxe/src/synchronizer/synchronizer.ts b/yarn-project/pxe/src/synchronizer/synchronizer.ts index a4022a682474..f60048e98607 100644 --- a/yarn-project/pxe/src/synchronizer/synchronizer.ts +++ b/yarn-project/pxe/src/synchronizer/synchronizer.ts @@ -59,18 +59,18 @@ export class Synchronizer implements L2BlockStreamEventHandler { break; } case 'chain-pruned': { - this.log.warn(`Pruning data after block ${event.blockNumber} due to reorg`); + this.log.warn(`Pruning data after block ${event.block.number} due to reorg`); // We first unnullify and then remove so that unnullified notes that were created after the block number end up deleted. const lastSynchedBlockNumber = await this.syncDataProvider.getBlockNumber(); - await this.noteDataProvider.unnullifyNotesAfter(event.blockNumber, lastSynchedBlockNumber); - await this.noteDataProvider.removeNotesAfter(event.blockNumber); + await this.noteDataProvider.unnullifyNotesAfter(event.block.number, lastSynchedBlockNumber); + await this.noteDataProvider.removeNotesAfter(event.block.number); // Remove all note tagging indexes to force a full resync. This is suboptimal, but unless we track the // block number in which each index is used it's all we can do. await this.taggingDataProvider.resetNoteSyncData(); // Update the header to the last block. - const newHeader = await this.node.getBlockHeader(event.blockNumber); + const newHeader = await this.node.getBlockHeader(event.block.number); if (!newHeader) { - this.log.error(`Block header not found for block number ${event.blockNumber} during chain prune`); + this.log.error(`Block header not found for block number ${event.block.number} during chain prune`); } else { await this.syncDataProvider.setHeader(newHeader); } diff --git a/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.test.ts index 74da854e8f8a..54cd45b6c5d5 100644 --- a/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.test.ts @@ -6,7 +6,7 @@ import times from 'lodash.times'; import type { BlockHeader } from '../../tx/block_header.js'; import type { L2Block } from '../l2_block.js'; -import type { L2BlockSource, L2Tips } from '../l2_block_source.js'; +import type { L2BlockId, L2BlockSource, L2Tips } from '../l2_block_source.js'; import type { PublishedL2Block } from '../published_l2_block.js'; import { L2BlockStream, @@ -47,15 +47,17 @@ describe('L2BlockStream', () => { const makeHeader = (number: number) => mock({ hash: () => Promise.resolve(new Fr(number)) } as BlockHeader); + const makeBlockId = (number: number): L2BlockId => ({ number, hash: new Fr(number).toString() }); + const setRemoteTips = (latest_: number, proven?: number, finalized?: number) => { proven = proven ?? 0; finalized = finalized ?? 0; latest = latest_; blockSource.getL2Tips.mockResolvedValue({ - latest: { number: latest, hash: latest.toString() }, - proven: { number: proven, hash: proven.toString() }, - finalized: { number: finalized, hash: finalized.toString() }, + latest: { number: latest, hash: new Fr(latest).toString() }, + proven: { number: proven, hash: new Fr(proven).toString() }, + finalized: { number: finalized, hash: new Fr(finalized).toString() }, }); }; @@ -64,7 +66,9 @@ describe('L2BlockStream', () => { setRemoteTips(5); await blockStream.work(); - expect(handler.events).toEqual([{ type: 'blocks-added', blocks: times(5, i => makeBlock(i + 1)) }]); + expect(handler.events).toEqual([ + { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 1)) }, + ] satisfies L2BlockStreamEvent[]); }); it('pulls new blocks from offset', async () => { @@ -73,7 +77,9 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(blockSource.getPublishedBlocks).toHaveBeenCalledWith(11, 5, undefined); - expect(handler.events).toEqual([{ type: 'blocks-added', blocks: times(5, i => makeBlock(i + 11)) }]); + expect(handler.events).toEqual([ + { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 11)) }, + ] satisfies L2BlockStreamEvent[]); }); it('pulls new blocks in multiple batches', async () => { @@ -88,7 +94,7 @@ describe('L2BlockStream', () => { { type: 'blocks-added', blocks: times(10, i => makeBlock(i + 21)) }, { type: 'blocks-added', blocks: times(10, i => makeBlock(i + 31)) }, { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 41)) }, - ]); + ] satisfies L2BlockStreamEvent[]); }); it('halts pulling blocks if stopped', async () => { @@ -97,7 +103,9 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(blockSource.getPublishedBlocks).toHaveBeenCalledTimes(1); - expect(handler.events).toEqual([{ type: 'blocks-added', blocks: times(10, i => makeBlock(i + 1)) }]); + expect(handler.events).toEqual([ + { type: 'blocks-added', blocks: times(10, i => makeBlock(i + 1)) }, + ] satisfies L2BlockStreamEvent[]); }); it('halts on handler error and retries', async () => { @@ -124,9 +132,9 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ - { type: 'chain-pruned', blockNumber: 36 }, + { type: 'chain-pruned', block: makeBlockId(36) }, { type: 'blocks-added', blocks: times(9, i => makeBlock(i + 37)) }, - ]); + ] satisfies L2BlockStreamEvent[]); }); it('emits events for chain proven and finalized', async () => { @@ -138,9 +146,9 @@ describe('L2BlockStream', () => { await blockStream.work(); expect(handler.events).toEqual([ { type: 'blocks-added', blocks: times(5, i => makeBlock(i + 41)) }, - { type: 'chain-proven', blockNumber: 40 }, - { type: 'chain-finalized', blockNumber: 35 }, - ]); + { type: 'chain-proven', block: makeBlockId(40) }, + { type: 'chain-finalized', block: makeBlockId(35) }, + ] satisfies L2BlockStreamEvent[]); }); }); }); diff --git a/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.ts b/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.ts index 80987a873dfd..6f7c49b77f4f 100644 --- a/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.ts +++ b/yarn-project/stdlib/src/block/l2_block_downloader/l2_block_stream.ts @@ -66,12 +66,20 @@ export class L2BlockStream { // Check if there was a reorg and emit a chain-pruned event if so. let latestBlockNumber = localTips.latest.number; - while (!(await this.areBlockHashesEqualAt(latestBlockNumber, { sourceCache: [sourceTips.latest] }))) { + const sourceCache = new BlockHashCache([sourceTips.latest]); + while (!(await this.areBlockHashesEqualAt(latestBlockNumber, { sourceCache }))) { latestBlockNumber--; } + if (latestBlockNumber < localTips.latest.number) { this.log.verbose(`Reorg detected. Pruning blocks from ${latestBlockNumber + 1} to ${localTips.latest.number}.`); - await this.emitEvent({ type: 'chain-pruned', blockNumber: latestBlockNumber }); + await this.emitEvent({ + type: 'chain-pruned', + block: { + number: latestBlockNumber, + hash: sourceCache.get(latestBlockNumber) ?? (await this.getBlockHashFromSource(latestBlockNumber))!, + }, + }); } // If we are just starting, use the starting block number from the options. @@ -100,10 +108,13 @@ export class L2BlockStream { // Update the proven and finalized tips. if (localTips.proven !== undefined && sourceTips.proven.number !== localTips.proven.number) { - await this.emitEvent({ type: 'chain-proven', blockNumber: sourceTips.proven.number }); + await this.emitEvent({ + type: 'chain-proven', + block: sourceTips.proven, + }); } if (localTips.finalized !== undefined && sourceTips.finalized.number !== localTips.finalized.number) { - await this.emitEvent({ type: 'chain-finalized', blockNumber: sourceTips.finalized.number }); + await this.emitEvent({ type: 'chain-finalized', block: sourceTips.finalized }); } } catch (err: any) { if (err.name === 'AbortError') { @@ -118,29 +129,31 @@ export class L2BlockStream { * @param blockNumber - The block number to test. * @param args - A cache of data already requested from source, to avoid re-requesting it. */ - private async areBlockHashesEqualAt(blockNumber: number, args: { sourceCache: L2BlockId[] }) { + private async areBlockHashesEqualAt(blockNumber: number, args: { sourceCache: BlockHashCache }) { if (blockNumber === 0) { return true; } const localBlockHash = await this.localData.getL2BlockHash(blockNumber); - const sourceBlockHash = - args.sourceCache.find(id => id.number === blockNumber && id.hash)?.hash ?? - (await this.l2BlockSource - .getBlockHeader(blockNumber) - .then(h => h?.hash()) - .then(hash => hash?.toString())); - this.log.trace(`Comparing block hashes for block ${blockNumber}`, { - localBlockHash, - sourceBlockHash, - sourceCacheNumber: args.sourceCache[0]?.number, - sourceCacheHash: args.sourceCache[0]?.hash, - }); + const sourceBlockHashFromCache = args.sourceCache.get(blockNumber); + const sourceBlockHash = args.sourceCache.get(blockNumber) ?? (await this.getBlockHashFromSource(blockNumber)); + if (!sourceBlockHashFromCache && sourceBlockHash) { + args.sourceCache.add({ number: blockNumber, hash: sourceBlockHash }); + } + + this.log.trace(`Comparing block hashes for block ${blockNumber}`, { localBlockHash, sourceBlockHash }); return localBlockHash === sourceBlockHash; } + private getBlockHashFromSource(blockNumber: number) { + return this.l2BlockSource + .getBlockHeader(blockNumber) + .then(h => h?.hash()) + .then(hash => hash?.toString()); + } + private async emitEvent(event: L2BlockStreamEvent) { this.log.debug( - `Emitting ${event.type} (${event.type === 'blocks-added' ? event.blocks.length : event.blockNumber})`, + `Emitting ${event.type} (${event.type === 'blocks-added' ? event.blocks.length : event.block.number})`, ); await this.handler.handleBlockStreamEvent(event); if (!this.isRunning() && !this.isSyncing) { @@ -149,6 +162,26 @@ export class L2BlockStream { } } +class BlockHashCache { + private readonly cache: Map = new Map(); + + constructor(initial: L2BlockId[] = []) { + for (const block of initial) { + this.add(block); + } + } + + public add(block: L2BlockId) { + if (block.hash) { + this.cache.set(block.number, block.hash); + } + } + + public get(blockNumber: number) { + return this.cache.get(blockNumber); + } +} + /** Interface to the local view of the chain. Implemented by world-state and l2-tips-store. */ export interface L2BlockStreamLocalDataProvider { getL2BlockHash(number: number): Promise; @@ -161,23 +194,19 @@ export interface L2BlockStreamEventHandler { } export type L2BlockStreamEvent = - | { + | /** Emits blocks added to the chain. */ { type: 'blocks-added'; - /** New blocks added to the chain. */ blocks: PublishedL2Block[]; } - | { + | /** Reports last correct block (new tip of the unproven chain). */ { type: 'chain-pruned'; - /** Last correct block number (new tip of the unproven chain). */ - blockNumber: number; + block: L2BlockId; } - | { + | /** Reports new proven block. */ { type: 'chain-proven'; - /** New proven block number */ - blockNumber: number; + block: L2BlockId; } - | { + | /** Reports new finalized block (proven and finalized on L1). */ { type: 'chain-finalized'; - /** New finalized block number */ - blockNumber: number; + block: L2BlockId; }; diff --git a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts index 222c973460d8..82952f03aed3 100644 --- a/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts +++ b/yarn-project/world-state/src/synchronizer/server_world_state_synchronizer.ts @@ -234,13 +234,13 @@ export class ServerWorldStateSynchronizer await this.handleL2Blocks(event.blocks.map(b => b.block)); break; case 'chain-pruned': - await this.handleChainPruned(event.blockNumber); + await this.handleChainPruned(event.block.number); break; case 'chain-proven': - await this.handleChainProven(event.blockNumber); + await this.handleChainProven(event.block.number); break; case 'chain-finalized': - await this.handleChainFinalized(event.blockNumber); + await this.handleChainFinalized(event.block.number); break; } }