diff --git a/yarn-project/archiver/src/archiver/archiver.test.ts b/yarn-project/archiver/src/archiver/archiver.test.ts index 0ec87a3b780e..06b9bbdf8b83 100644 --- a/yarn-project/archiver/src/archiver/archiver.test.ts +++ b/yarn-project/archiver/src/archiver/archiver.test.ts @@ -74,9 +74,10 @@ describe('Archiver', () => { blocks = blockNumbers.map(x => L2Block.random(x, 4, x, x + 1, 2, 2)); - rollupRead = mock({ - archiveAt: (args: readonly [bigint]) => Promise.resolve(blocks[Number(args[0] - 1n)].archive.root.toString()), - }); + rollupRead = mock(); + rollupRead.archiveAt.mockImplementation((args: readonly [bigint]) => + Promise.resolve(blocks[Number(args[0] - 1n)].archive.root.toString()), + ); ((archiver as any).rollup as any).read = rollupRead; }); @@ -275,6 +276,78 @@ describe('Archiver', () => { expect(loggerSpy).toHaveBeenNthCalledWith(2, `No blocks to retrieve from ${1n} to ${50n}`); }, 10_000); + it('Handle L2 reorg', async () => { + const loggerSpy = jest.spyOn((archiver as any).log, 'verbose'); + + let latestBlockNum = await archiver.getBlockNumber(); + expect(latestBlockNum).toEqual(0); + + const numL2BlocksInTest = 2; + + const rollupTxs = blocks.map(makeRollupTx); + + publicClient.getBlockNumber.mockResolvedValueOnce(50n).mockResolvedValueOnce(100n).mockResolvedValueOnce(150n); + + // We will return status at first to have an empty round, then as if we have 2 pending blocks, and finally + // Just a single pending block returning a "failure" for the expected pending block + rollupRead.status + .mockResolvedValueOnce([0n, GENESIS_ROOT, 0n, GENESIS_ROOT, GENESIS_ROOT]) + .mockResolvedValueOnce([0n, GENESIS_ROOT, 2n, blocks[1].archive.root.toString(), GENESIS_ROOT]) + .mockResolvedValueOnce([0n, GENESIS_ROOT, 1n, blocks[0].archive.root.toString(), Fr.ZERO.toString()]); + + rollupRead.archiveAt + .mockResolvedValueOnce(blocks[0].archive.root.toString()) + .mockResolvedValueOnce(blocks[1].archive.root.toString()) + .mockResolvedValueOnce(Fr.ZERO.toString()); + + // This can look slightly odd, but we will need to do an empty request for the messages, and will entirely skip + // a call to the proposed blocks because of changes with status. + mockGetLogs({ + messageSent: [], + }); + mockGetLogs({ + messageSent: [makeMessageSentEvent(66n, 1n, 0n), makeMessageSentEvent(68n, 1n, 1n)], + L2BlockProposed: [ + makeL2BlockProposedEvent(70n, 1n, blocks[0].archive.root.toString()), + makeL2BlockProposedEvent(80n, 2n, blocks[1].archive.root.toString()), + ], + }); + mockGetLogs({ + messageSent: [], + }); + mockGetLogs({ + messageSent: [], + }); + + rollupTxs.forEach(tx => publicClient.getTransaction.mockResolvedValueOnce(tx)); + + await archiver.start(false); + + while ((await archiver.getBlockNumber()) !== numL2BlocksInTest) { + await sleep(100); + } + + latestBlockNum = await archiver.getBlockNumber(); + expect(latestBlockNum).toEqual(numL2BlocksInTest); + + // For some reason, this is 1-indexed. + expect(loggerSpy).toHaveBeenNthCalledWith( + 1, + `Retrieved no new L1 -> L2 messages between L1 blocks ${1n} and ${50}.`, + ); + expect(loggerSpy).toHaveBeenNthCalledWith(2, `No blocks to retrieve from ${1n} to ${50n}`); + + // Lets take a look to see if we can find re-org stuff! + await sleep(1000); + + expect(loggerSpy).toHaveBeenNthCalledWith(6, `L2 prune have occurred, unwind state`); + expect(loggerSpy).toHaveBeenNthCalledWith(7, `Unwinding 1 block from block 2`); + + // Should also see the block number be reduced + latestBlockNum = await archiver.getBlockNumber(); + expect(latestBlockNum).toEqual(numL2BlocksInTest - 1); + }, 10_000); + // logs should be created in order of how archiver syncs. const mockGetLogs = (logs: { messageSent?: ReturnType[]; diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index a74ec853ff6e..87b37acdaba6 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -56,7 +56,6 @@ import { type ArchiverDataStore } from './archiver_store.js'; import { type ArchiverConfig } from './config.js'; import { retrieveBlockFromRollup, retrieveL1ToL2Messages } from './data_retrieval.js'; import { ArchiverInstrumentation } from './instrumentation.js'; -import { type SingletonDataRetrieval } from './structs/data_retrieval.js'; /** * Helper interface to combine all sources this archiver implementation provides. @@ -199,11 +198,8 @@ export class Archiver implements ArchiveSource { * * This code does not handle reorgs. */ - const { - blocksSynchedTo = this.l1StartBlock, - messagesSynchedTo = this.l1StartBlock, - provenLogsSynchedTo = this.l1StartBlock, - } = await this.store.getSynchPoint(); + const { blocksSynchedTo = this.l1StartBlock, messagesSynchedTo = this.l1StartBlock } = + await this.store.getSynchPoint(); const currentL1BlockNumber = await this.publicClient.getBlockNumber(); // ********** Ensuring Consistency of data pulled from L1 ********** @@ -225,10 +221,7 @@ export class Archiver implements ArchiveSource { * in future but for the time being it should give us the guarantees that we need */ - await this.updateLastProvenL2Block(provenLogsSynchedTo, currentL1BlockNumber); - // ********** Events that are processed per L1 block ********** - await this.handleL1ToL2Messages(blockUntilSynced, messagesSynchedTo, currentL1BlockNumber); // ********** Events that are processed per L2 block ********** @@ -268,46 +261,84 @@ export class Archiver implements ArchiveSource { ); } - private async updateLastProvenL2Block(provenSynchedTo: bigint, currentL1BlockNumber: bigint) { - if (currentL1BlockNumber <= provenSynchedTo) { - return; - } - - const provenBlockNumber = await this.rollup.read.getProvenBlockNumber(); - if (provenBlockNumber) { - await this.store.setProvenL2BlockNumber({ - retrievedData: Number(provenBlockNumber), - lastProcessedL1BlockNumber: currentL1BlockNumber, - }); - } - } - private async handleL2blocks(blockUntilSynced: boolean, blocksSynchedTo: bigint, currentL1BlockNumber: bigint) { if (currentL1BlockNumber <= blocksSynchedTo) { return; } - const lastBlock = await this.getBlock(-1); - - const [, , pendingBlockNumber, pendingArchive, archiveOfMyBlock] = await this.rollup.read.status([ - BigInt(lastBlock?.number ?? 0), - ]); - - const noBlocksButInitial = lastBlock === undefined && pendingBlockNumber == 0n; - const noBlockSinceLast = - lastBlock && - pendingBlockNumber === BigInt(lastBlock.number) && - pendingArchive === lastBlock.archive.root.toString(); + const localPendingBlockNumber = BigInt(await this.getBlockNumber()); + const [provenBlockNumber, provenArchive, pendingBlockNumber, pendingArchive, archiveForLocalPendingBlockNumber] = + await this.rollup.read.status([localPendingBlockNumber]); + + const updateProvenBlock = async () => { + // Only update the proven block number if we are behind. And only if we have the state + if (provenBlockNumber > BigInt(await this.getProvenBlockNumber())) { + const localBlockForDestinationProvenBlockNumber = await this.getBlock(Number(provenBlockNumber)); + if ( + localBlockForDestinationProvenBlockNumber && + provenArchive === localBlockForDestinationProvenBlockNumber.archive.root.toString() + ) { + this.log.info(`Updating the proven block number to ${provenBlockNumber}`); + await this.store.setProvenL2BlockNumber(Number(provenBlockNumber)); + } + } + }; - if (noBlocksButInitial || noBlockSinceLast) { + // This is an edge case that we only hit if there are no proposed blocks. + const noBlocks = localPendingBlockNumber === 0n && pendingBlockNumber === 0n; + if (noBlocks) { await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber); this.log.verbose(`No blocks to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`); return; } - if (lastBlock && archiveOfMyBlock !== lastBlock.archive.root.toString()) { - // @todo Either `prune` have been called, or L1 have re-orged deep enough to remove a block. - // Issue#8620 and Issue#8621 + // Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there + // are any state that could be impacted by it. If we have no blocks, there is no impact. + if (localPendingBlockNumber > 0) { + const localPendingBlock = await this.getBlock(Number(localPendingBlockNumber)); + if (localPendingBlock === undefined) { + throw new Error(`Missing block ${localPendingBlockNumber}`); + } + + const noBlockSinceLast = localPendingBlock && pendingArchive === localPendingBlock.archive.root.toString(); + if (noBlockSinceLast) { + // While there have been no L2 blocks, there might have been a proof, so we will update if needed. + await updateProvenBlock(); + await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber); + this.log.verbose(`No blocks to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`); + return; + } + + const localPendingBlockInChain = archiveForLocalPendingBlockNumber === localPendingBlock.archive.root.toString(); + if (!localPendingBlockInChain) { + // If our local pending block tip is not in the chain on L1 a "prune" must have happened + // or the L1 have reorged. + // In any case, we have to figure out how far into the past the action will take us. + // For simplicity here, we will simply rewind until we end in a block that is also on the chain on L1. + this.log.verbose(`L2 prune have occurred, unwind state`); + + let tipAfterUnwind = localPendingBlockNumber; + while (true) { + const candidateBlock = await this.getBlock(Number(tipAfterUnwind)); + if (candidateBlock === undefined) { + break; + } + + const archiveAtContract = await this.rollup.read.archiveAt([BigInt(candidateBlock.number)]); + + if (archiveAtContract === candidateBlock.archive.root.toString()) { + break; + } + tipAfterUnwind--; + } + + const blocksToUnwind = localPendingBlockNumber - tipAfterUnwind; + this.log.verbose( + `Unwinding ${blocksToUnwind} block${blocksToUnwind > 1n ? 's' : ''} from block ${localPendingBlockNumber}`, + ); + + await this.store.unwindBlocks(Number(localPendingBlockNumber), Number(blocksToUnwind)); + } } this.log.debug(`Retrieving blocks from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`); @@ -321,7 +352,7 @@ export class Archiver implements ArchiveSource { ); if (retrievedBlocks.length === 0) { - await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber); + // We are not calling `setBlockSynchedL1BlockNumber` because it may cause sync issues if based off infura. this.log.verbose(`Retrieved no new blocks from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`); return; } @@ -365,6 +396,8 @@ export class Archiver implements ArchiveSource { const timer = new Timer(); await this.store.addBlocks(retrievedBlocks); + // Important that we update AFTER inserting the blocks. + await updateProvenBlock(); this.instrumentation.processNewBlocks( timer.ms() / retrievedBlocks.length, retrievedBlocks.map(b => b.data), @@ -476,7 +509,7 @@ export class Archiver implements ArchiveSource { /** * Gets an l2 block. - * @param number - The block number to return (inclusive). + * @param number - The block number to return. * @returns The requested L2 block. */ public async getBlock(number: number): Promise { @@ -484,6 +517,9 @@ export class Archiver implements ArchiveSource { if (number < 0) { number = await this.store.getSynchedL2BlockNumber(); } + if (number == 0) { + return undefined; + } const blocks = await this.store.getBlocks(number, 1); return blocks.length === 0 ? undefined : blocks[0].data; } @@ -554,8 +590,8 @@ export class Archiver implements ArchiveSource { } /** Forcefully updates the last proven block number. Use for testing. */ - public setProvenBlockNumber(block: SingletonDataRetrieval): Promise { - return this.store.setProvenL2BlockNumber(block); + public setProvenBlockNumber(blockNumber: number): Promise { + return this.store.setProvenL2BlockNumber(blockNumber); } public getContractClass(id: Fr): Promise { diff --git a/yarn-project/archiver/src/archiver/archiver_store.ts b/yarn-project/archiver/src/archiver/archiver_store.ts index 9ddbb8e10506..0da5c3664347 100644 --- a/yarn-project/archiver/src/archiver/archiver_store.ts +++ b/yarn-project/archiver/src/archiver/archiver_store.ts @@ -23,7 +23,7 @@ import { type UnconstrainedFunctionWithMembershipProof, } from '@aztec/types/contracts'; -import { type DataRetrieval, type SingletonDataRetrieval } from './structs/data_retrieval.js'; +import { type DataRetrieval } from './structs/data_retrieval.js'; import { type L1Published } from './structs/published.js'; /** @@ -50,6 +50,15 @@ export interface ArchiverDataStore { */ addBlocks(blocks: L1Published[]): Promise; + /** + * Unwinds blocks from the database + * @param from - The tip of the chain, passed for verification purposes, + * ensuring that we don't end up deleting something we did not intend + * @param blocksToUnwind - The number of blocks we are to unwind + * @returns True if the operation is successful + */ + unwindBlocks(from: number, blocksToUnwind: number): Promise; + /** * Gets up to `limit` amount of L2 blocks starting from `from`. * @param from - Number of the first block to return (inclusive). @@ -145,7 +154,7 @@ export interface ArchiverDataStore { * Stores the number of the latest proven L2 block processed. * @param l2BlockNumber - The number of the latest proven L2 block processed. */ - setProvenL2BlockNumber(l2BlockNumber: SingletonDataRetrieval): Promise; + setProvenL2BlockNumber(l2BlockNumber: number): Promise; /** * Stores the l1 block number that blocks have been synched until diff --git a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts index 4acaaeb9b39b..89f5ecd8cee4 100644 --- a/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts +++ b/yarn-project/archiver/src/archiver/archiver_store_test_suite.ts @@ -69,8 +69,8 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch await expect(store.getBlocks(1, 0)).rejects.toThrow('Invalid limit: 0'); }); - it('resets `from` to the first block if it is out of range', async () => { - await expect(store.getBlocks(INITIAL_L2_BLOCK_NUM - 100, 1)).resolves.toEqual(blocks.slice(0, 1)); + it('throws an error if `from` it is out of range', async () => { + await expect(store.getBlocks(INITIAL_L2_BLOCK_NUM - 100, 1)).rejects.toThrow('Invalid start: -99'); }); }); @@ -90,7 +90,6 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch await expect(store.getSynchPoint()).resolves.toEqual({ blocksSynchedTo: undefined, messagesSynchedTo: undefined, - provenLogsSynchedTo: undefined, } satisfies ArchiverL1SynchPoint); }); @@ -99,7 +98,6 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch await expect(store.getSynchPoint()).resolves.toEqual({ blocksSynchedTo: 19n, messagesSynchedTo: undefined, - provenLogsSynchedTo: undefined, } satisfies ArchiverL1SynchPoint); }); @@ -111,16 +109,6 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch await expect(store.getSynchPoint()).resolves.toEqual({ blocksSynchedTo: undefined, messagesSynchedTo: 1n, - provenLogsSynchedTo: undefined, - } satisfies ArchiverL1SynchPoint); - }); - - it('returns the L1 block number that most recently logged a proven block', async () => { - await store.setProvenL2BlockNumber({ lastProcessedL1BlockNumber: 3n, retrievedData: 5 }); - await expect(store.getSynchPoint()).resolves.toEqual({ - blocksSynchedTo: undefined, - messagesSynchedTo: undefined, - provenLogsSynchedTo: 3n, } satisfies ArchiverL1SynchPoint); }); }); diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts index 024df9f10659..38b37c32e045 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/block_store.ts @@ -26,6 +26,9 @@ export class BlockStore { /** Stores L1 block number in which the last processed L2 block was included */ #lastSynchedL1Block: AztecSingleton; + /** Stores l2 block number of the last proven block */ + #lastProvenL2Block: AztecSingleton; + /** Index mapping transaction hash (as a string) to its location in a block */ #txIndex: AztecMap; @@ -40,6 +43,7 @@ export class BlockStore { this.#txIndex = db.openMap('archiver_tx_index'); this.#contractIndex = db.openMap('archiver_contract_index'); this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block'); + this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block'); } /** @@ -73,6 +77,39 @@ export class BlockStore { }); } + /** + * Unwinds blocks from the database + * @param from - The tip of the chain, passed for verification purposes, + * ensuring that we don't end up deleting something we did not intend + * @param blocksToUnwind - The number of blocks we are to unwind + * @returns True if the operation is successful + */ + unwindBlocks(from: number, blocksToUnwind: number) { + return this.db.transaction(() => { + // We should only allow deleting the very last ye, otherwise we can really get some messy shit. + const last = this.getSynchedL2BlockNumber(); + if (from != last) { + throw new Error(`Can only remove the tip`); + } + + for (let i = 0; i < blocksToUnwind; i++) { + const blockNumber = from - i; + const block = this.getBlock(blockNumber); + + if (block === undefined) { + throw new Error(`Cannot remove block ${blockNumber} from the store, we don't have it`); + } + void this.#blocks.delete(block.data.number); + block.data.body.txEffects.forEach(tx => { + void this.#txIndex.delete(tx.txHash.toString()); + }); + void this.#blockBodies.delete(block.data.body.getTxsEffectsHash().toString('hex')); + } + + return true; + }); + } + /** * Gets up to `limit` amount of L2 blocks starting from `from`. * @param start - Number of the first block to return (inclusive). @@ -191,13 +228,21 @@ export class BlockStore { void this.#lastSynchedL1Block.set(l1BlockNumber); } + getProvenL2BlockNumber(): number { + return this.#lastProvenL2Block.get() ?? 0; + } + + setProvenL2BlockNumber(blockNumber: number) { + void this.#lastProvenL2Block.set(blockNumber); + } + #computeBlockRange(start: number, limit: number): Required, 'start' | 'end'>> { if (limit < 1) { throw new Error(`Invalid limit: ${limit}`); } if (start < INITIAL_L2_BLOCK_NUM) { - start = INITIAL_L2_BLOCK_NUM; + throw new Error(`Invalid start: ${start}`); } const end = start + limit; diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts index 0c01d3907245..a061e6c1c783 100644 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/kv_archiver_store/kv_archiver_store.ts @@ -26,7 +26,7 @@ import { } from '@aztec/types/contracts'; import { type ArchiverDataStore, type ArchiverL1SynchPoint } from '../archiver_store.js'; -import { type DataRetrieval, type SingletonDataRetrieval } from '../structs/data_retrieval.js'; +import { type DataRetrieval } from '../structs/data_retrieval.js'; import { type L1Published } from '../structs/published.js'; import { BlockStore } from './block_store.js'; import { ContractArtifactsStore } from './contract_artifacts_store.js'; @@ -34,14 +34,12 @@ import { ContractClassStore } from './contract_class_store.js'; import { ContractInstanceStore } from './contract_instance_store.js'; import { LogStore } from './log_store.js'; import { MessageStore } from './message_store.js'; -import { ProvenStore } from './proven_store.js'; /** * LMDB implementation of the ArchiverDataStore interface. */ export class KVArchiverDataStore implements ArchiverDataStore { #blockStore: BlockStore; - #provenStore: ProvenStore; #logStore: LogStore; #messageStore: MessageStore; #contractClassStore: ContractClassStore; @@ -52,7 +50,6 @@ export class KVArchiverDataStore implements ArchiverDataStore { constructor(db: AztecKVStore, logsMaxPageSize: number = 1000) { this.#blockStore = new BlockStore(db); - this.#provenStore = new ProvenStore(db); this.#logStore = new LogStore(db, this.#blockStore, logsMaxPageSize); this.#messageStore = new MessageStore(db); this.#contractClassStore = new ContractClassStore(db); @@ -105,6 +102,17 @@ export class KVArchiverDataStore implements ArchiverDataStore { return this.#blockStore.addBlocks(blocks); } + /** + * Unwinds blocks from the database + * @param from - The tip of the chain, passed for verification purposes, + * ensuring that we don't end up deleting something we did not intend + * @param blocksToUnwind - The number of blocks we are to unwind + * @returns True if the operation is successful + */ + unwindBlocks(from: number, blocksToUnwind: number): Promise { + return this.#blockStore.unwindBlocks(from, blocksToUnwind); + } + /** * Gets up to `limit` amount of L2 blocks starting from `from`. * @@ -228,11 +236,12 @@ export class KVArchiverDataStore implements ArchiverDataStore { } getProvenL2BlockNumber(): Promise { - return Promise.resolve(this.#provenStore.getProvenL2BlockNumber()); + return Promise.resolve(this.#blockStore.getProvenL2BlockNumber()); } - async setProvenL2BlockNumber(blockNumber: SingletonDataRetrieval) { - await this.#provenStore.setProvenL2BlockNumber(blockNumber); + setProvenL2BlockNumber(blockNumber: number) { + this.#blockStore.setProvenL2BlockNumber(blockNumber); + return Promise.resolve(); } setBlockSynchedL1BlockNumber(l1BlockNumber: bigint) { @@ -252,7 +261,6 @@ export class KVArchiverDataStore implements ArchiverDataStore { return Promise.resolve({ blocksSynchedTo: this.#blockStore.getSynchedL1BlockNumber(), messagesSynchedTo: this.#messageStore.getSynchedL1BlockNumber(), - provenLogsSynchedTo: this.#provenStore.getSynchedL1BlockNumber(), }); } } diff --git a/yarn-project/archiver/src/archiver/kv_archiver_store/proven_store.ts b/yarn-project/archiver/src/archiver/kv_archiver_store/proven_store.ts deleted file mode 100644 index d94b75b7ada4..000000000000 --- a/yarn-project/archiver/src/archiver/kv_archiver_store/proven_store.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { type AztecKVStore, type AztecSingleton } from '@aztec/kv-store'; - -import { type SingletonDataRetrieval } from '../structs/data_retrieval.js'; - -export class ProvenStore { - /** Stores L1 block number in which the last processed L2 block was included */ - #lastSynchedL1Block: AztecSingleton; - - /** Stores last proven L2 block number */ - #lastProvenL2Block: AztecSingleton; - - constructor(private db: AztecKVStore) { - this.#lastSynchedL1Block = db.openSingleton('archiver_last_l1_block_proven_logs'); - this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block'); - } - - /** - * Gets the most recent L1 block processed. - */ - getSynchedL1BlockNumber(): bigint | undefined { - return this.#lastSynchedL1Block.get(); - } - - getProvenL2BlockNumber(): number { - return this.#lastProvenL2Block.get() ?? 0; - } - - async setProvenL2BlockNumber(blockNumber: SingletonDataRetrieval) { - await this.db.transaction(() => { - void this.#lastProvenL2Block.set(blockNumber.retrievedData); - void this.#lastSynchedL1Block.set(blockNumber.lastProcessedL1BlockNumber); - }); - } -} diff --git a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts index ee17a6ed9ca2..028d871c13af 100644 --- a/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts +++ b/yarn-project/archiver/src/archiver/memory_archiver_store/memory_archiver_store.ts @@ -26,7 +26,7 @@ import { } from '@aztec/types/contracts'; import { type ArchiverDataStore, type ArchiverL1SynchPoint } from '../archiver_store.js'; -import { type DataRetrieval, type SingletonDataRetrieval } from '../structs/data_retrieval.js'; +import { type DataRetrieval } from '../structs/data_retrieval.js'; import { type L1Published } from '../structs/published.js'; import { L1ToL2MessageStore } from './l1_to_l2_message_store.js'; @@ -79,7 +79,6 @@ export class MemoryArchiverStore implements ArchiverDataStore { private lastL1BlockNewBlocks: bigint | undefined = undefined; private lastL1BlockNewMessages: bigint | undefined = undefined; - private lastL1BlockNewProvenLogs: bigint | undefined = undefined; private lastProvenL2BlockNumber: number = 0; @@ -160,6 +159,36 @@ export class MemoryArchiverStore implements ArchiverDataStore { return Promise.resolve(true); } + /** + * Unwinds blocks from the database + * @param from - The tip of the chain, passed for verification purposes, + * ensuring that we don't end up deleting something we did not intend + * @param blocksToUnwind - The number of blocks we are to unwind + * @returns True if the operation is successful + */ + public async unwindBlocks(from: number, blocksToUnwind: number): Promise { + const last = await this.getSynchedL2BlockNumber(); + if (from != last) { + throw new Error(`Can only remove the tip`); + } + + const blocks = await this.getBlocks(from - blocksToUnwind, blocksToUnwind); + if (blocks.length != blocksToUnwind) { + throw new Error(`Invalid number of blocks received ${blocks.length}`); + } + + while (blocks.length > 0) { + const block = blocks.pop(); + if (block === undefined) { + break; + } + this.l2Blocks.pop(); + block.data.body.txEffects.forEach(() => this.txEffects.pop()); + } + + return Promise.resolve(true); + } + /** * Append new logs to the store's list. * @param encryptedLogs - The encrypted logs to be added to the store. @@ -231,7 +260,11 @@ export class MemoryArchiverStore implements ArchiverDataStore { return Promise.reject(new Error(`Invalid limit: ${limit}`)); } - const fromIndex = Math.max(from - INITIAL_L2_BLOCK_NUM, 0); + if (from < INITIAL_L2_BLOCK_NUM) { + return Promise.reject(new Error(`Invalid start: ${from}`)); + } + + const fromIndex = from - INITIAL_L2_BLOCK_NUM; if (fromIndex >= this.l2Blocks.length) { return Promise.resolve([]); } @@ -414,9 +447,8 @@ export class MemoryArchiverStore implements ArchiverDataStore { return Promise.resolve(this.lastProvenL2BlockNumber); } - public setProvenL2BlockNumber(l2BlockNumber: SingletonDataRetrieval): Promise { - this.lastProvenL2BlockNumber = l2BlockNumber.retrievedData; - this.lastL1BlockNewProvenLogs = l2BlockNumber.lastProcessedL1BlockNumber; + public setProvenL2BlockNumber(l2BlockNumber: number): Promise { + this.lastProvenL2BlockNumber = l2BlockNumber; return Promise.resolve(); } @@ -434,7 +466,6 @@ export class MemoryArchiverStore implements ArchiverDataStore { return Promise.resolve({ blocksSynchedTo: this.lastL1BlockNewBlocks, messagesSynchedTo: this.lastL1BlockNewMessages, - provenLogsSynchedTo: this.lastL1BlockNewProvenLogs, }); } diff --git a/yarn-project/end-to-end/Earthfile b/yarn-project/end-to-end/Earthfile index 843e7eec2c20..7fc1f96b7bf8 100644 --- a/yarn-project/end-to-end/Earthfile +++ b/yarn-project/end-to-end/Earthfile @@ -113,6 +113,9 @@ e2e-p2p: e2e-l1-with-wall-time: DO +E2E_TEST --test=./src/e2e_l1_with_wall_time.test.ts +e2e-synching: + DO +E2E_TEST --test=./src/e2e_synching.test.ts + e2e-2-pxes: DO +E2E_TEST --test=./src/e2e_2_pxes.test.ts diff --git a/yarn-project/end-to-end/src/e2e_synching.test.ts b/yarn-project/end-to-end/src/e2e_synching.test.ts index 2cbb21530fa2..7db29b59ca79 100644 --- a/yarn-project/end-to-end/src/e2e_synching.test.ts +++ b/yarn-project/end-to-end/src/e2e_synching.test.ts @@ -33,6 +33,7 @@ * blockCount: 10, txCount: 9, complexity: Spam: {"numberOfBlocks":17, "syncTime":49.40888188171387} */ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; +import { createArchiver } from '@aztec/archiver'; import { AztecNodeService } from '@aztec/aztec-node'; import { type AccountWallet, @@ -44,6 +45,7 @@ import { GrumpkinScalar, computeSecretHash, createDebugLogger, + sleep, } from '@aztec/aztec.js'; // eslint-disable-next-line no-restricted-imports import { ExtendedNote, L2Block, Note, type TxHash } from '@aztec/circuit-types'; @@ -430,65 +432,106 @@ describe('e2e_synching', () => { await teardown(); }; - it.each(variants)('replay and then sync - %s', async (variant: TestVariant) => { - await testTheVariant(variant); - }); + describe('replay history and then do a fresh sync', () => { + it.each(variants)('vanilla - %s', async (variant: TestVariant) => { + await testTheVariant(variant); + }); - it('replay, then prune and only then perform an initial sync', async () => { - if (AZTEC_GENERATE_TEST_DATA) { - return; - } + describe('a wild prune appears', () => { + it('archiver catches reorg as it occur and deletes blocks', async () => { + if (AZTEC_GENERATE_TEST_DATA) { + return; + } + + const variant = variants[0]; + + const beforeSync = async (opts: Partial) => { + const rollup = getContract({ + address: opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: opts.deployL1ContractsValues!.walletClient, + }); + + const archiver = await createArchiver(opts.config!); + + const pendingBlockNumber = await rollup.read.getPendingBlockNumber(); + const assumeProvenThrough = pendingBlockNumber - BigInt(variant.blockCount) / 2n; + await rollup.write.setAssumeProvenThroughBlockNumber([assumeProvenThrough]); + + const timeliness = await rollup.read.TIMELINESS_PROVING_IN_SLOTS(); + const [, , slot] = await rollup.read.blocks([(await rollup.read.getProvenBlockNumber()) + 1n]); + const timeJumpTo = await rollup.read.getTimestampForSlot([slot + timeliness]); - const variant = variants[0]; + await opts.cheatCodes!.eth.warp(Number(timeJumpTo)); - const beforeSync = async (opts: Partial) => { - const rollup = getContract({ - address: opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress.toString(), - abi: RollupAbi, - client: opts.deployL1ContractsValues!.walletClient, + expect(await archiver.getBlockNumber()).toBeGreaterThan(Number(assumeProvenThrough)); + + await rollup.write.prune(); + + // We need to sleep a bit to make sure that we have caught the prune and deleted blocks. + await sleep(3000); + expect(await archiver.getBlockNumber()).toBe(Number(assumeProvenThrough)); + }; + await testTheVariant(variant, beforeSync); }); - const pendingBlockNumber = await rollup.read.getPendingBlockNumber(); - await rollup.write.setAssumeProvenThroughBlockNumber([pendingBlockNumber - BigInt(variant.blockCount) / 2n]); + it('fresh sync can extend chain', async () => { + if (AZTEC_GENERATE_TEST_DATA) { + return; + } - const timeliness = await rollup.read.TIMELINESS_PROVING_IN_SLOTS(); - const [, , slot] = await rollup.read.blocks([(await rollup.read.getProvenBlockNumber()) + 1n]); - const timeJumpTo = await rollup.read.getTimestampForSlot([slot + timeliness]); + const variant = variants[0]; - await opts.cheatCodes!.eth.warp(Number(timeJumpTo)); + const beforeSync = async (opts: Partial) => { + const rollup = getContract({ + address: opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: opts.deployL1ContractsValues!.walletClient, + }); - await rollup.write.prune(); - }; + const pendingBlockNumber = await rollup.read.getPendingBlockNumber(); + await rollup.write.setAssumeProvenThroughBlockNumber([pendingBlockNumber - BigInt(variant.blockCount) / 2n]); - // After we have synched the chain, we will publish a block. Here we are VERY interested in seeing the block number. - const afterSync = async (opts: Partial) => { - const watcher = new AnvilTestWatcher( - opts.cheatCodes!.eth, - opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress, - opts.deployL1ContractsValues!.publicClient, - ); - await watcher.start(); + const timeliness = await rollup.read.TIMELINESS_PROVING_IN_SLOTS(); + const [, , slot] = await rollup.read.blocks([(await rollup.read.getProvenBlockNumber()) + 1n]); + const timeJumpTo = await rollup.read.getTimestampForSlot([slot + timeliness]); - // The sync here could likely be avoided by using the node we just synched. - const aztecNode = await AztecNodeService.createAndSync(opts.config!, new NoopTelemetryClient()); - const sequencer = aztecNode.getSequencer(); + await opts.cheatCodes!.eth.warp(Number(timeJumpTo)); - const { pxe } = await setupPXEService(aztecNode!); + await rollup.write.prune(); + }; - variant.setPXE(pxe); + // After we have synched the chain, we will publish a block. Here we are VERY interested in seeing the block number. + const afterSync = async (opts: Partial) => { + const watcher = new AnvilTestWatcher( + opts.cheatCodes!.eth, + opts.deployL1ContractsValues!.l1ContractAddresses.rollupAddress, + opts.deployL1ContractsValues!.publicClient, + ); + await watcher.start(); - const blockBefore = await aztecNode.getBlock(await aztecNode.getBlockNumber()); + // The sync here could likely be avoided by using the node we just synched. + const aztecNode = await AztecNodeService.createAndSync(opts.config!, new NoopTelemetryClient()); + const sequencer = aztecNode.getSequencer(); - sequencer?.updateSequencerConfig({ minTxsPerBlock: variant.txCount, maxTxsPerBlock: variant.txCount }); - const txs = await variant.createAndSendTxs(); - await Promise.all(txs.map(tx => tx.wait({ timeout: 1200 }))); + const { pxe } = await setupPXEService(aztecNode!); - const blockAfter = await aztecNode.getBlock(await aztecNode.getBlockNumber()); + variant.setPXE(pxe); - expect(blockAfter!.number).toEqual(blockBefore!.number + 1); - expect(blockAfter!.header.lastArchive).toEqual(blockBefore!.archive); - }; + const blockBefore = await aztecNode.getBlock(await aztecNode.getBlockNumber()); - await testTheVariant(variant, beforeSync, afterSync); + sequencer?.updateSequencerConfig({ minTxsPerBlock: variant.txCount, maxTxsPerBlock: variant.txCount }); + const txs = await variant.createAndSendTxs(); + await Promise.all(txs.map(tx => tx.wait({ timeout: 1200 }))); + + const blockAfter = await aztecNode.getBlock(await aztecNode.getBlockNumber()); + + expect(blockAfter!.number).toEqual(blockBefore!.number + 1); + expect(blockAfter!.header.lastArchive).toEqual(blockBefore!.archive); + }; + + await testTheVariant(variant, beforeSync, afterSync); + }); + }); }); });