Skip to content

Commit

Permalink
feat: PXE handles reorgs
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
  • Loading branch information
spalladino committed Nov 15, 2024
1 parent 6fec1dc commit 1b55bed
Show file tree
Hide file tree
Showing 45 changed files with 627 additions and 295 deletions.
5 changes: 3 additions & 2 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 Down Expand Up @@ -589,7 +590,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 @@ -933,7 +934,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 Down
3 changes: 2 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 Down
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
Expand Up @@ -6,7 +6,6 @@ import {
type L2BlockL2Logs,
type LogFilter,
type LogType,
type TxEffect,
type TxHash,
type TxReceipt,
type TxScopedL2Log,
Expand Down Expand Up @@ -159,7 +158,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExtendedUnencryptedL2Log,
type FromLogType,
type GetUnencryptedLogsResponse,
type InBlock,
type InboxLeaf,
type L2Block,
type L2BlockL2Logs,
Expand All @@ -17,6 +18,7 @@ import {
TxReceipt,
TxScopedL2Log,
type UnencryptedL2BlockL2Logs,
wrapInBlock,
} from '@aztec/circuit-types';
import {
type ContractClassPublic,
Expand Down Expand Up @@ -50,7 +52,7 @@ export class MemoryArchiverStore implements ArchiverDataStore {
/**
* An array containing all the tx effects in the L2 blocks that have been fetched so far.
*/
private txEffects: TxEffect[] = [];
private txEffects: InBlock<TxEffect>[] = [];

private noteEncryptedLogsPerBlock: Map<number, EncryptedNoteL2BlockL2Logs> = new Map();

Expand Down Expand Up @@ -181,7 +183,7 @@ export class MemoryArchiverStore implements ArchiverDataStore {

this.lastL1BlockNewBlocks = blocks[blocks.length - 1].l1.blockNumber;
this.l2Blocks.push(...blocks);
this.txEffects.push(...blocks.flatMap(b => b.data.body.txEffects));
this.txEffects.push(...blocks.flatMap(b => b.data.body.txEffects.map(txEffect => wrapInBlock(txEffect, b.data))));

return Promise.resolve(true);
}
Expand Down Expand Up @@ -365,8 +367,8 @@ export class MemoryArchiverStore implements ArchiverDataStore {
* @param txHash - The txHash of the tx effect.
* @returns The requested tx effect.
*/
public getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
const txEffect = this.txEffects.find(tx => tx.txHash.equals(txHash));
public getTxEffect(txHash: TxHash): Promise<InBlock<TxEffect> | undefined> {
const txEffect = this.txEffects.find(tx => tx.data.txHash.equals(txHash));
return Promise.resolve(txEffect);
}

Expand Down
10 changes: 8 additions & 2 deletions yarn-project/archiver/src/test/mock_l2_block_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,14 @@ export class MockL2BlockSource implements L2BlockSource {
* @returns The requested tx effect.
*/
public getTxEffect(txHash: TxHash) {
const txEffect = this.l2Blocks.flatMap(b => b.body.txEffects).find(tx => tx.txHash.equals(txHash));
return Promise.resolve(txEffect);
const match = this.l2Blocks
.flatMap(b => b.body.txEffects.map(tx => [tx, b] as const))
.find(([tx]) => tx.txHash.equals(txHash));
if (!match) {
return Promise.resolve(undefined);
}
const [txEffect, block] = match;
return Promise.resolve({ data: txEffect, l2BlockNumber: block.number, l2BlockHash: block.hash().toString() });
}

/**
Expand Down
13 changes: 9 additions & 4 deletions yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type EpochProofQuote,
type FromLogType,
type GetUnencryptedLogsResponse,
type InBlock,
type L1ToL2MessageSource,
type L2Block,
type L2BlockL2Logs,
Expand Down Expand Up @@ -117,14 +118,18 @@ export class AztecNodeService implements AztecNode {
this.log.info(message);
}

addEpochProofQuote(quote: EpochProofQuote): Promise<void> {
public addEpochProofQuote(quote: EpochProofQuote): Promise<void> {
return Promise.resolve(this.p2pClient.addEpochProofQuote(quote));
}

getEpochProofQuotes(epoch: bigint): Promise<EpochProofQuote[]> {
public getEpochProofQuotes(epoch: bigint): Promise<EpochProofQuote[]> {
return this.p2pClient.getEpochProofQuotes(epoch);
}

public getL2Tips() {
return this.blockSource.getL2Tips();
}

/**
* initializes the Aztec Node, wait for component to sync.
* @param config - The configuration to be used by the aztec node.
Expand Down Expand Up @@ -372,7 +377,7 @@ export class AztecNodeService implements AztecNode {
return txReceipt;
}

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

Expand Down Expand Up @@ -708,7 +713,7 @@ export class AztecNodeService implements AztecNode {
* Returns the currently committed block header, or the initial header if no blocks have been produced.
* @returns The current committed block header.
*/
public async getHeader(blockNumber: L2BlockNumber = 'latest'): Promise<Header> {
public async getBlockHeader(blockNumber: L2BlockNumber = 'latest'): Promise<Header> {
return (
(await this.getBlock(blockNumber === 'latest' ? -1 : blockNumber))?.header ??
this.worldStateSynchronizer.getCommitted().getInitialHeader()
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/aztec.js/src/contract/sent_tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class SentTx {
}
if (opts?.debug) {
const txHash = await this.getTxHash();
const tx = (await this.pxe.getTxEffect(txHash))!;
const { data: tx } = (await this.pxe.getTxEffect(txHash))!;
receipt.debugInfo = {
noteHashes: tx.noteHashes,
nullifiers: tx.nullifiers,
Expand Down
3 changes: 1 addition & 2 deletions yarn-project/aztec.js/src/wallet/base_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
type SiblingPath,
type SyncStatus,
type Tx,
type TxEffect,
type TxExecutionRequest,
type TxHash,
type TxProvingResult,
Expand Down Expand Up @@ -122,7 +121,7 @@ export abstract class BaseWallet implements Wallet {
sendTx(tx: Tx): Promise<TxHash> {
return this.pxe.sendTx(tx);
}
getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
getTxEffect(txHash: TxHash) {
return this.pxe.getTxEffect(txHash);
}
getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
Expand Down
36 changes: 36 additions & 0 deletions yarn-project/circuit-types/src/in_block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Fr } from '@aztec/circuits.js';
import { schemas } from '@aztec/foundation/schemas';

import { type ZodTypeAny, z } from 'zod';

import { type L2Block } from './l2_block.js';

export type InBlock<T> = {
l2BlockNumber: number;
l2BlockHash: string;
data: T;
};

export function randomInBlock<T>(data: T): InBlock<T> {
return {
data,
l2BlockNumber: Math.floor(Math.random() * 1000),
l2BlockHash: Fr.random().toString(),
};
}

export function wrapInBlock<T>(data: T, block: L2Block): InBlock<T> {
return {
data,
l2BlockNumber: block.number,
l2BlockHash: block.hash().toString(),
};
}

export function inBlockSchemaFor<T extends ZodTypeAny>(schema: T) {
return z.object({
data: schema,
l2BlockNumber: schemas.Integer,
l2BlockHash: z.string(),
});
}
1 change: 1 addition & 0 deletions yarn-project/circuit-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './simulation_error.js';
export * from './tx/index.js';
export * from './tx_effect.js';
export * from './tx_execution_request.js';
export * from './in_block.js';
7 changes: 4 additions & 3 deletions yarn-project/circuit-types/src/interfaces/archiver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { readFileSync } from 'fs';
import omit from 'lodash.omit';
import { resolve } from 'path';

import { type InBlock } from '../in_block.js';
import { L2Block } from '../l2_block.js';
import { type L2Tips } from '../l2_block_source.js';
import { ExtendedUnencryptedL2Log } from '../logs/extended_unencrypted_l2_log.js';
Expand Down Expand Up @@ -106,7 +107,7 @@ describe('ArchiverApiSchema', () => {

it('getTxEffect', async () => {
const result = await context.client.getTxEffect(new TxHash(Buffer.alloc(32, 1)));
expect(result).toBeInstanceOf(TxEffect);
expect(result!.data).toBeInstanceOf(TxEffect);
});

it('getSettledTxReceipt', async () => {
Expand Down Expand Up @@ -261,9 +262,9 @@ class MockArchiver implements ArchiverApi {
getBlocks(from: number, _limit: number, _proven?: boolean | undefined): Promise<L2Block[]> {
return Promise.resolve([L2Block.random(from)]);
}
getTxEffect(_txHash: TxHash): Promise<TxEffect | undefined> {
getTxEffect(_txHash: TxHash): Promise<InBlock<TxEffect> | undefined> {
expect(_txHash).toBeInstanceOf(TxHash);
return Promise.resolve(TxEffect.empty());
return Promise.resolve({ l2BlockNumber: 1, l2BlockHash: '0x12', data: TxEffect.random() });
}
getSettledTxReceipt(txHash: TxHash): Promise<TxReceipt | undefined> {
expect(txHash).toBeInstanceOf(TxHash);
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/circuit-types/src/interfaces/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type ApiSchemaFor, optional, schemas } from '@aztec/foundation/schemas'

import { z } from 'zod';

import { inBlockSchemaFor } from '../in_block.js';
import { L2Block } from '../l2_block.js';
import { type L2BlockSource, L2TipsSchema } from '../l2_block_source.js';
import { GetUnencryptedLogsResponseSchema, TxScopedL2Log } from '../logs/get_logs_response.js';
Expand Down Expand Up @@ -42,7 +43,7 @@ export const ArchiverApiSchema: ApiSchemaFor<ArchiverApi> = {
.function()
.args(schemas.Integer, schemas.Integer, optional(z.boolean()))
.returns(z.array(L2Block.schema)),
getTxEffect: z.function().args(TxHash.schema).returns(TxEffect.schema.optional()),
getTxEffect: z.function().args(TxHash.schema).returns(inBlockSchemaFor(TxEffect.schema).optional()),
getSettledTxReceipt: z.function().args(TxHash.schema).returns(TxReceipt.schema.optional()),
getL2SlotNumber: z.function().args().returns(schemas.BigInt),
getL2EpochNumber: z.function().args().returns(schemas.BigInt),
Expand Down
30 changes: 24 additions & 6 deletions yarn-project/circuit-types/src/interfaces/aztec-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import omit from 'lodash.omit';
import times from 'lodash.times';
import { resolve } from 'path';

import { type InBlock } from '../in_block.js';
import { L2Block } from '../l2_block.js';
import { type L2Tips } from '../l2_block_source.js';
import { ExtendedUnencryptedL2Log } from '../logs/extended_unencrypted_l2_log.js';
import { type GetUnencryptedLogsResponse, TxScopedL2Log } from '../logs/get_logs_response.js';
import {
Expand Down Expand Up @@ -80,6 +82,15 @@ describe('AztecNodeApiSchema', () => {
expect([...tested].sort()).toEqual(all.sort());
});

it('getL2Tips', async () => {
const result = await context.client.getL2Tips();
expect(result).toEqual({
latest: { number: 1, hash: `0x01` },
proven: { number: 1, hash: `0x01` },
finalized: { number: 1, hash: `0x01` },
});
});

it('findLeavesIndexes', async () => {
const response = await context.client.findLeavesIndexes(1, MerkleTreeId.ARCHIVE, [Fr.random(), Fr.random()]);
expect(response).toEqual([1n, undefined]);
Expand Down Expand Up @@ -231,7 +242,7 @@ describe('AztecNodeApiSchema', () => {

it('getTxEffect', async () => {
const response = await context.client.getTxEffect(TxHash.random());
expect(response).toBeInstanceOf(TxEffect);
expect(response!.data).toBeInstanceOf(TxEffect);
});

it('getPendingTxs', async () => {
Expand All @@ -254,8 +265,8 @@ describe('AztecNodeApiSchema', () => {
expect(response).toBeInstanceOf(Fr);
});

it('getHeader', async () => {
const response = await context.client.getHeader();
it('getBlockHeader', async () => {
const response = await context.client.getBlockHeader();
expect(response).toBeInstanceOf(Header);
});

Expand Down Expand Up @@ -318,6 +329,13 @@ describe('AztecNodeApiSchema', () => {
class MockAztecNode implements AztecNode {
constructor(private artifact: ContractArtifact) {}

getL2Tips(): Promise<L2Tips> {
return Promise.resolve({
latest: { number: 1, hash: `0x01` },
proven: { number: 1, hash: `0x01` },
finalized: { number: 1, hash: `0x01` },
});
}
findLeavesIndexes(
blockNumber: number | 'latest',
treeId: MerkleTreeId,
Expand Down Expand Up @@ -472,9 +490,9 @@ class MockAztecNode implements AztecNode {
expect(txHash).toBeInstanceOf(TxHash);
return Promise.resolve(TxReceipt.empty());
}
getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
getTxEffect(txHash: TxHash): Promise<InBlock<TxEffect> | undefined> {
expect(txHash).toBeInstanceOf(TxHash);
return Promise.resolve(TxEffect.random());
return Promise.resolve({ l2BlockNumber: 1, l2BlockHash: '0x12', data: TxEffect.random() });
}
getPendingTxs(): Promise<Tx[]> {
return Promise.resolve([Tx.random()]);
Expand All @@ -491,7 +509,7 @@ class MockAztecNode implements AztecNode {
expect(slot).toBeInstanceOf(Fr);
return Promise.resolve(Fr.random());
}
getHeader(_blockNumber?: number | 'latest' | undefined): Promise<Header> {
getBlockHeader(_blockNumber?: number | 'latest' | undefined): Promise<Header> {
return Promise.resolve(Header.empty());
}
simulatePublicCalls(tx: Tx): Promise<PublicSimulationOutput> {
Expand Down
Loading

0 comments on commit 1b55bed

Please sign in to comment.