Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion yarn-project/archiver/src/archiver-misc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
import { EthAddress } from '@aztec/foundation/eth-address';
import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import { BlockHeader } from '@aztec/stdlib/tx';
import { getTelemetryClient } from '@aztec/telemetry-client';

import { EventEmitter } from 'events';
Expand Down Expand Up @@ -57,7 +58,9 @@ describe('Archiver misc', () => {
const instrumentation = mock<ArchiverInstrumentation>({ isEnabled: () => true, tracer });
const archiverStore = createArchiverDataStores(await openTmpStore('archiver_misc_test'), { logsMaxPageSize: 1000 });
const events = new EventEmitter() as ArchiverEmitter;
const l2TipsCache = new L2TipsCache(archiverStore.blocks);
const initialHeader = BlockHeader.empty();
const initialBlockHash = await initialHeader.hash();
const l2TipsCache = new L2TipsCache(archiverStore.blocks, initialBlockHash);

archiver = new Archiver(
publicClient,
Expand All @@ -77,6 +80,8 @@ describe('Archiver misc', () => {
l1Constants,
synchronizer,
events,
initialHeader,
initialBlockHash,
l2TipsCache,
);
});
Expand Down
144 changes: 111 additions & 33 deletions yarn-project/archiver/src/archiver-store.test.ts

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion yarn-project/archiver/src/archiver-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type { ProposedCheckpointInput } from '@aztec/stdlib/checkpoint';
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
import { CheckpointHeader } from '@aztec/stdlib/rollup';
import { BlockHeader } from '@aztec/stdlib/tx';
import { getTelemetryClient } from '@aztec/telemetry-client';

import { jest } from '@jest/globals';
Expand Down Expand Up @@ -123,7 +124,9 @@ describe('Archiver Sync', () => {
const events = new EventEmitter() as ArchiverEmitter;

// Create L2 tips cache shared by archiver and synchronizer
const l2TipsCache = new L2TipsCache(archiverStore.blocks);
const initialHeader = BlockHeader.empty();
const initialBlockHash = await initialHeader.hash();
const l2TipsCache = new L2TipsCache(archiverStore.blocks, initialBlockHash);

// Create the L1 synchronizer
synchronizer = new ArchiverL1Synchronizer(
Expand Down Expand Up @@ -156,6 +159,8 @@ describe('Archiver Sync', () => {
l1Constants,
synchronizer,
events,
initialHeader,
initialBlockHash,
l2TipsCache,
);
});
Expand Down
10 changes: 7 additions & 3 deletions yarn-project/archiver/src/archiver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { RunningPromise, makeLoggingErrorHandler } from '@aztec/foundation/runni
import { DateProvider, elapsed } from '@aztec/foundation/timer';
import {
type ArchiverEmitter,
type BlockHash,
L2Block,
type L2BlockSink,
type L2Tips,
Expand All @@ -28,6 +29,7 @@ import {
getTimestampForSlot,
getTimestampRangeForEpoch,
} from '@aztec/stdlib/epoch-helpers';
import type { BlockHeader } from '@aztec/stdlib/tx';
import { type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';

import { type ArchiverConfig, mapArchiverConfig } from './config.js';
Expand Down Expand Up @@ -140,17 +142,19 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra
},
synchronizer: ArchiverL1Synchronizer,
events: ArchiverEmitter,
l2TipsCache?: L2TipsCache,
initialHeader: BlockHeader,
initialBlockHash: BlockHash,
l2TipsCache: L2TipsCache,
private readonly log: Logger = createLogger('archiver'),
) {
super(dataStores, l1Constants);
super(dataStores, l1Constants, initialHeader, initialBlockHash, l1Constants.genesisArchiveRoot);

this.tracer = instrumentation.tracer;
this.instrumentation = instrumentation;
this.initialSyncPromise = promiseWithResolvers();
this.synchronizer = synchronizer;
this.events = events;
this.l2TipsCache = l2TipsCache ?? new L2TipsCache(this.dataStores.blocks);
this.l2TipsCache = l2TipsCache;
this.updater = new ArchiverDataStoreUpdater(this.dataStores, this.l2TipsCache, {
rollupManaLimit: l1Constants.rollupManaLimit,
});
Expand Down
16 changes: 13 additions & 3 deletions yarn-project/archiver/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import { createStore } from '@aztec/kv-store/lmdb-v2';
import { protocolContractNames } from '@aztec/protocol-contracts';
import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle';
import { FunctionType, decodeFunctionSignature } from '@aztec/stdlib/abi';
import type { ArchiverEmitter } from '@aztec/stdlib/block';
import type { ArchiverEmitter, BlockHash } from '@aztec/stdlib/block';
import { type ContractClassPublicWithCommitment, computePublicBytecodeCommitment } from '@aztec/stdlib/contract';
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
import type { BlockHeader } from '@aztec/stdlib/tx';
import { getTelemetryClient } from '@aztec/telemetry-client';

import { EventEmitter } from 'events';
Expand Down Expand Up @@ -46,12 +47,17 @@ export async function createArchiverStore(
* @param config - The archiver configuration.
* @param deps - The archiver dependencies (blobClient, epochCache, dateProvider, telemetry).
* @param opts - The options.
* @param initialHeader - The genesis block header from world-state, used to answer block-0 queries.
* @param initialBlockHash - Precomputed hash of `initialHeader`. Hoisted to the caller so the archiver
* can expose `getGenesisBlockHash()` synchronously.
* @returns The local archiver.
*/
export async function createArchiver(
config: ArchiverConfig & DataStoreConfig,
deps: ArchiverDeps,
opts: { blockUntilSync: boolean } = { blockUntilSync: true },
initialHeader: BlockHeader,
initialBlockHash: BlockHash,
): Promise<Archiver> {
const archiverStore = await createArchiverStore(config);
await registerProtocolContracts(archiverStore);
Expand Down Expand Up @@ -133,8 +139,10 @@ export async function createArchiver(
// Create the event emitter that will be shared by archiver and synchronizer
const events = new EventEmitter() as ArchiverEmitter;

// Create L2 tips cache shared by archiver and synchronizer
const l2TipsCache = new L2TipsCache(archiverStore.blocks);
// Create L2 tips cache shared by archiver and synchronizer. The genesis block hash is dynamic —
// it depends on the injected initial header (genesisTimestamp + prefilled state). Hoisted to the
// caller so we can pass the same value to the archiver and expose it via `getGenesisBlockHash()`.
const l2TipsCache = new L2TipsCache(archiverStore.blocks, initialBlockHash);

// Create the L1 synchronizer
const synchronizer = new ArchiverL1Synchronizer(
Expand Down Expand Up @@ -167,6 +175,8 @@ export async function createArchiver(
l1Constants,
synchronizer,
events,
initialHeader,
initialBlockHash,
l2TipsCache,
);

Expand Down
130 changes: 116 additions & 14 deletions yarn-project/archiver/src/modules/data_source_base.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { BlockNumber, CheckpointNumber, type EpochNumber, type SlotNumber } from '@aztec/foundation/branded-types';
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
import {
BlockNumber,
CheckpointNumber,
type EpochNumber,
IndexWithinCheckpoint,
type SlotNumber,
} from '@aztec/foundation/branded-types';
import type { Fr } from '@aztec/foundation/curves/bn254';
import type { EthAddress } from '@aztec/foundation/eth-address';
import type { FunctionSelector } from '@aztec/stdlib/abi';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
type BlockData,
type BlockHash,
type BlockQuery,
type BlockTag,
type BlocksQuery,
Body,
L2Block,
type L2Tips,
} from '@aztec/stdlib/block';
Expand All @@ -25,14 +34,22 @@ import type { L2LogsSource } from '@aztec/stdlib/interfaces/server';
import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
import type { IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
import type { BlockHeader, IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
import type { UInt64 } from '@aztec/stdlib/types';

import type { ArchiverDataSource } from '../interfaces.js';
import type { ResolvedBlockQuery, ResolvedBlocksQuery } from '../store/block_store.js';
import type { ArchiverDataStores } from '../store/data_stores.js';
import type { ValidateCheckpointResult } from './validation.js';

/**
* Sentinel returned by {@link ArchiverDataSourceBase#resolveBlockQuery} when a query resolves
* to the genesis block. Forces single-block lookup methods to take the genesis branch
* explicitly rather than silently falling through to the BlockStore (which never has a block 0).
*/
type GenesisBlockQuery = { genesis: true };

/**
* Abstract base class implementing ArchiverDataSource using a bundle of archiver substores.
* Provides implementations for all read-side methods and declares abstract methods for
Expand All @@ -41,10 +58,60 @@ import type { ValidateCheckpointResult } from './validation.js';
export abstract class ArchiverDataSourceBase
implements ArchiverDataSource, L2LogsSource, ContractDataSource, L1ToL2MessageSource
{
/** The injected genesis block header. */
protected readonly initialHeader: BlockHeader;
/** Precomputed hash of the initial header, exposed via {@link getGenesisBlockHash}. */
protected readonly initialBlockHash: BlockHash;
/** Archive root after block 0 was appended — read from L1 (`Rollup.getGenesisArchiveTreeRoot`). */
protected readonly genesisArchiveRoot: Fr;

constructor(
protected readonly stores: ArchiverDataStores,
protected readonly l1Constants?: L1RollupConstants,
) {}
protected readonly l1Constants: L1RollupConstants | undefined,
initialHeader: BlockHeader,
initialBlockHash: BlockHash,
genesisArchiveRoot: Fr,
) {
this.initialHeader = initialHeader;
this.initialBlockHash = initialBlockHash;
this.genesisArchiveRoot = genesisArchiveRoot;
}

/** Returns the precomputed hash of the genesis block header. */
public getGenesisBlockHash(): BlockHash {
return this.initialBlockHash;
}

/** Returns the genesis L2Block. */
private getGenesisBlock(): L2Block {
return new L2Block(
new AppendOnlyTreeSnapshot(this.genesisArchiveRoot, 1),
this.initialHeader,
Body.empty(),
CheckpointNumber.ZERO,
IndexWithinCheckpoint(0),
);
}

/** Returns genesis block data. */
private getGenesisBlockData(): BlockData {
return {
header: this.initialHeader,
archive: new AppendOnlyTreeSnapshot(this.genesisArchiveRoot, 1),
blockHash: this.initialBlockHash,
checkpointNumber: CheckpointNumber.ZERO,
indexWithinCheckpoint: IndexWithinCheckpoint(0),
};
}

/**
* Type guard distinguishing the genesis sentinel from a {@link ResolvedBlockQuery}.
* `resolveBlockQuery` already rewrites every genesis-matching shape to the sentinel,
* so callers only need this single sync check.
*/
private isGenesisBlockQuery(query: ResolvedBlockQuery | GenesisBlockQuery): query is GenesisBlockQuery {
return 'genesis' in query;
}

abstract getRollupAddress(): Promise<EthAddress>;

Expand Down Expand Up @@ -85,9 +152,12 @@ export abstract class ArchiverDataSourceBase
return this.stores.blocks.getLatestL2BlockNumber();
}
const resolved = await this.resolveBlockQuery(query);
if (!resolved) {
if (resolved === undefined) {
return undefined;
}
if (this.isGenesisBlockQuery(resolved)) {
return BlockNumber.ZERO;
}
return this.stores.blocks.getBlockNumber(resolved);
}

Expand Down Expand Up @@ -284,36 +354,62 @@ export abstract class ArchiverDataSourceBase

public async getBlock(query: BlockQuery): Promise<L2Block | undefined> {
const resolved = await this.resolveBlockQuery(query);
return resolved ? this.stores.blocks.getBlock(resolved) : undefined;
if (resolved === undefined) {
return undefined;
}
if (this.isGenesisBlockQuery(resolved)) {
return this.getGenesisBlock();
}
return this.stores.blocks.getBlock(resolved);
}

/**
* Range queries iterate physical blocks only; the genesis block is NOT prepended.
* `L2BlockStream` consumers (`world-state.handleL2Blocks`, etc.) emit `blocks-added` events for
* real blocks and would be surprised by a synthetic block 0. Use {@link getBlock} or
* {@link getBlockData} for genesis-aware single-block lookups.
*/
public async getBlocks(query: BlocksQuery): Promise<L2Block[]> {
const resolved = await this.resolveBlocksQuery(query);
return resolved ? this.stores.blocks.getBlocks(resolved) : [];
}

public async getBlockData(query: BlockQuery): Promise<BlockData | undefined> {
const resolved = await this.resolveBlockQuery(query);
return resolved ? this.stores.blocks.getBlockData(resolved) : undefined;
if (resolved === undefined) {
return undefined;
}
if (this.isGenesisBlockQuery(resolved)) {
return this.getGenesisBlockData();
}
return this.stores.blocks.getBlockData(resolved);
}

/** See {@link getBlocks} — range queries do not prepend the genesis block. */
public async getBlocksData(query: BlocksQuery): Promise<BlockData[]> {
const resolved = await this.resolveBlocksQuery(query);
return resolved ? this.stores.blocks.getBlocksData(resolved) : [];
}

/**
* Resolves a tag-based BlockQuery to a number-based query understood by BlockStore.
* Returns undefined when the tag points at block 0 (genesis / no blocks yet) so callers
* can short-circuit without entering BlockStore.
* Resolves a {@link BlockQuery} to either the genesis sentinel or a {@link ResolvedBlockQuery}
* understood by BlockStore. Detects every shape that points at block 0 — `{number:0}`,
* `{hash}` matching the initial header, `{archive}` matching the post-genesis archive root,
* and `{tag}` resolving to 0 — and rewrites them to the sentinel so callers branch once.
*/
private async resolveBlockQuery(query: BlockQuery): Promise<ResolvedBlockQuery | undefined> {
if (!('tag' in query)) {
return query;
private async resolveBlockQuery(query: BlockQuery): Promise<ResolvedBlockQuery | GenesisBlockQuery | undefined> {
if ('number' in query) {
return query.number === BlockNumber.ZERO ? { genesis: true } : query;
}
if ('hash' in query) {
return query.hash.equals(this.initialBlockHash) ? { genesis: true } : query;
}
if ('archive' in query) {
return query.archive.equals(this.genesisArchiveRoot) ? { genesis: true } : query;
}
const number = await this.resolveBlockTag(query.tag);
if (number === BlockNumber.ZERO) {
return undefined;
return { genesis: true };
}
return { number };
}
Expand All @@ -340,6 +436,12 @@ export abstract class ArchiverDataSourceBase
*/
private async resolveBlocksQuery(query: BlocksQuery): Promise<ResolvedBlocksQuery | undefined> {
if (!('epoch' in query)) {
if (query.from < INITIAL_L2_BLOCK_NUM) {
throw new Error(
`getBlocks/getBlocksData: 'from' must be >= ${INITIAL_L2_BLOCK_NUM}, got ${query.from}. ` +
`Use getBlock({number:0})/getBlockData({number:0}) for genesis-aware single-block lookups.`,
);
}
return query;
}
const checkpointNumbers = await this.getCheckpointNumbersForEpoch(query.epoch);
Expand Down
5 changes: 3 additions & 2 deletions yarn-project/archiver/src/store/block_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
BlockHash,
CommitteeAttestation,
EthAddress,
GENESIS_BLOCK_HEADER_HASH,
L2Block,
type ValidateCheckpointResult,
} from '@aztec/stdlib/block';
Expand Down Expand Up @@ -2565,7 +2566,7 @@ describe('BlockStore', () => {
);
await blockStore.addCheckpoints([checkpoint1]);

const l2TipsCache = new L2TipsCache(blockStore);
const l2TipsCache = new L2TipsCache(blockStore, GENESIS_BLOCK_HEADER_HASH);
const tips = await l2TipsCache.getL2Tips();

// proposedCheckpoint should always be defined
Expand Down Expand Up @@ -2601,7 +2602,7 @@ describe('BlockStore', () => {
feeAssetPriceModifier: 50n,
});

const l2TipsCache = new L2TipsCache(blockStore);
const l2TipsCache = new L2TipsCache(blockStore, GENESIS_BLOCK_HEADER_HASH);
const tips = await l2TipsCache.getL2Tips();

expect(tips.proposedCheckpoint).toBeDefined();
Expand Down
Loading
Loading