From e1588f06fb4c3320dc55ec2f1ed7edc1b845b333 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 28 Mar 2025 15:02:52 -0300 Subject: [PATCH 1/2] fix: Handle proven chain events referring to unseen blocks The L2BlockStream works by comparing a local and a remote block source. For simplicity, we had created an L2TipsStore that keeps track of "local" block tips seen, either in memory or storage. However, when using the in-memory one, if the block stream reported a proven block number that the tips store hadn't seen, the tip store would throw due to not having its block hash when it tried to retrieve it later. This fixes it by emitting the block hash along with the number from all block stream events, and storing them in the tips store. Fixes #13142 --- .../src/e2e_p2p/validators_sentinel.test.ts | 148 +++++++++++------- .../src/stores/l2_tips_memory_store.ts | 15 +- .../kv-store/src/stores/l2_tips_store.ts | 15 +- .../src/stores/l2_tips_store_suite.test.ts | 26 ++- yarn-project/p2p/src/client/p2p_client.ts | 4 +- .../pxe/src/synchronizer/synchronizer.test.ts | 2 +- .../pxe/src/synchronizer/synchronizer.ts | 10 +- .../l2_block_downloader/l2_block_stream.ts | 87 ++++++---- .../server_world_state_synchronizer.ts | 6 +- 9 files changed, 202 insertions(+), 111 deletions(-) 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.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; } } From 8a99e0ae9b04f3f155f587f74778568afcc8431e Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 28 Mar 2025 17:13:52 -0300 Subject: [PATCH 2/2] Fix test --- .../l2_block_stream.test.ts | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) 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[]); }); }); });