Skip to content

Commit

Permalink
feat: PXE handles reorgs (#9913)
Browse files Browse the repository at this point in the history
- Archiver and node return L2 block number and hash for `getTxEffect`
- PXE database stores L2 block number and hash for each note
- PXE database exposes a method for pruning notes after a given block
number
- PXE synchronizer uses L2 block stream to detect reorgs and clean up
notes

Pending: reorg'ing nullifiers.

Fixes #8463

---------

Co-authored-by: thunkar <[email protected]>
Co-authored-by: Nicolás Venturo <[email protected]>
  • Loading branch information
3 people authored Nov 20, 2024
1 parent 280d169 commit aafef9c
Show file tree
Hide file tree
Showing 50 changed files with 1,125 additions and 335 deletions.
30 changes: 27 additions & 3 deletions yarn-project/archiver/src/archiver/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type EncryptedL2Log,
type FromLogType,
type GetUnencryptedLogsResponse,
type InBlock,
type InboxLeaf,
type L1ToL2MessageSource,
type L2Block,
Expand All @@ -12,6 +13,7 @@ import {
type L2Tips,
type LogFilter,
type LogType,
type NullifierWithBlockSource,
type TxEffect,
type TxHash,
type TxReceipt,
Expand Down Expand Up @@ -73,7 +75,11 @@ import { type L1Published } from './structs/published.js';
/**
* Helper interface to combine all sources this archiver implementation provides.
*/
export type ArchiveSource = L2BlockSource & L2LogsSource & ContractDataSource & L1ToL2MessageSource;
export type ArchiveSource = L2BlockSource &
L2LogsSource &
ContractDataSource &
L1ToL2MessageSource &
NullifierWithBlockSource;

/**
* Pulls L2 blocks in a non-blocking manner and provides interface for their retrieval.
Expand Down Expand Up @@ -589,7 +595,7 @@ export class Archiver implements ArchiveSource {
}
}

public getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
public getTxEffect(txHash: TxHash) {
return this.store.getTxEffect(txHash);
}

Expand Down Expand Up @@ -643,6 +649,17 @@ export class Archiver implements ArchiveSource {
return this.store.getLogsByTags(tags);
}

/**
* Returns the provided nullifier indexes scoped to the block
* they were first included in, or undefined if they're not present in the tree
* @param blockNumber Max block number to search for the nullifiers
* @param nullifiers Nullifiers to get
* @returns The block scoped indexes of the provided nullifiers, or undefined if the nullifier doesn't exist in the tree
*/
findNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]): Promise<(InBlock<bigint> | undefined)[]> {
return this.store.findNullifiersIndexesWithBlock(blockNumber, nullifiers);
}

/**
* Gets unencrypted logs based on the provided filter.
* @param filter - The filter to apply to the logs.
Expand Down Expand Up @@ -770,6 +787,9 @@ class ArchiverStoreHelper
ArchiverDataStore,
| 'addLogs'
| 'deleteLogs'
| 'addNullifiers'
| 'deleteNullifiers'
| 'addContractClasses'
| 'deleteContractClasses'
| 'addContractInstances'
| 'deleteContractInstances'
Expand Down Expand Up @@ -904,6 +924,7 @@ class ArchiverStoreHelper
).every(Boolean);
}),
)),
this.store.addNullifiers(blocks.map(block => block.data)),
this.store.addBlocks(blocks),
].every(Boolean);
}
Expand Down Expand Up @@ -943,7 +964,7 @@ class ArchiverStoreHelper
getBlockHeaders(from: number, limit: number): Promise<Header[]> {
return this.store.getBlockHeaders(from, limit);
}
getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
getTxEffect(txHash: TxHash): Promise<InBlock<TxEffect> | undefined> {
return this.store.getTxEffect(txHash);
}
getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
Expand All @@ -968,6 +989,9 @@ class ArchiverStoreHelper
getLogsByTags(tags: Fr[]): Promise<TxScopedL2Log[][]> {
return this.store.getLogsByTags(tags);
}
findNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]): Promise<(InBlock<bigint> | undefined)[]> {
return this.store.findNullifiersIndexesWithBlock(blockNumber, nullifiers);
}
getUnencryptedLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse> {
return this.store.getUnencryptedLogs(filter);
}
Expand Down
20 changes: 19 additions & 1 deletion yarn-project/archiver/src/archiver/archiver_store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type FromLogType,
type GetUnencryptedLogsResponse,
type InBlock,
type InboxLeaf,
type L2Block,
type L2BlockL2Logs,
Expand Down Expand Up @@ -79,7 +80,7 @@ export interface ArchiverDataStore {
* @param txHash - The txHash of the tx corresponding to the tx effect.
* @returns The requested tx effect (or undefined if not found).
*/
getTxEffect(txHash: TxHash): Promise<TxEffect | undefined>;
getTxEffect(txHash: TxHash): Promise<InBlock<TxEffect> | undefined>;

/**
* Gets a receipt of a settled tx.
Expand All @@ -96,6 +97,23 @@ export interface ArchiverDataStore {
addLogs(blocks: L2Block[]): Promise<boolean>;
deleteLogs(blocks: L2Block[]): Promise<boolean>;

/**
* Append new nullifiers to the store's list.
* @param blocks - The blocks for which to add the nullifiers.
* @returns True if the operation is successful.
*/
addNullifiers(blocks: L2Block[]): Promise<boolean>;
deleteNullifiers(blocks: L2Block[]): Promise<boolean>;

/**
* Returns the provided nullifier indexes scoped to the block
* they were first included in, or undefined if they're not present in the tree
* @param blockNumber Max block number to search for the nullifiers
* @param nullifiers Nullifiers to get
* @returns The block scoped indexes of the provided nullifiers, or undefined if the nullifier doesn't exist in the tree
*/
findNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]): Promise<(InBlock<bigint> | undefined)[]>;

/**
* Append L1 to L2 messages to the store.
* @param messages - The L1 to L2 messages to be added to the store and the last processed L1 block.
Expand Down
80 changes: 67 additions & 13 deletions yarn-project/archiver/src/archiver/archiver_store_test_suite.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InboxLeaf, L2Block, LogId, LogType, TxHash } from '@aztec/circuit-types';
import { InboxLeaf, L2Block, LogId, LogType, TxHash, wrapInBlock } from '@aztec/circuit-types';
import '@aztec/circuit-types/jest';
import {
AztecAddress,
Expand All @@ -7,6 +7,7 @@ import {
Fr,
INITIAL_L2_BLOCK_NUM,
L1_TO_L2_MSG_SUBTREE_HEIGHT,
MAX_NULLIFIERS_PER_TX,
SerializableContractInstance,
} from '@aztec/circuits.js';
import {
Expand Down Expand Up @@ -191,14 +192,14 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
});

it.each([
() => blocks[0].data.body.txEffects[0],
() => blocks[9].data.body.txEffects[3],
() => blocks[3].data.body.txEffects[1],
() => blocks[5].data.body.txEffects[2],
() => blocks[1].data.body.txEffects[0],
() => wrapInBlock(blocks[0].data.body.txEffects[0], blocks[0].data),
() => wrapInBlock(blocks[9].data.body.txEffects[3], blocks[9].data),
() => wrapInBlock(blocks[3].data.body.txEffects[1], blocks[3].data),
() => wrapInBlock(blocks[5].data.body.txEffects[2], blocks[5].data),
() => wrapInBlock(blocks[1].data.body.txEffects[0], blocks[1].data),
])('retrieves a previously stored transaction', async getExpectedTx => {
const expectedTx = getExpectedTx();
const actualTx = await store.getTxEffect(expectedTx.txHash);
const actualTx = await store.getTxEffect(expectedTx.data.txHash);
expect(actualTx).toEqual(expectedTx);
});

Expand All @@ -207,16 +208,16 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
});

it.each([
() => blocks[0].data.body.txEffects[0],
() => blocks[9].data.body.txEffects[3],
() => blocks[3].data.body.txEffects[1],
() => blocks[5].data.body.txEffects[2],
() => blocks[1].data.body.txEffects[0],
() => wrapInBlock(blocks[0].data.body.txEffects[0], blocks[0].data),
() => wrapInBlock(blocks[9].data.body.txEffects[3], blocks[9].data),
() => wrapInBlock(blocks[3].data.body.txEffects[1], blocks[3].data),
() => wrapInBlock(blocks[5].data.body.txEffects[2], blocks[5].data),
() => wrapInBlock(blocks[1].data.body.txEffects[0], blocks[1].data),
])('tries to retrieves a previously stored transaction after deleted', async getExpectedTx => {
await store.unwindBlocks(blocks.length, blocks.length);

const expectedTx = getExpectedTx();
const actualTx = await store.getTxEffect(expectedTx.txHash);
const actualTx = await store.getTxEffect(expectedTx.data.txHash);
expect(actualTx).toEqual(undefined);
});

Expand Down Expand Up @@ -705,5 +706,58 @@ export function describeArchiverDataStore(testName: string, getStore: () => Arch
}
});
});

describe('findNullifiersIndexesWithBlock', () => {
let blocks: L2Block[];
const numBlocks = 10;
const nullifiersPerBlock = new Map<number, Fr[]>();

beforeEach(() => {
blocks = times(numBlocks, (index: number) => L2Block.random(index + 1, 1));

blocks.forEach((block, blockIndex) => {
nullifiersPerBlock.set(
blockIndex,
block.body.txEffects.flatMap(txEffect => txEffect.nullifiers),
);
});
});

it('returns wrapped nullifiers with blocks if they exist', async () => {
await store.addNullifiers(blocks);
const nullifiersToRetrieve = [...nullifiersPerBlock.get(0)!, ...nullifiersPerBlock.get(5)!, Fr.random()];
const blockScopedNullifiers = await store.findNullifiersIndexesWithBlock(10, nullifiersToRetrieve);

expect(blockScopedNullifiers).toHaveLength(nullifiersToRetrieve.length);
const [undefinedNullifier] = blockScopedNullifiers.slice(-1);
const realNullifiers = blockScopedNullifiers.slice(0, -1);
realNullifiers.forEach((blockScopedNullifier, index) => {
expect(blockScopedNullifier).not.toBeUndefined();
const { data, l2BlockNumber } = blockScopedNullifier!;
expect(data).toEqual(expect.any(BigInt));
expect(l2BlockNumber).toEqual(index < MAX_NULLIFIERS_PER_TX ? 1 : 6);
});
expect(undefinedNullifier).toBeUndefined();
});

it('returns wrapped nullifiers filtering by blockNumber', async () => {
await store.addNullifiers(blocks);
const nullifiersToRetrieve = [...nullifiersPerBlock.get(0)!, ...nullifiersPerBlock.get(5)!];
const blockScopedNullifiers = await store.findNullifiersIndexesWithBlock(5, nullifiersToRetrieve);

expect(blockScopedNullifiers).toHaveLength(nullifiersToRetrieve.length);
const undefinedNullifiers = blockScopedNullifiers.slice(-MAX_NULLIFIERS_PER_TX);
const realNullifiers = blockScopedNullifiers.slice(0, -MAX_NULLIFIERS_PER_TX);
realNullifiers.forEach(blockScopedNullifier => {
expect(blockScopedNullifier).not.toBeUndefined();
const { data, l2BlockNumber } = blockScopedNullifier!;
expect(data).toEqual(expect.any(BigInt));
expect(l2BlockNumber).toEqual(1);
});
undefinedNullifiers.forEach(undefinedNullifier => {
expect(undefinedNullifier).toBeUndefined();
});
});
});
});
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, L2Block, type TxEffect, type TxHash, TxReceipt } from '@aztec/circuit-types';
import { Body, type InBlock, L2Block, type TxEffect, type TxHash, TxReceipt } from '@aztec/circuit-types';
import { AppendOnlyTreeSnapshot, type AztecAddress, Header, INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js';
import { createDebugLogger } from '@aztec/foundation/log';
import { type AztecKVStore, type AztecMap, type AztecSingleton, type Range } from '@aztec/kv-store';
Expand Down Expand Up @@ -170,14 +170,22 @@ export class BlockStore {
* @param txHash - The txHash of the tx corresponding to the tx effect.
* @returns The requested tx effect (or undefined if not found).
*/
getTxEffect(txHash: TxHash): TxEffect | undefined {
getTxEffect(txHash: TxHash): InBlock<TxEffect> | undefined {
const [blockNumber, txIndex] = this.getTxLocation(txHash) ?? [];
if (typeof blockNumber !== 'number' || typeof txIndex !== 'number') {
return undefined;
}

const block = this.getBlock(blockNumber);
return block?.data.body.txEffects[txIndex];
if (!block) {
return undefined;
}

return {
data: block.data.body.txEffects[txIndex],
l2BlockNumber: block.data.number,
l2BlockHash: block.data.hash().toString(),
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
type FromLogType,
type GetUnencryptedLogsResponse,
type InBlock,
type InboxLeaf,
type L2Block,
type L2BlockL2Logs,
type LogFilter,
type LogType,
type TxEffect,
type TxHash,
type TxReceipt,
type TxScopedL2Log,
Expand All @@ -33,13 +33,15 @@ 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 { NullifierStore } from './nullifier_store.js';

/**
* LMDB implementation of the ArchiverDataStore interface.
*/
export class KVArchiverDataStore implements ArchiverDataStore {
#blockStore: BlockStore;
#logStore: LogStore;
#nullifierStore: NullifierStore;
#messageStore: MessageStore;
#contractClassStore: ContractClassStore;
#contractInstanceStore: ContractInstanceStore;
Expand All @@ -54,6 +56,7 @@ export class KVArchiverDataStore implements ArchiverDataStore {
this.#contractClassStore = new ContractClassStore(db);
this.#contractInstanceStore = new ContractInstanceStore(db);
this.#contractArtifactStore = new ContractArtifactsStore(db);
this.#nullifierStore = new NullifierStore(db);
}

getContractArtifact(address: AztecAddress): Promise<ContractArtifact | undefined> {
Expand Down Expand Up @@ -159,7 +162,7 @@ export class KVArchiverDataStore implements ArchiverDataStore {
* @param txHash - The txHash of the tx corresponding to the tx effect.
* @returns The requested tx effect (or undefined if not found).
*/
getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
getTxEffect(txHash: TxHash) {
return Promise.resolve(this.#blockStore.getTxEffect(txHash));
}

Expand All @@ -185,6 +188,23 @@ export class KVArchiverDataStore implements ArchiverDataStore {
return this.#logStore.deleteLogs(blocks);
}

/**
* Append new nullifiers to the store's list.
* @param blocks - The blocks for which to add the nullifiers.
* @returns True if the operation is successful.
*/
addNullifiers(blocks: L2Block[]): Promise<boolean> {
return this.#nullifierStore.addNullifiers(blocks);
}

deleteNullifiers(blocks: L2Block[]): Promise<boolean> {
return this.#nullifierStore.deleteNullifiers(blocks);
}

findNullifiersIndexesWithBlock(blockNumber: number, nullifiers: Fr[]): Promise<(InBlock<bigint> | undefined)[]> {
return this.#nullifierStore.findNullifiersIndexesWithBlock(blockNumber, nullifiers);
}

getTotalL1ToL2MessageCount(): Promise<bigint> {
return Promise.resolve(this.#messageStore.getTotalL1ToL2MessageCount());
}
Expand Down
Loading

0 comments on commit aafef9c

Please sign in to comment.