From c48ee00103e77a8822b681a51f62106241384b7b Mon Sep 17 00:00:00 2001 From: PhilWindle <60546371+PhilWindle@users.noreply.github.com> Date: Wed, 18 Mar 2026 02:06:20 +0000 Subject: [PATCH 1/2] cherry-pick: fix: only delete logs from rolled-back blocks, not entire tag (A-686) (#21687) (with conflicts) --- .../src/store/kv_archiver_store.test.ts | 146 +++++++++++++++++- yarn-project/archiver/src/store/log_store.ts | 53 +++++-- .../stdlib/src/logs/tx_scoped_l2_log.test.ts | 17 ++ .../stdlib/src/logs/tx_scoped_l2_log.ts | 16 ++ 4 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 yarn-project/stdlib/src/logs/tx_scoped_l2_log.test.ts diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts index d05044ded8d2..5392a94b38fe 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -1799,19 +1799,153 @@ describe('KVArchiverDataStore', () => { }); }); +<<<<<<< HEAD it('deleteLogs', async () => { const block = publishedCheckpoints[0].checkpoint.blocks[0]; await store.addProposedBlocks([block]); await expect(store.addLogs([block])).resolves.toEqual(true); +======= + describe('deleteLogs', () => { + it('deletes public logs for a block', async () => { + const block = publishedCheckpoints[0].checkpoint.blocks[0]; + await store.addProposedBlock(block); + await expect(store.addLogs([block])).resolves.toEqual(true); +>>>>>>> 946ddb14a4 (fix: only delete logs from rolled-back blocks, not entire tag (A-686) (#21687)) + + expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual( + block.body.txEffects.map(txEffect => txEffect.publicLogs).flat().length, + ); + + await store.deleteLogs([block]); - expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual( - block.body.txEffects.map(txEffect => txEffect.publicLogs).flat().length, - ); + expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual(0); + }); - // This one is a pain for memory as we would never want to just delete memory in the middle. - await store.deleteLogs([block]); + it('deletes contract class logs for a block', async () => { + // Create a block that explicitly has contract class logs + const block = await L2Block.random(BlockNumber(1), { + txsPerBlock: 2, + txOptions: { numContractClassLogs: 1 }, + state: makeStateForBlock(1, 2), + }); + await store.addProposedBlock(block); + await store.addLogs([block]); + + const logsBefore = await store.getContractClassLogs({ fromBlock: BlockNumber(1) }); + expect(logsBefore.logs.length).toBeGreaterThan(0); + + await store.deleteLogs([block]); + + const logsAfter = await store.getContractClassLogs({ fromBlock: BlockNumber(1) }); + expect(logsAfter.logs.length).toEqual(0); + }); + + it('retains private logs from non-reorged block when same tag appears in reorged block', async () => { + const sharedTag = makePrivateLogTag(1, 0, 0); + + // Block 1 with a private log using sharedTag + const cp1 = await makeCheckpointWithLogs(1, { + numTxsPerBlock: 1, + privateLogs: { numLogsPerTx: 1 }, + }); + const block1 = cp1.checkpoint.blocks[0]; + + // Block 2 with a private log using the SAME tag + const cp2 = await makeCheckpointWithLogs(2, { + previousArchive: block1.archive, + numTxsPerBlock: 1, + privateLogs: { numLogsPerTx: 1 }, + }); + const block2 = cp2.checkpoint.blocks[0]; + // Override block2's private log tag to match block1's + block2.body.txEffects[0].privateLogs[0] = makePrivateLog(sharedTag); + + await addProposedBlocks(store, [block1, block2], { force: true }); + await store.addLogs([block1, block2]); + + // Both blocks' logs should be present + const logsBefore = await store.getPrivateLogsByTags([sharedTag]); + expect(logsBefore[0]).toHaveLength(2); + + // Reorg: delete block 2 + await store.deleteLogs([block2]); + + // Block 1's log should still be present + const logsAfter = await store.getPrivateLogsByTags([sharedTag]); + expect(logsAfter[0]).toHaveLength(1); + expect(logsAfter[0][0].blockNumber).toEqual(1); + }); - expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual(0); + it('retains public logs from non-reorged block when same tag appears in reorged block', async () => { + const contractAddress = AztecAddress.fromNumber(543254); + const sharedTag = makePublicLogTag(1, 0, 0); + + // Block 1 with a public log using sharedTag + const cp1 = await makeCheckpointWithLogs(1, { + numTxsPerBlock: 1, + publicLogs: { numLogsPerTx: 1, contractAddress }, + }); + const block1 = cp1.checkpoint.blocks[0]; + + // Block 2 with a public log using the SAME tag from the same contract + const cp2 = await makeCheckpointWithLogs(2, { + previousArchive: block1.archive, + numTxsPerBlock: 1, + publicLogs: { numLogsPerTx: 1, contractAddress }, + }); + const block2 = cp2.checkpoint.blocks[0]; + // Override block2's public log tag to match block1's + block2.body.txEffects[0].publicLogs[0] = makePublicLog(sharedTag, contractAddress); + + await addProposedBlocks(store, [block1, block2], { force: true }); + await store.addLogs([block1, block2]); + + // Both blocks' logs should be present + const logsBefore = await store.getPublicLogsByTagsFromContract(contractAddress, [sharedTag]); + expect(logsBefore[0]).toHaveLength(2); + + // Reorg: delete block 2 + await store.deleteLogs([block2]); + + // Block 1's log should still be present + const logsAfter = await store.getPublicLogsByTagsFromContract(contractAddress, [sharedTag]); + expect(logsAfter[0]).toHaveLength(1); + expect(logsAfter[0][0].blockNumber).toEqual(1); + }); + + it('deletes multiple blocks at once', async () => { + const cp1 = await makeCheckpointWithLogs(1, { + numTxsPerBlock: 2, + privateLogs: { numLogsPerTx: 1 }, + publicLogs: { numLogsPerTx: 1 }, + }); + const block1 = cp1.checkpoint.blocks[0]; + + const cp2 = await makeCheckpointWithLogs(2, { + previousArchive: block1.archive, + numTxsPerBlock: 2, + privateLogs: { numLogsPerTx: 1 }, + publicLogs: { numLogsPerTx: 1 }, + }); + const block2 = cp2.checkpoint.blocks[0]; + + await addProposedBlocks(store, [block1, block2], { force: true }); + await store.addLogs([block1, block2]); + + // Verify logs exist + expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toBeGreaterThan(0); + + // Delete both blocks at once + await store.deleteLogs([block1, block2]); + + expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual(0); + }); + + it('is a no-op when deleting blocks with no logs', async () => { + const block = publishedCheckpoints[0].checkpoint.blocks[0]; + // Don't add logs, just try to delete + await expect(store.deleteLogs([block])).resolves.toEqual(true); + }); }); describe('getTxEffect', () => { diff --git a/yarn-project/archiver/src/store/log_store.ts b/yarn-project/archiver/src/store/log_store.ts index e389cba458e2..b633a53bf42f 100644 --- a/yarn-project/archiver/src/store/log_store.ts +++ b/yarn-project/archiver/src/store/log_store.ts @@ -1,6 +1,6 @@ import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; -import { filterAsync } from '@aztec/foundation/collection'; +import { compactArray, filterAsync } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize'; @@ -290,18 +290,49 @@ export class LogStore { deleteLogs(blocks: L2Block[]): Promise { return this.db.transactionAsync(async () => { - await Promise.all( - blocks.map(async block => { - // Delete private logs - const privateKeys = (await this.#privateLogKeysByBlock.getAsync(block.number)) ?? []; - await Promise.all(privateKeys.map(tag => this.#privateLogsByTag.delete(tag))); - - // Delete public logs - const publicKeys = (await this.#publicLogKeysByBlock.getAsync(block.number)) ?? []; - await Promise.all(publicKeys.map(key => this.#publicLogsByContractAndTag.delete(key))); - }), + const blockNumbers = new Set(blocks.map(block => block.number)); + const firstBlockToDelete = Math.min(...blockNumbers); + + // Collect all unique private tags across all blocks being deleted + const allPrivateTags = new Set( + compactArray(await Promise.all(blocks.map(block => this.#privateLogKeysByBlock.getAsync(block.number)))).flat(), + ); + + // Trim private logs: for each tag, delete all instances including and after the first block being deleted. + // This hinges on the invariant that logs for a given tag are always inserted in order of block number, which is enforced in #addPrivateLogs. + for (const tag of allPrivateTags) { + const existing = await this.#privateLogsByTag.getAsync(tag); + if (existing === undefined || existing.length === 0) { + continue; + } + const lastIndexToKeep = existing.findLastIndex( + buf => TxScopedL2Log.getBlockNumberFromBuffer(buf) < firstBlockToDelete, + ); + const remaining = existing.slice(0, lastIndexToKeep + 1); + await (remaining.length > 0 ? this.#privateLogsByTag.set(tag, remaining) : this.#privateLogsByTag.delete(tag)); + } + + // Collect all unique public keys across all blocks being deleted + const allPublicKeys = new Set( + compactArray(await Promise.all(blocks.map(block => this.#publicLogKeysByBlock.getAsync(block.number)))).flat(), ); + // And do the same as we did with private logs + for (const key of allPublicKeys) { + const existing = await this.#publicLogsByContractAndTag.getAsync(key); + if (existing === undefined || existing.length === 0) { + continue; + } + const lastIndexToKeep = existing.findLastIndex( + buf => TxScopedL2Log.getBlockNumberFromBuffer(buf) < firstBlockToDelete, + ); + const remaining = existing.slice(0, lastIndexToKeep + 1); + await (remaining.length > 0 + ? this.#publicLogsByContractAndTag.set(key, remaining) + : this.#publicLogsByContractAndTag.delete(key)); + } + + // After trimming the tagged logs, we can delete the block-level keys that track which tags are in which blocks. await Promise.all( blocks.map(block => Promise.all([ diff --git a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.test.ts b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.test.ts new file mode 100644 index 000000000000..d7b42331e3cf --- /dev/null +++ b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.test.ts @@ -0,0 +1,17 @@ +import { TxScopedL2Log } from './tx_scoped_l2_log.js'; + +describe('TxScopedL2Log', () => { + it('should serialize and deserialize correctly', () => { + const log = TxScopedL2Log.random(); + const buffer = log.toBuffer(); + const deserializedLog = TxScopedL2Log.fromBuffer(buffer); + expect(deserializedLog.equals(log)).toBe(true); + }); + + it('should extract block number from buffer correctly', () => { + const log = TxScopedL2Log.random(); + const buffer = log.toBuffer(); + const blockNumber = TxScopedL2Log.getBlockNumberFromBuffer(buffer); + expect(blockNumber).toBe(log.blockNumber); + }); +}); diff --git a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts index 31481f4ab49d..7e9264ac94de 100644 --- a/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts +++ b/yarn-project/stdlib/src/logs/tx_scoped_l2_log.ts @@ -1,4 +1,5 @@ import { BlockNumber, BlockNumberSchema } from '@aztec/foundation/branded-types'; +import { times } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { schemas as foundationSchemas } from '@aztec/foundation/schemas'; import { @@ -83,6 +84,21 @@ export class TxScopedL2Log { return new TxScopedL2Log(txHash, blockNumber, blockTimestamp, logData, noteHashes, firstNullifier); } + static getBlockNumberFromBuffer(buffer: Buffer) { + return BlockNumber(buffer.readUint32BE(Fr.SIZE_IN_BYTES)); + } + + static random() { + return new TxScopedL2Log( + TxHash.fromField(Fr.random()), + BlockNumber(Math.floor(Math.random() * 100000) + 1), + BigInt(Math.floor(Date.now() / 1000)), + times(3, Fr.random), + times(3, Fr.random), + Fr.random(), + ); + } + equals(other: TxScopedL2Log) { return ( this.txHash.equals(other.txHash) && From c2a952c9c5b0f7b1b875a71cb774e42a2108cea7 Mon Sep 17 00:00:00 2001 From: AztecBot Date: Wed, 18 Mar 2026 02:10:37 +0000 Subject: [PATCH 2/2] fix: resolve cherry-pick conflicts - adapt addProposedBlock to v4 addProposedBlocks API --- .../src/store/kv_archiver_store.test.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/yarn-project/archiver/src/store/kv_archiver_store.test.ts b/yarn-project/archiver/src/store/kv_archiver_store.test.ts index 5392a94b38fe..328dd8d5137f 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.test.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.test.ts @@ -1799,18 +1799,11 @@ describe('KVArchiverDataStore', () => { }); }); -<<<<<<< HEAD - it('deleteLogs', async () => { - const block = publishedCheckpoints[0].checkpoint.blocks[0]; - await store.addProposedBlocks([block]); - await expect(store.addLogs([block])).resolves.toEqual(true); -======= describe('deleteLogs', () => { it('deletes public logs for a block', async () => { const block = publishedCheckpoints[0].checkpoint.blocks[0]; - await store.addProposedBlock(block); + await store.addProposedBlocks([block]); await expect(store.addLogs([block])).resolves.toEqual(true); ->>>>>>> 946ddb14a4 (fix: only delete logs from rolled-back blocks, not entire tag (A-686) (#21687)) expect((await store.getPublicLogs({ fromBlock: BlockNumber(1) })).logs.length).toEqual( block.body.txEffects.map(txEffect => txEffect.publicLogs).flat().length, @@ -1828,7 +1821,7 @@ describe('KVArchiverDataStore', () => { txOptions: { numContractClassLogs: 1 }, state: makeStateForBlock(1, 2), }); - await store.addProposedBlock(block); + await store.addProposedBlocks([block]); await store.addLogs([block]); const logsBefore = await store.getContractClassLogs({ fromBlock: BlockNumber(1) }); @@ -1860,7 +1853,7 @@ describe('KVArchiverDataStore', () => { // Override block2's private log tag to match block1's block2.body.txEffects[0].privateLogs[0] = makePrivateLog(sharedTag); - await addProposedBlocks(store, [block1, block2], { force: true }); + await store.addProposedBlocks([block1, block2], { force: true }); await store.addLogs([block1, block2]); // Both blocks' logs should be present @@ -1897,7 +1890,7 @@ describe('KVArchiverDataStore', () => { // Override block2's public log tag to match block1's block2.body.txEffects[0].publicLogs[0] = makePublicLog(sharedTag, contractAddress); - await addProposedBlocks(store, [block1, block2], { force: true }); + await store.addProposedBlocks([block1, block2], { force: true }); await store.addLogs([block1, block2]); // Both blocks' logs should be present @@ -1929,7 +1922,7 @@ describe('KVArchiverDataStore', () => { }); const block2 = cp2.checkpoint.blocks[0]; - await addProposedBlocks(store, [block1, block2], { force: true }); + await store.addProposedBlocks([block1, block2], { force: true }); await store.addLogs([block1, block2]); // Verify logs exist