From 25a11bf6a6812f0ece44e90d65bae256406681e8 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 13 Feb 2026 15:10:53 -0300 Subject: [PATCH] feat(archiver): return L2 block data to avoid fetching full block The block proposal handler requires access to a block header, along with its checkpoint number. However, the checkpoint number is NOT part of the block header, and it's a bit painful to add (since the block header goes into circuits). Instead, the checkpoint number for a given block is only returned as part of the full L2Block, including txs. This PR adds an intermediate struct `BlockData` (similar to `CheckpointData` from #20467) that contains the block header plus checkpoint number, archive root, index within checkpoint, etc. --- .../archiver/src/modules/data_source_base.ts | 17 +++++- .../archiver/src/store/block_store.ts | 54 ++++++++++++++---- .../archiver/src/store/kv_archiver_store.ts | 24 +++++++- .../archiver/src/test/mock_l2_block_source.ts | 29 ++++++++++ .../aztec-node/src/aztec-node/server.ts | 17 +++++- .../aztec-node/src/sentinel/sentinel.test.ts | 2 +- .../aztec-node/src/sentinel/sentinel.ts | 8 +-- .../src/sequencer/sequencer.test.ts | 20 ++++++- .../src/sequencer/sequencer.ts | 22 ++++---- yarn-project/stdlib/src/block/block_data.ts | 26 +++++++++ yarn-project/stdlib/src/block/index.ts | 1 + .../stdlib/src/block/l2_block_source.ts | 15 +++++ .../stdlib/src/interfaces/archiver.test.ts | 18 +++++- .../stdlib/src/interfaces/archiver.ts | 3 + .../stdlib/src/interfaces/aztec-node.test.ts | 8 ++- .../src/block_proposal_handler.ts | 49 +++++++---------- .../validator-client/src/validator.test.ts | 55 ++++++++----------- 17 files changed, 270 insertions(+), 98 deletions(-) create mode 100644 yarn-project/stdlib/src/block/block_data.ts diff --git a/yarn-project/archiver/src/modules/data_source_base.ts b/yarn-project/archiver/src/modules/data_source_base.ts index 7be5a1b801d8..2cd512830e74 100644 --- a/yarn-project/archiver/src/modules/data_source_base.ts +++ b/yarn-project/archiver/src/modules/data_source_base.ts @@ -4,7 +4,14 @@ import type { EthAddress } from '@aztec/foundation/eth-address'; import { isDefined } from '@aztec/foundation/types'; import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { type BlockHash, CheckpointedL2Block, CommitteeAttestation, L2Block, type L2Tips } from '@aztec/stdlib/block'; +import { + type BlockData, + type BlockHash, + CheckpointedL2Block, + CommitteeAttestation, + L2Block, + type L2Tips, +} from '@aztec/stdlib/block'; import { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { ContractClassPublic, ContractDataSource, ContractInstanceWithAddress } from '@aztec/stdlib/contract'; import { type L1RollupConstants, getSlotRangeForEpoch } from '@aztec/stdlib/epoch-helpers'; @@ -129,6 +136,14 @@ export abstract class ArchiverDataSourceBase return this.store.getBlockHeaderByArchive(archive); } + public getBlockData(number: BlockNumber): Promise { + return this.store.getBlockData(number); + } + + public getBlockDataByArchive(archive: Fr): Promise { + return this.store.getBlockDataByArchive(archive); + } + public async getL2Block(number: BlockNumber): Promise { // If the number provided is -ve, then return the latest block. if (number < 0) { diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index 732fa7e13c3b..636df8cc299a 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -9,6 +9,7 @@ import { isDefined } from '@aztec/foundation/types'; import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton, Range } from '@aztec/kv-store'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + type BlockData, BlockHash, Body, CheckpointedL2Block, @@ -655,6 +656,32 @@ export class BlockStore { } } + /** + * Gets block metadata (without tx data) by block number. + * @param blockNumber - The number of the block to return. + * @returns The requested block data. + */ + async getBlockData(blockNumber: BlockNumber): Promise { + const blockStorage = await this.#blocks.getAsync(blockNumber); + if (!blockStorage || !blockStorage.header) { + return undefined; + } + return this.getBlockDataFromBlockStorage(blockStorage); + } + + /** + * Gets block metadata (without tx data) by archive root. + * @param archive - The archive root of the block to return. + * @returns The requested block data. + */ + async getBlockDataByArchive(archive: Fr): Promise { + const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString()); + if (blockNumber === undefined) { + return undefined; + } + return this.getBlockData(BlockNumber(blockNumber)); + } + /** * Gets an L2 block. * @param blockNumber - The number of the block to return. @@ -759,15 +786,24 @@ export class BlockStore { } } + private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData { + return { + header: BlockHeader.fromBuffer(blockStorage.header), + archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive), + blockHash: Fr.fromBuffer(blockStorage.blockHash), + checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber), + indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint), + }; + } + private async getBlockFromBlockStorage( blockNumber: number, blockStorage: BlockStorage, ): Promise { - const header = BlockHeader.fromBuffer(blockStorage.header); - const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive); - const blockHash = blockStorage.blockHash; - header.setHash(Fr.fromBuffer(blockHash)); - const blockHashString = bufferToHex(blockHash); + const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } = + this.getBlockDataFromBlockStorage(blockStorage); + header.setHash(blockHash); + const blockHashString = bufferToHex(blockStorage.blockHash); const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString); if (blockTxsBuffer === undefined) { this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`); @@ -786,13 +822,7 @@ export class BlockStore { txEffects.push(deserializeIndexedTxEffect(txEffect).data); } const body = new Body(txEffects); - const block = new L2Block( - archive, - header, - body, - CheckpointNumber(blockStorage.checkpointNumber!), - IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint), - ); + const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint); if (block.number !== blockNumber) { throw new Error( diff --git a/yarn-project/archiver/src/store/kv_archiver_store.ts b/yarn-project/archiver/src/store/kv_archiver_store.ts index 2be54f985f2d..ed31527c65ed 100644 --- a/yarn-project/archiver/src/store/kv_archiver_store.ts +++ b/yarn-project/archiver/src/store/kv_archiver_store.ts @@ -6,7 +6,13 @@ import { createLogger } from '@aztec/foundation/log'; import type { AztecAsyncKVStore, CustomRange, StoreSize } from '@aztec/kv-store'; import { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { BlockHash, CheckpointedL2Block, L2Block, type ValidateCheckpointResult } from '@aztec/stdlib/block'; +import { + type BlockData, + BlockHash, + CheckpointedL2Block, + L2Block, + type ValidateCheckpointResult, +} from '@aztec/stdlib/block'; import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { ContractClassPublic, @@ -369,6 +375,22 @@ export class KVArchiverDataStore implements ContractDataSource { return this.#blockStore.getBlockHeaderByArchive(archive); } + /** + * Gets block metadata (without tx data) by block number. + * @param blockNumber - The block number to return. + */ + getBlockData(blockNumber: BlockNumber): Promise { + return this.#blockStore.getBlockData(blockNumber); + } + + /** + * Gets block metadata (without tx data) by archive root. + * @param archive - The archive root to return. + */ + getBlockDataByArchive(archive: Fr): Promise { + return this.#blockStore.getBlockDataByArchive(archive); + } + /** * Gets a tx effect. * @param txHash - The hash of the tx corresponding to the tx effect. diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index 3d06e42e7391..d0a44894ec4e 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -8,6 +8,7 @@ import { createLogger } from '@aztec/foundation/log'; import type { FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + type BlockData, BlockHash, CheckpointedL2Block, L2Block, @@ -255,6 +256,34 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(block?.header); } + public async getBlockData(number: BlockNumber): Promise { + const block = this.l2Blocks[number - 1]; + if (!block) { + return undefined; + } + return { + header: block.header, + archive: block.archive, + blockHash: await block.hash(), + checkpointNumber: block.checkpointNumber, + indexWithinCheckpoint: block.indexWithinCheckpoint, + }; + } + + public async getBlockDataByArchive(archive: Fr): Promise { + const block = this.l2Blocks.find(b => b.archive.root.equals(archive)); + if (!block) { + return undefined; + } + return { + header: block.header, + archive: block.archive, + blockHash: await block.hash(), + checkpointNumber: block.checkpointNumber, + indexWithinCheckpoint: block.indexWithinCheckpoint, + }; + } + getBlockHeader(number: number | 'latest'): Promise { return Promise.resolve(this.l2Blocks.at(typeof number === 'number' ? number - 1 : -1)?.header); } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 0765a48e40ab..30e0b38bbaf6 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -33,7 +33,14 @@ import { } from '@aztec/slasher'; import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { BlockHash, type BlockParameter, type DataInBlock, L2Block, type L2BlockSource } from '@aztec/stdlib/block'; +import { + type BlockData, + BlockHash, + type BlockParameter, + type DataInBlock, + L2Block, + type L2BlockSource, +} from '@aztec/stdlib/block'; import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint'; import type { ContractClassPublic, @@ -1106,6 +1113,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { return await this.blockSource.getBlockHeaderByArchive(archive); } + public getBlockData(number: BlockNumber): Promise { + return this.blockSource.getBlockData(number); + } + + public getBlockDataByArchive(archive: Fr): Promise { + return this.blockSource.getBlockDataByArchive(archive); + } + /** * Simulates the public part of a transaction with the current state. * @param tx - The transaction to simulate. diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts index 23bd50f032cd..add05d7e7525 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.test.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.test.ts @@ -589,7 +589,7 @@ describe('sentinel', () => { ts, nowMs: ts * 1000n, }); - archiver.getL2Block.calledWith(blockNumber).mockResolvedValue(mockBlock); + archiver.getBlockHeader.calledWith(blockNumber).mockResolvedValue(mockBlock.header); archiver.getL1Constants.mockResolvedValue(l1Constants); epochCache.getL1Constants.mockReturnValue(l1Constants); diff --git a/yarn-project/aztec-node/src/sentinel/sentinel.ts b/yarn-project/aztec-node/src/sentinel/sentinel.ts index 44e4dc80d022..4be3a6972772 100644 --- a/yarn-project/aztec-node/src/sentinel/sentinel.ts +++ b/yarn-project/aztec-node/src/sentinel/sentinel.ts @@ -139,15 +139,15 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme return; } const blockNumber = event.block.number; - const block = await this.archiver.getL2Block(blockNumber); - if (!block) { - this.logger.error(`Failed to get block ${blockNumber}`, { block }); + const header = await this.archiver.getBlockHeader(blockNumber); + if (!header) { + this.logger.error(`Failed to get block header ${blockNumber}`); return; } // TODO(palla/slash): We should only be computing proven performance if this is // a full proof epoch and not a partial one, otherwise we'll end up with skewed stats. - const epoch = getEpochAtSlot(block.header.getSlot(), this.epochCache.getL1Constants()); + const epoch = getEpochAtSlot(header.getSlot(), this.epochCache.getL1Constants()); this.logger.debug(`Computing proven performance for epoch ${epoch}`); const performance = await this.computeProvenPerformance(epoch); this.logger.info(`Computed proven performance for epoch ${epoch}`, performance); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 0aa7c921015a..9329b90fba22 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -1,7 +1,13 @@ import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; import type { EpochCache, EpochCommitteeInfo } from '@aztec/epoch-cache'; import type { RollupContract } from '@aztec/ethereum/contracts'; -import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; +import { + BlockNumber, + CheckpointNumber, + EpochNumber, + IndexWithinCheckpoint, + SlotNumber, +} from '@aztec/foundation/branded-types'; import { omit, times, timesParallel } from '@aztec/foundation/collection'; import { Secp256k1Signer } from '@aztec/foundation/crypto/secp256k1-signer'; import { Fr } from '@aztec/foundation/curves/bn254'; @@ -12,6 +18,7 @@ import type { P2P } from '@aztec/p2p'; import type { SlasherClientInterface } from '@aztec/slasher'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { + type BlockData, CommitteeAttestation, CommitteeAttestationsAndSigners, GENESIS_CHECKPOINT_HEADER_HASH, @@ -31,7 +38,8 @@ import { type WorldStateSynchronizerStatus, } from '@aztec/stdlib/interfaces/server'; import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging'; -import { GlobalVariables, type Tx } from '@aztec/stdlib/tx'; +import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees'; +import { BlockHeader, GlobalVariables, type Tx } from '@aztec/stdlib/tx'; import type { FullNodeCheckpointsBuilder, ValidatorClient } from '@aztec/validator-client'; import { expect } from '@jest/globals'; @@ -235,7 +243,13 @@ describe('sequencer', () => { checkpointBuilder.setBlockProvider(() => block); l2BlockSource = mock({ - getL2Block: mockFn().mockResolvedValue(L2Block.empty()), + getBlockData: mockFn().mockResolvedValue({ + header: BlockHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + blockHash: Fr.ZERO, + checkpointNumber: CheckpointNumber(0), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + } satisfies BlockData), getBlockNumber: mockFn().mockResolvedValue(lastBlockNumber), getL2Tips: mockFn().mockResolvedValue({ proposed: { number: lastBlockNumber, hash }, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index de3dd62cd897..b34bb0c273f9 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -12,7 +12,7 @@ import type { DateProvider } from '@aztec/foundation/timer'; import type { TypedEventEmitter } from '@aztec/foundation/types'; import type { P2P } from '@aztec/p2p'; import type { SlasherClientInterface } from '@aztec/slasher'; -import type { L2Block, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block'; +import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block'; import type { Checkpoint } from '@aztec/stdlib/checkpoint'; import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers'; import { @@ -301,10 +301,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter= slot) { + if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= slot) { this.log.warn( `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, - { ...logCtx, block: syncedTo.block.header.toInspect() }, + { ...logCtx, block: syncedTo.blockData.header.toInspect() }, ); this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken'); return undefined; @@ -523,18 +523,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter; + /** + * Gets block metadata (without tx data) by block number. + * @param number - The block number to retrieve. + * @returns The requested block data (or undefined if not found). + */ + getBlockData(number: BlockNumber): Promise; + + /** + * Gets block metadata (without tx data) by archive root. + * @param archive - The archive root to retrieve. + * @returns The requested block data (or undefined if not found). + */ + getBlockDataByArchive(archive: Fr): Promise; + /** * Gets an L2 block by block number. * @param number - The block number to return. diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 2d605b35b0b4..6235da1d6c81 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -10,7 +10,7 @@ import type { ContractArtifact } from '../abi/abi.js'; import { FunctionSelector } from '../abi/function_selector.js'; import { AztecAddress } from '../aztec-address/index.js'; import { CheckpointedL2Block } from '../block/checkpointed_l2_block.js'; -import { BlockHash, CommitteeAttestation, L2Block } from '../block/index.js'; +import { type BlockData, BlockHash, CommitteeAttestation, L2Block } from '../block/index.js'; import type { L2Tips } from '../block/l2_block_source.js'; import type { ValidateCheckpointResult } from '../block/validate_block_result.js'; import { Checkpoint } from '../checkpoint/checkpoint.js'; @@ -110,6 +110,16 @@ describe('ArchiverApiSchema', () => { expect(result).toBeInstanceOf(BlockHeader); }); + it('getBlockData', async () => { + const result = await context.client.getBlockData(BlockNumber(1)); + expect(result).toBeUndefined(); + }); + + it('getBlockDataByArchive', async () => { + const result = await context.client.getBlockDataByArchive(Fr.random()); + expect(result).toBeUndefined(); + }); + it('getBlockHeaderByHash', async () => { const result = await context.client.getBlockHeaderByHash(BlockHash.random()); expect(result).toBeInstanceOf(BlockHeader); @@ -453,6 +463,12 @@ class MockArchiver implements ArchiverApi { getBlockHeaderByArchive(_archive: Fr): Promise { return Promise.resolve(BlockHeader.empty()); } + getBlockData(_number: BlockNumber): Promise { + return Promise.resolve(undefined); + } + getBlockDataByArchive(_archive: Fr): Promise { + return Promise.resolve(undefined); + } getL2Block(number: BlockNumber): Promise { return L2Block.random(number); } diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 51d62fd5cb6b..4caf08b4a30a 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -4,6 +4,7 @@ import type { ApiSchemaFor } from '@aztec/foundation/schemas'; import { z } from 'zod'; +import { BlockDataSchema } from '../block/block_data.js'; import { BlockHash } from '../block/block_hash.js'; import { CheckpointedL2Block } from '../block/checkpointed_l2_block.js'; import { L2Block } from '../block/l2_block.js'; @@ -104,6 +105,8 @@ export const ArchiverApiSchema: ApiSchemaFor = { getCheckpointedBlockByArchive: z.function().args(schemas.Fr).returns(CheckpointedL2Block.schema.optional()), getBlockHeaderByHash: z.function().args(BlockHash.schema).returns(BlockHeader.schema.optional()), getBlockHeaderByArchive: z.function().args(schemas.Fr).returns(BlockHeader.schema.optional()), + getBlockData: z.function().args(BlockNumberSchema).returns(BlockDataSchema.optional()), + getBlockDataByArchive: z.function().args(schemas.Fr).returns(BlockDataSchema.optional()), getL2Block: z.function().args(BlockNumberSchema).returns(L2Block.schema.optional()), getL2BlockByHash: z.function().args(BlockHash.schema).returns(L2Block.schema.optional()), getL2BlockByArchive: z.function().args(schemas.Fr).returns(L2Block.schema.optional()), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index 2243efd963dc..436945dd773e 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -16,7 +16,7 @@ import times from 'lodash.times'; import type { ContractArtifact } from '../abi/abi.js'; import { AztecAddress } from '../aztec-address/index.js'; import type { DataInBlock } from '../block/in_block.js'; -import { BlockHash, type BlockParameter, CommitteeAttestation, L2Block } from '../block/index.js'; +import { type BlockData, BlockHash, type BlockParameter, CommitteeAttestation, L2Block } from '../block/index.js'; import type { L2Tips } from '../block/l2_block_source.js'; import { Checkpoint } from '../checkpoint/checkpoint.js'; import { L1PublishedData, PublishedCheckpoint } from '../checkpoint/published_checkpoint.js'; @@ -637,6 +637,12 @@ class MockAztecNode implements AztecNode { getBlockHeaderByArchive(_archive: Fr): Promise { return Promise.resolve(BlockHeader.empty()); } + getBlockData(_number: BlockNumber): Promise { + return Promise.resolve(undefined); + } + getBlockDataByArchive(_archive: Fr): Promise { + return Promise.resolve(undefined); + } getCurrentMinFees(): Promise { return Promise.resolve(GasFees.empty()); } diff --git a/yarn-project/validator-client/src/block_proposal_handler.ts b/yarn-project/validator-client/src/block_proposal_handler.ts index fba4fcb25f3f..e1811bd36957 100644 --- a/yarn-project/validator-client/src/block_proposal_handler.ts +++ b/yarn-project/validator-client/src/block_proposal_handler.ts @@ -9,7 +9,7 @@ import { retryUntil } from '@aztec/foundation/retry'; import { DateProvider, Timer } from '@aztec/foundation/timer'; import type { P2P, PeerId } from '@aztec/p2p'; import { BlockProposalValidator } from '@aztec/p2p/msg_validators'; -import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block'; +import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block'; import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; import { @@ -18,7 +18,7 @@ import { computeInHashFromL1ToL2Messages, } from '@aztec/stdlib/messaging'; import type { BlockProposal } from '@aztec/stdlib/p2p'; -import { BlockHeader, type CheckpointGlobalVariables, type FailedTx, type Tx } from '@aztec/stdlib/tx'; +import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx'; import { ReExFailedTxsError, ReExStateMismatchError, @@ -153,16 +153,16 @@ export class BlockProposalHandler { } // Check that the parent proposal is a block we know, otherwise reexecution would fail - const parentBlockHeader = await this.getParentBlock(proposal); - if (parentBlockHeader === undefined) { + const parentBlock = await this.getParentBlock(proposal); + if (parentBlock === undefined) { this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo); return { isValid: false, reason: 'parent_block_not_found' }; } // Check that the parent block's slot is not greater than the proposal's slot. - if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() > slotNumber) { + if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) { this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, { - parentBlockSlot: parentBlockHeader.getSlot().toString(), + parentBlockSlot: parentBlock.header.getSlot().toString(), proposalSlot: slotNumber.toString(), ...proposalInfo, }); @@ -171,9 +171,9 @@ export class BlockProposalHandler { // Compute the block number based on the parent block const blockNumber = - parentBlockHeader === 'genesis' + parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) - : BlockNumber(parentBlockHeader.getBlockNumber() + 1); + : BlockNumber(parentBlock.header.getBlockNumber() + 1); // Check that this block number does not exist already const existingBlock = await this.blockSource.getBlockHeader(blockNumber); @@ -190,7 +190,7 @@ export class BlockProposalHandler { }); // Compute the checkpoint number for this block and validate checkpoint consistency - const checkpointResult = await this.computeCheckpointNumber(proposal, parentBlockHeader, proposalInfo); + const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo); if (checkpointResult.reason) { return { isValid: false, blockNumber, reason: checkpointResult.reason }; } @@ -260,7 +260,7 @@ export class BlockProposalHandler { return { isValid: true, blockNumber, reexecutionResult }; } - private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockHeader | undefined> { + private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> { const parentArchive = proposal.blockHeader.lastArchive.root; const slot = proposal.slotNumber; const config = this.checkpointsBuilder.getConfig(); @@ -276,12 +276,11 @@ export class BlockProposalHandler { try { return ( - (await this.blockSource.getBlockHeaderByArchive(parentArchive)) ?? + (await this.blockSource.getBlockDataByArchive(parentArchive)) ?? (timeoutDurationMs <= 0 ? undefined : await retryUntil( - () => - this.blockSource.syncImmediate().then(() => this.blockSource.getBlockHeaderByArchive(parentArchive)), + () => this.blockSource.syncImmediate().then(() => this.blockSource.getBlockDataByArchive(parentArchive)), 'force archiver sync', timeoutDurationMs / 1000, 0.5, @@ -297,12 +296,12 @@ export class BlockProposalHandler { } } - private async computeCheckpointNumber( + private computeCheckpointNumber( proposal: BlockProposal, - parentBlockHeader: 'genesis' | BlockHeader, + parentBlock: 'genesis' | BlockData, proposalInfo: object, - ): Promise { - if (parentBlockHeader === 'genesis') { + ): CheckpointComputationResult { + if (parentBlock === 'genesis') { // First block is in checkpoint 1 if (proposal.indexWithinCheckpoint !== 0) { this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo); @@ -311,19 +310,9 @@ export class BlockProposalHandler { return { checkpointNumber: CheckpointNumber.INITIAL }; } - // Get the parent block to find its checkpoint number - // TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup, - // or at least the L2BlockSource should return a different struct that includes it. - const parentBlockNumber = parentBlockHeader.getBlockNumber(); - const parentBlock = await this.blockSource.getL2Block(parentBlockNumber); - if (!parentBlock) { - this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo); - return { reason: 'invalid_proposal' }; - } - if (proposal.indexWithinCheckpoint === 0) { // If this is the first block in a new checkpoint, increment the checkpoint number - if (!(proposal.blockHeader.getSlot() > parentBlockHeader.getSlot())) { + if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) { this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo); return { reason: 'invalid_proposal' }; } @@ -335,7 +324,7 @@ export class BlockProposalHandler { this.log.warn(`Non-sequential indexWithinCheckpoint`, proposalInfo); return { reason: 'invalid_proposal' }; } - if (proposal.blockHeader.getSlot() !== parentBlockHeader.getSlot()) { + if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) { this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo); return { reason: 'invalid_proposal' }; } @@ -356,7 +345,7 @@ export class BlockProposalHandler { */ private validateNonFirstBlockInCheckpoint( proposal: BlockProposal, - parentBlock: L2Block, + parentBlock: BlockData, proposalInfo: object, ): CheckpointComputationResult | undefined { const proposalGlobals = proposal.blockHeader.globalVariables; diff --git a/yarn-project/validator-client/src/validator.test.ts b/yarn-project/validator-client/src/validator.test.ts index d68edcf7207f..097959351792 100644 --- a/yarn-project/validator-client/src/validator.test.ts +++ b/yarn-project/validator-client/src/validator.test.ts @@ -23,7 +23,7 @@ import { } from '@aztec/p2p'; import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block'; +import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block'; import type { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers'; import { Gas } from '@aztec/stdlib/gas'; import type { SlasherConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server'; @@ -336,23 +336,19 @@ describe('ValidatorClient', () => { epochCache.filterInCommittee.mockResolvedValue([EthAddress.fromString(validatorAccounts[0].address)]); epochCache.isEscapeHatchOpenAtSlot.mockResolvedValue(false); - // Return parent block header when requested - blockSource.getBlockHeaderByArchive.mockResolvedValue({ - getBlockNumber: () => blockNumber - 1, - getSlot: () => SlotNumber(Number(blockHeader.globalVariables.slotNumber) - 1), - } as BlockHeader); - - // Return parent block when requested (needed for checkpoint number computation) - // The parent block has slot - 1, which is different from the proposal's slot + // Return parent block data when requested (includes checkpoint info, avoids loading full L2Block) const parentSlot = SlotNumber(Number(blockHeader.globalVariables.slotNumber) - 1); - blockSource.getL2Block.mockResolvedValue({ - checkpointNumber: CheckpointNumber(1), - indexWithinCheckpoint: IndexWithinCheckpoint(0), + blockSource.getBlockDataByArchive.mockResolvedValue({ header: { - globalVariables: blockHeader.globalVariables, + getBlockNumber: () => blockNumber - 1, getSlot: () => parentSlot, + globalVariables: blockHeader.globalVariables, }, - } as unknown as L2Block); + archive: new AppendOnlyTreeSnapshot(Fr.random(), blockNumber - 1), + blockHash: Fr.random(), + checkpointNumber: CheckpointNumber(1), + indexWithinCheckpoint: IndexWithinCheckpoint(0), + } as unknown as BlockData); blockSource.getGenesisValues.mockResolvedValue({ genesisArchiveRoot: new Fr(GENESIS_ARCHIVE_ROOT) }); blockSource.syncImmediate.mockImplementation(() => Promise.resolve()); @@ -484,11 +480,11 @@ describe('ValidatorClient', () => { it('should wait for previous block to sync', async () => { epochCache.filterInCommittee.mockResolvedValue([EthAddress.fromString(validatorAccounts[0].address)]); - blockSource.getBlockHeaderByArchive.mockResolvedValueOnce(undefined); - blockSource.getBlockHeaderByArchive.mockResolvedValueOnce(undefined); - blockSource.getBlockHeaderByArchive.mockResolvedValueOnce(undefined); + blockSource.getBlockDataByArchive.mockResolvedValueOnce(undefined); + blockSource.getBlockDataByArchive.mockResolvedValueOnce(undefined); + blockSource.getBlockDataByArchive.mockResolvedValueOnce(undefined); const isValid = await validatorClient.validateBlockProposal(proposal, sender); - expect(blockSource.getBlockHeaderByArchive).toHaveBeenCalledTimes(4); + expect(blockSource.getBlockDataByArchive).toHaveBeenCalledTimes(4); expect(isValid).toBe(true); }); @@ -693,23 +689,18 @@ describe('ValidatorClient', () => { nextSlot: SlotNumber(nonFirstBlockProposal.slotNumber + 1), }); - // Mock parent block header returned by getBlockHeaderByArchive - const parentBlockHeader = { - getBlockNumber: () => BlockNumber(parentBlockNumber), - getSlot: () => SlotNumber(parentSlotNumber), - globalVariables: parentGlobalVariables, - } as BlockHeader; - blockSource.getBlockHeaderByArchive.mockResolvedValue(parentBlockHeader); - - // Mock parent block returned by getL2Block - const parentBlock = { - checkpointNumber: parentCheckpointNumber, - indexWithinCheckpoint: IndexWithinCheckpoint(0), // Parent is first block in checkpoint + // Mock parent block data returned by getBlockDataByArchive + blockSource.getBlockDataByArchive.mockResolvedValue({ header: { + getBlockNumber: () => BlockNumber(parentBlockNumber), + getSlot: () => SlotNumber(parentSlotNumber), globalVariables: parentGlobalVariables, }, - } as unknown as L2Block; - blockSource.getL2Block.mockResolvedValue(parentBlock); + archive: new AppendOnlyTreeSnapshot(Fr.random(), parentBlockNumber), + blockHash: Fr.random(), + checkpointNumber: parentCheckpointNumber, + indexWithinCheckpoint: IndexWithinCheckpoint(0), // Parent is first block in checkpoint + } as unknown as BlockData); // Set time for the slot const genesisTime = 1n;