From a0f4da4589848ea8a3105e3c9892b1cc3eccf8f7 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 10 Jun 2026 23:05:01 -0300 Subject: [PATCH 1/3] refactor: remove proposedCheckpoint tip; add atomic getProposedCheckpoint Replace the source-side `proposedCheckpoint` tip on `L2Tips` with a dedicated atomic archiver read `getProposedCheckpoint()` returning `{ tip, data }` (or undefined when no proposal leads the checkpointed frontier). The tip and its payload were read separately (JS-side tips cache vs. a direct store read on `#proposedCheckpoints`), forcing snapshot-race reconciliation in the sequencer. The single atomic read makes tip and payload coherent by construction, so that reconciliation code is deleted. `L2Tips`, `ChainTips`, and `LocalL2Tips` collapse to one shape, and the PXE/TXE fabrication shims are removed. Public node behavior is preserved: `'proposed'` in getCheckpointNumber/ resolveCheckpointParameter still means the proposed frontier (incl. unconfirmed), only the backing read changes. `getProposedCheckpoint` is archiver-internal + sequencer-facing; it is not exposed over node RPC. Fixes A-978 --- .../archiver/src/archiver-misc.test.ts | 5 +- .../archiver/src/archiver-sync.test.ts | 14 +- yarn-project/archiver/src/archiver.ts | 20 ++- .../archiver/src/modules/data_source_base.ts | 5 + .../archiver/src/store/block_store.test.ts | 36 ++-- .../archiver/src/store/block_store.ts | 70 +++++--- .../src/test/mock_l1_to_l2_message_source.ts | 1 - .../archiver/src/test/mock_l2_block_source.ts | 28 +-- .../aztec-node/src/aztec-node/server.test.ts | 164 +++++++++++------- .../aztec-node/src/aztec-node/server.ts | 42 +++-- .../e2e_l1_publisher/e2e_l1_publisher.test.ts | 1 - .../block_synchronizer/block_stream_source.ts | 5 +- .../sequencer/automine/automine_sequencer.ts | 8 +- .../sequencer/checkpoint_proposal_job.test.ts | 8 - .../checkpoint_proposal_job.timing.test.ts | 4 - .../src/sequencer/sequencer.test.ts | 138 +++++++-------- .../src/sequencer/sequencer.ts | 39 +---- .../stdlib/src/block/l2_block_source.ts | 38 ++-- .../l2_block_stream/l2_block_stream.test.ts | 10 +- .../stdlib/src/interfaces/archiver.test.ts | 41 ++++- .../stdlib/src/interfaces/archiver.ts | 5 + .../stdlib/src/interfaces/chain_tips.ts | 16 +- .../stdlib/src/interfaces/prover-node.test.ts | 2 - yarn-project/stdlib/src/tests/factories.ts | 4 - .../txe/src/state_machine/archiver.ts | 1 - 25 files changed, 373 insertions(+), 332 deletions(-) diff --git a/yarn-project/archiver/src/archiver-misc.test.ts b/yarn-project/archiver/src/archiver-misc.test.ts index 6fce5a928814..14612a3bff0f 100644 --- a/yarn-project/archiver/src/archiver-misc.test.ts +++ b/yarn-project/archiver/src/archiver-misc.test.ts @@ -210,16 +210,13 @@ describe('Archiver misc', () => { describe('isPruneDueAtSlot', () => { /** * Builds a fake L2Tips. `pending` is the L1-confirmed pending checkpoint (= `tips.checkpointed` - * in production). `proposedCheckpoint` is set to `pending + 1` to catch any implementation that - * accidentally reads the local-optimistic proposed checkpoint instead of the L1-confirmed one. + * in production). */ function makeTips(pending: CheckpointNumber, proven: CheckpointNumber): L2Tips { const block = { number: BlockNumber(0), hash: '0x' }; const tip = (n: CheckpointNumber) => ({ block, checkpoint: { number: n, hash: '0x' } }); - const proposedAhead = CheckpointNumber(Number(pending) + 1); return { proposed: block, - proposedCheckpoint: tip(proposedAhead), checkpointed: tip(pending), proven: tip(proven), finalized: tip(proven), diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index a5e1e2d6e886..55ec29951fe1 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -2075,11 +2075,13 @@ describe('Archiver Sync', () => { // Proposed checkpoint should still be set expect(await archiverStore.blocks.getLastProposedCheckpoint()).toBeDefined(); - // Proposed tip should be ahead of the checkpointed tip + // Proposed checkpoint should lead the checkpointed tip const tips = await archiver.getL2Tips(); - expect(tips.proposedCheckpoint.checkpoint.number).toEqual(CheckpointNumber(2)); + const proposedCheckpointResult = await archiver.getProposedCheckpoint(); + expect(proposedCheckpointResult).toBeDefined(); + expect(proposedCheckpointResult!.tip.checkpoint.number).toEqual(CheckpointNumber(2)); expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); - expect(tips.proposedCheckpoint.block.number).toBeGreaterThan(tips.checkpointed.block.number); + expect(proposedCheckpointResult!.tip.block.number).toBeGreaterThan(tips.checkpointed.block.number); }, 15_000); it('prunes blocks and clears stale pending checkpoint when slot ends', async () => { @@ -2142,11 +2144,9 @@ describe('Archiver Sync', () => { expect(await archiver.getBlockNumber()).toEqual(lastBlockInCheckpoint1); expect(await archiver.getCheckpointNumber()).toEqual(CheckpointNumber(1)); - // Proposed checkpoint should be cleared, so proposed tip falls back to checkpointed tip + // Proposed checkpoint should be cleared, so no proposed checkpoint leads the checkpointed tip expect(await archiverStore.blocks.getLastProposedCheckpoint()).toBeUndefined(); - const tips = await archiver.getL2Tips(); - expect(tips.proposedCheckpoint.checkpoint.number).toEqual(tips.checkpointed.checkpoint.number); - expect(tips.proposedCheckpoint.block.number).toEqual(tips.checkpointed.block.number); + expect(await archiver.getProposedCheckpoint()).toBeUndefined(); }, 15_000); }); diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index 8ea62a41dd2d..5619c2ce0224 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -5,7 +5,7 @@ import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses' import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types'; import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types'; import { Buffer32 } from '@aztec/foundation/buffer'; -import { merge, pick } from '@aztec/foundation/collection'; +import { merge } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/curves/bn254'; import { EthAddress } from '@aztec/foundation/eth-address'; import { type Logger, createLogger } from '@aztec/foundation/log'; @@ -435,19 +435,23 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra const tips = await this.getL2Tips(); const now = BigInt(this.dateProvider.nowInSeconds()); + // Frontier block covered by a proposed (or, falling back, confirmed) checkpoint. Blocks beyond it + // have no enclosing checkpoint proposal and are the orphan-pruning candidates. + const proposedCheckpointBlockNumber = await this.stores.blocks.getProposedCheckpointL2BlockNumber(); + // The proposed tip is a proposed-checkpointed block, so there are no orphan proposed blocks to prune - if (tips.proposedCheckpoint.block.number === tips.proposed.number) { - this.log.trace( - `No orphan proposed blocks to prune: proposed tip ${tips.proposed.number} is checkpointed`, - pick(tips, 'proposed', 'proposedCheckpoint'), - ); + if (proposedCheckpointBlockNumber === tips.proposed.number) { + this.log.trace(`No orphan proposed blocks to prune: proposed tip ${tips.proposed.number} is checkpointed`, { + proposed: tips.proposed, + proposedCheckpointBlockNumber, + }); return; } // Load the blocks that are candidates for pruning (ie blocks without a proposed checkpoint covering them) const blocksWithoutProposedCheckpoint = await this.stores.blocks.getBlocksData({ - from: BlockNumber(tips.proposedCheckpoint.block.number + 1), - limit: tips.proposed.number - tips.proposedCheckpoint.block.number, + from: BlockNumber(proposedCheckpointBlockNumber + 1), + limit: tips.proposed.number - proposedCheckpointBlockNumber, }); // Iterate through them in order, the first one with a slot that should have received a proposed checkpoint diff --git a/yarn-project/archiver/src/modules/data_source_base.ts b/yarn-project/archiver/src/modules/data_source_base.ts index eb2beb5dedb4..41f51f153a4e 100644 --- a/yarn-project/archiver/src/modules/data_source_base.ts +++ b/yarn-project/archiver/src/modules/data_source_base.ts @@ -21,6 +21,7 @@ import { type CheckpointsQuery, L2Block, type L2Tips, + type ProposedCheckpoint, type ProposedCheckpointQuery, } from '@aztec/stdlib/block'; import { @@ -288,6 +289,10 @@ export abstract class ArchiverDataSourceBase return this.stores.blocks.getProposedCheckpointBySlot(query.slot); } + public getProposedCheckpoint(): Promise { + return this.stores.blocks.getProposedCheckpoint(); + } + public getTxEffect(txHash: TxHash): Promise { return this.stores.blocks.getTxEffect(txHash); } diff --git a/yarn-project/archiver/src/store/block_store.test.ts b/yarn-project/archiver/src/store/block_store.test.ts index d7b3eb65d62e..efabed86a00f 100644 --- a/yarn-project/archiver/src/store/block_store.test.ts +++ b/yarn-project/archiver/src/store/block_store.test.ts @@ -13,7 +13,6 @@ import { BlockHash, CommitteeAttestation, EthAddress, - GENESIS_BLOCK_HEADER_HASH, L2Block, type ValidateCheckpointResult, } from '@aztec/stdlib/block'; @@ -40,7 +39,6 @@ import { makeStateForBlock, } from '../test/mock_structs.js'; import { BlockStore } from './block_store.js'; -import { L2TipsCache } from './l2_tips_cache.js'; async function addProposedBlocks( blockStore: BlockStore, @@ -2627,8 +2625,8 @@ describe('BlockStore', () => { }); }); - describe('L2TipsCache proposedCheckpoint', () => { - it('returns proposedCheckpoint equal to checkpointed when no pending exists', async () => { + describe('getProposedCheckpoint', () => { + it('returns undefined when no pending checkpoint leads the checkpointed frontier', async () => { // Add checkpoint 1 with blocks 1-3 const checkpoint1 = makePublishedCheckpoint( await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }), @@ -2636,17 +2634,10 @@ describe('BlockStore', () => { ); await blockStore.addCheckpoints([checkpoint1]); - const l2TipsCache = new L2TipsCache(blockStore, GENESIS_BLOCK_HEADER_HASH); - const tips = await l2TipsCache.getL2Tips(); - - // proposedCheckpoint should always be defined - expect(tips.proposedCheckpoint).toBeDefined(); - // With no proposed checkpoint, it should equal the checkpointed tip - expect(tips.proposedCheckpoint!.block.number).toBe(tips.checkpointed.block.number); - expect(tips.proposedCheckpoint!.checkpoint.number).toBe(tips.checkpointed.checkpoint.number); + expect(await blockStore.getProposedCheckpoint()).toBeUndefined(); }); - it('returns proposedCheckpoint ahead of checkpointed when pending is set', async () => { + it('returns the leading proposed checkpoint with a tip derived from its payload', async () => { // Add checkpoint 1 const checkpoint1 = makePublishedCheckpoint( await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 }), @@ -2663,21 +2654,26 @@ describe('BlockStore', () => { await blockStore.addProposedBlock(block2, { force: true }); // Set proposed checkpoint + const header = CheckpointHeader.empty(); await blockStore.addProposedCheckpoint({ checkpointNumber: CheckpointNumber(2), - header: CheckpointHeader.empty(), + header, startBlock: BlockNumber(2), blockCount: 1, totalManaUsed: 100n, feeAssetPriceModifier: 50n, }); - const l2TipsCache = new L2TipsCache(blockStore, GENESIS_BLOCK_HEADER_HASH); - const tips = await l2TipsCache.getL2Tips(); - - expect(tips.proposedCheckpoint).toBeDefined(); - expect(tips.proposedCheckpoint!.block.number).toBeGreaterThan(tips.checkpointed.block.number); - expect(tips.proposedCheckpoint!.checkpoint.number).toBeGreaterThan(tips.checkpointed.checkpoint.number); + const proposedCheckpoint = await blockStore.getProposedCheckpoint(); + expect(proposedCheckpoint).toBeDefined(); + // Tip is derived from the payload: last block = startBlock + blockCount - 1, checkpoint number + // and header hash from the stored proposed checkpoint. + expect(proposedCheckpoint!.tip.block.number).toBe(BlockNumber(2)); + expect(proposedCheckpoint!.tip.block.hash).toBe((await block2.hash()).toString()); + expect(proposedCheckpoint!.tip.checkpoint.number).toBe(CheckpointNumber(2)); + expect(proposedCheckpoint!.tip.checkpoint.hash).toBe(header.hash().toString()); + expect(proposedCheckpoint!.data.checkpointNumber).toBe(CheckpointNumber(2)); + expect(proposedCheckpoint!.data.totalManaUsed).toBe(100n); }); }); diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index 1901119016c4..e419a9755e1c 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -17,6 +17,7 @@ import { L2Block, type L2TipId, type L2Tips, + type ProposedCheckpoint, type ValidateCheckpointResult, deserializeValidateCheckpointResult, serializeValidateCheckpointResult, @@ -864,6 +865,38 @@ export class BlockStore { return undefined; } + /** + * Returns the latest proposed checkpoint that leads the checkpointed frontier, paired with its + * derived chain tip, in a single read-only transaction so the tip and payload are a coherent + * snapshot. Returns undefined when no proposed checkpoint exists beyond the latest confirmed + * checkpoint. The tip's block number is `startBlock + blockCount - 1` and its block hash is read + * from the block store; the checkpoint hash is derived from the stored header. + */ + async getProposedCheckpoint(): Promise { + return await this.db.transactionAsync(async () => { + const [entry] = await toArray(this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 })); + if (entry === undefined) { + return undefined; + } + const latestCheckpointNumber = await this.getLatestCheckpointNumber(); + if (entry[0] <= latestCheckpointNumber) { + return undefined; + } + const data = this.convertToProposedCheckpointData(entry[1]); + const blockNumber = BlockNumber(data.startBlock + data.blockCount - 1); + const blockStorage = await this.#blocks.getAsync(blockNumber); + if (!blockStorage) { + throw new BlockNotFoundError(blockNumber); + } + const blockHash = BlockHash.fromBuffer(blockStorage.blockHash).toString(); + const tip: L2TipId = { + block: { number: blockNumber, hash: blockHash }, + checkpoint: { number: data.checkpointNumber, hash: data.header.hash().toString() }, + }; + return { tip, data }; + }); + } + /** * Evicts all pending checkpoints with checkpoint number >= fromNumber. * Used for divergent-mined-checkpoint cleanup: when L1 mines checkpoint N with a different archive, @@ -1167,14 +1200,13 @@ export class BlockStore { } /** - * Resolves all five L2 chain tips (proposed, proposedCheckpoint, checkpointed, proven, finalized) - * in a single read-only transaction so the snapshot is internally consistent. Each underlying - * record is read at most once: latest block, latest confirmed checkpoint, and latest pending - * checkpoint are each loaded directly (no separate "find the number, then look up data" hop), - * the proven/finalized checkpoint singletons are read once and their storage entries are - * reused if they coincide with the latest checkpoint, and per-tip block hashes are deduped - * when two tips land on the same block (e.g. finalized == proven, or proposedCheckpoint falls - * back to checkpointed when no pending checkpoint exists). + * Resolves all four L2 chain tips (proposed, checkpointed, proven, finalized) in a single + * read-only transaction so the snapshot is internally consistent. Each underlying record is + * read at most once: latest block and latest confirmed checkpoint are loaded directly (no + * separate "find the number, then look up data" hop), the proven/finalized checkpoint + * singletons are read once and their storage entries are reused if they coincide with the + * latest checkpoint, and per-tip block hashes are deduped when two tips land on the same block + * (e.g. finalized == proven). * * The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by * block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied @@ -1197,9 +1229,6 @@ export class BlockStore { // Load latest block and checkpoint entries const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 })); - const [proposedCheckpointEntry] = await toArray( - this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }), - ); const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 })); const latestCheckpointNumber = latestCheckpointEntry ? CheckpointNumber(latestCheckpointEntry[0]) @@ -1285,14 +1314,6 @@ export class BlockStore { const provenTip = await buildTipFromCheckpoint(provenCheckpoint); const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint); - // Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is - // inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and - // report the checkpointed tip as the proposed checkpoint to maintain the invariant. - const proposedCheckpointTip = - proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber - ? checkpointedTip - : await buildTipFromCheckpoint(proposedCheckpointEntry[1]); - // A checkpointed block past the latest stored block would mean a checkpoint // references blocks that aren't in blocks. if (proposedBlockId.number < checkpointedTip.block.number) { @@ -1304,11 +1325,10 @@ export class BlockStore { // Assert that checkpoint numbers are increasing if ( finalizedTip.checkpoint.number > provenTip.checkpoint.number || - provenTip.checkpoint.number > checkpointedTip.checkpoint.number || - checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number + provenTip.checkpoint.number > checkpointedTip.checkpoint.number ) { throw new Error( - `Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number} proposed=${proposedCheckpointTip.checkpoint.number}`, + `Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}`, ); } @@ -1316,17 +1336,15 @@ export class BlockStore { if ( finalizedTip.block.number > provenTip.block.number || provenTip.block.number > checkpointedTip.block.number || - checkpointedTip.block.number > proposedCheckpointTip.block.number || - proposedCheckpointTip.block.number > proposedBlockId.number + checkpointedTip.block.number > proposedBlockId.number ) { throw new Error( - `Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposedCheckpoint=${proposedCheckpointTip.block.number} proposed=${proposedBlockId.number}`, + `Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposed=${proposedBlockId.number}`, ); } return { proposed: proposedBlockId, - proposedCheckpoint: proposedCheckpointTip, checkpointed: checkpointedTip, proven: provenTip, finalized: finalizedTip, diff --git a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts index d2edd93167d7..f3b72a1c940f 100644 --- a/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts +++ b/yarn-project/archiver/src/test/mock_l1_to_l2_message_source.ts @@ -44,7 +44,6 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource { checkpointed: tip, proven: tip, finalized: tip, - proposedCheckpoint: tip, }); } } 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 312c35e61c07..74e78c736493 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -27,6 +27,7 @@ import { L2Block, type L2BlockSource, type L2Tips, + type ProposedCheckpoint, type ProposedCheckpointQuery, type ValidateCheckpointResult, } from '@aztec/stdlib/block'; @@ -55,7 +56,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { private provenBlockNumber: number = 0; private finalizedBlockNumber: number = 0; private checkpointedBlockNumber: number = 0; - private proposedCheckpointBlockNumber: number = 0; private initialHeader: BlockHeader = BlockHeader.empty(); private initialHeaderHash: BlockHash = GENESIS_BLOCK_HEADER_HASH; @@ -164,7 +164,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { }); // Keep tip numbers consistent with remaining blocks. this.checkpointedBlockNumber = Math.min(this.checkpointedBlockNumber, maxBlockNum); - this.proposedCheckpointBlockNumber = Math.min(this.proposedCheckpointBlockNumber, maxBlockNum); this.provenBlockNumber = Math.min(this.provenBlockNumber, maxBlockNum); this.finalizedBlockNumber = Math.min(this.finalizedBlockNumber, maxBlockNum); this.log.verbose(`Removed ${numBlocks} blocks from the mock L2 block source`); @@ -181,17 +180,9 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { this.finalizedBlockNumber = finalizedBlockNumber; } - public setProposedCheckpointBlockNumber(blockNumber: number) { - this.proposedCheckpointBlockNumber = blockNumber; - } - public setCheckpointedBlockNumber(checkpointedBlockNumber: number) { const prevCheckpointed = this.checkpointedBlockNumber; this.checkpointedBlockNumber = checkpointedBlockNumber; - // Proposed checkpoint is always at least as advanced as checkpointed - if (this.proposedCheckpointBlockNumber < checkpointedBlockNumber) { - this.proposedCheckpointBlockNumber = checkpointedBlockNumber; - } // Auto-create single-block checkpoints for newly checkpointed blocks that don't have one yet. // This handles blocks added via addProposedBlocks that are now being marked as checkpointed. const newCheckpoints: Checkpoint[] = []; @@ -255,10 +246,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return block ? block.header.globalVariables.blockNumber : undefined; } - public getProposedCheckpointL2BlockNumber() { - return Promise.resolve(BlockNumber(this.proposedCheckpointBlockNumber)); - } - public getCheckpoint(query: CheckpointQuery): Promise { const checkpoint = this.resolveCheckpointQuery(query); if (!checkpoint) { @@ -373,19 +360,17 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { } async getL2Tips(): Promise { - const [latest, proven, finalized, checkpointed, proposedCheckpoint] = [ + const [latest, proven, finalized, checkpointed] = [ await this.getBlockNumber(), this.provenBlockNumber, this.finalizedBlockNumber, this.checkpointedBlockNumber, - await this.getProposedCheckpointL2BlockNumber(), ] as const; const latestBlock = this.l2Blocks[latest - 1]; const provenBlock = this.l2Blocks[proven - 1]; const finalizedBlock = this.l2Blocks[finalized - 1]; const checkpointedBlock = this.l2Blocks[checkpointed - 1]; - const proposedCheckpointBlock = this.l2Blocks[proposedCheckpoint - 1]; // For genesis tips (block number 0) report the dynamic initial header hash so consumers // running L2BlockStream against this mock agree at block 0 with their local tip store. @@ -413,10 +398,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { number: BlockNumber(checkpointed), hash: await tipHash(checkpointedBlock, checkpointed), }; - const proposedCheckpointBlockId = { - number: BlockNumber(proposedCheckpoint), - hash: await tipHash(proposedCheckpointBlock, proposedCheckpoint), - }; const makeTipId = (blockId: typeof latestBlockId) => { const checkpointNumber = this.findCheckpointNumberForBlock(blockId.number) ?? CheckpointNumber(0); @@ -435,7 +416,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { checkpointed: makeTipId(checkpointedBlockId), proven: makeTipId(provenBlockId), finalized: makeTipId(finalizedBlockId), - proposedCheckpoint: makeTipId(proposedCheckpointBlockId), }; } @@ -614,6 +594,10 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(undefined); } + getProposedCheckpoint(): Promise { + return Promise.resolve(undefined); + } + /** Returns checkpoints whose slot falls within the given epoch. */ private getCheckpointsInEpoch(epochNumber: EpochNumber): Checkpoint[] { const epochDuration = DefaultL1ContractsConfig.aztecEpochDuration; diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 8d84ab641270..0fac83ed1914 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -31,6 +31,7 @@ import { L2Block, type L2BlockSource, type L2Tips, + type ProposedCheckpoint, } from '@aztec/stdlib/block'; import type { CheckpointData, ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -744,16 +745,14 @@ describe('aztec node', () => { const checkpointNumber = CheckpointNumber(1); const proposedCheckpointBlockNumber = BlockNumber(9); const targetSlot = SlotNumber(10); - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ - proposed: proposedCheckpointBlockNumber, - proposedCheckpoint: checkpointNumber, - proposedCheckpointBlock: proposedCheckpointBlockNumber, + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: proposedCheckpointBlockNumber })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber, + blockNumber: proposedCheckpointBlockNumber, + slotNumber: SlotNumber(9), }), ); - l2BlockSource.getBlockData.mockResolvedValue( - makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), - ); mockNextL1Slot(SlotNumber(5)); globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ chainId, @@ -767,7 +766,7 @@ describe('aztec node', () => { await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); - expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + // Slot is read from the proposed checkpoint payload header, so no block fetch is needed for it. expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( EthAddress.ZERO, @@ -781,16 +780,14 @@ describe('aztec node', () => { const checkpointNumber = CheckpointNumber(1); const proposedCheckpointBlockNumber = BlockNumber(9); const targetSlot = SlotNumber(12); - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ - proposed: proposedCheckpointBlockNumber, - proposedCheckpoint: checkpointNumber, - proposedCheckpointBlock: proposedCheckpointBlockNumber, + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: proposedCheckpointBlockNumber })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber, + blockNumber: proposedCheckpointBlockNumber, + slotNumber: SlotNumber(9), }), ); - l2BlockSource.getBlockData.mockResolvedValue( - makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), - ); mockNextL1Slot(targetSlot); globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ chainId, @@ -804,7 +801,6 @@ describe('aztec node', () => { await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); - expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( EthAddress.ZERO, @@ -819,24 +815,17 @@ describe('aztec node', () => { const proposedCheckpointBlockNumber = BlockNumber(9); const latestProposedBlockNumber = BlockNumber(12); const targetSlot = SlotNumber(12); - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ - proposed: latestProposedBlockNumber, - proposedCheckpoint: checkpointNumber, - proposedCheckpointBlock: proposedCheckpointBlockNumber, + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: latestProposedBlockNumber })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber, + blockNumber: proposedCheckpointBlockNumber, + slotNumber: SlotNumber(9), }), ); - l2BlockSource.getBlockData.mockImplementation(query => { - if (!('number' in query)) { - return Promise.resolve(undefined); - } - if (query.number === proposedCheckpointBlockNumber) { - return Promise.resolve( - makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), - ); - } - return Promise.resolve(makeSimulationBlockData(latestProposedBlockNumber, targetSlot, checkpointNumber)); - }); + l2BlockSource.getBlockData.mockResolvedValue( + makeSimulationBlockData(latestProposedBlockNumber, targetSlot, checkpointNumber), + ); mockNextL1Slot(SlotNumber(5)); globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ chainId, @@ -850,7 +839,7 @@ describe('aztec node', () => { await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); - expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + // The latest proposed block is ahead of the proposed checkpoint, so its slot is fetched. expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: latestProposedBlockNumber }); expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( @@ -866,11 +855,12 @@ describe('aztec node', () => { const proposedCheckpointBlockNumber = BlockNumber(9); const latestProposedBlockNumber = BlockNumber(12); const targetSlot = SlotNumber(13); - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ - proposed: latestProposedBlockNumber, - proposedCheckpoint: checkpointNumber, - proposedCheckpointBlock: proposedCheckpointBlockNumber, + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: latestProposedBlockNumber })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber, + blockNumber: proposedCheckpointBlockNumber, + slotNumber: SlotNumber(9), }), ); l2BlockSource.getBlockData.mockResolvedValue(undefined); @@ -887,7 +877,7 @@ describe('aztec node', () => { await expect(node.simulatePublicCalls(tx)).rejects.toThrow(); - expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: proposedCheckpointBlockNumber }); + // Latest proposed block slot is unavailable; falls back to the next L1 timestamp slot. expect(l2BlockSource.getBlockData).toHaveBeenCalledWith({ number: latestProposedBlockNumber }); expect(globalVariablesBuilder.buildGlobalVariables).not.toHaveBeenCalled(); expect(globalVariablesBuilder.buildCheckpointGlobalVariables).toHaveBeenCalledWith( @@ -902,13 +892,12 @@ describe('aztec node', () => { const checkpointNumber = CheckpointNumber(0); const proposedCheckpointBlockNumber = BlockNumber(0); const targetSlot = SlotNumber(1); + // No proposed checkpoint leads the frontier; the proposed-checkpoint frontier falls back to the + // checkpointed tip (block 0, slot 0), whose slot is read via getBlockData. l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ - proposed: proposedCheckpointBlockNumber, - proposedCheckpoint: checkpointNumber, - proposedCheckpointBlock: proposedCheckpointBlockNumber, - }), + makeTips({ proposed: proposedCheckpointBlockNumber, checkpointed: checkpointNumber }), ); + l2BlockSource.getProposedCheckpoint.mockResolvedValue(undefined); l2BlockSource.getBlockData.mockResolvedValue( makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(0), checkpointNumber), ); @@ -1405,9 +1394,8 @@ describe('aztec node', () => { /** Builds an L2Tips stub with the given checkpoint numbers per tip. */ function makeTips(args: { proposed?: BlockNumber; - proposedCheckpointBlock?: BlockNumber; - proposedCheckpoint?: CheckpointNumber; checkpointed?: CheckpointNumber; + checkpointedBlock?: BlockNumber; proven?: CheckpointNumber; finalized?: CheckpointNumber; }): L2Tips { @@ -1418,13 +1406,36 @@ describe('aztec node', () => { }); return { proposed: makeBlockId(args.proposed), - checkpointed: makeTipId(args.checkpointed ?? CheckpointNumber(0)), - proposedCheckpoint: makeTipId(args.proposedCheckpoint ?? CheckpointNumber(0), args.proposedCheckpointBlock), + checkpointed: makeTipId(args.checkpointed ?? CheckpointNumber(0), args.checkpointedBlock), proven: makeTipId(args.proven ?? CheckpointNumber(0)), finalized: makeTipId(args.finalized ?? CheckpointNumber(0)), }; } + /** Builds a ProposedCheckpoint stub (the atomic leading-proposed-checkpoint snapshot). */ + function makeProposedCheckpoint(args: { + checkpointNumber: CheckpointNumber; + blockNumber: BlockNumber; + slotNumber: SlotNumber; + }): ProposedCheckpoint { + return { + tip: { + block: { number: args.blockNumber, hash: '' }, + checkpoint: { number: args.checkpointNumber, hash: '' }, + }, + data: { + checkpointNumber: args.checkpointNumber, + header: CheckpointHeader.random({ slotNumber: args.slotNumber }), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: args.blockNumber, + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, + }, + }; + } + describe('getCheckpoint', () => { /** Builds a minimal ProposedCheckpointData stub. */ function makeProposedCheckpointData( @@ -1460,7 +1471,14 @@ describe('aztec node', () => { describe('throw guards', () => { it('throws BadRequestError when "proposed" resolves to a proposed entry and includeL1PublishInfo is requested', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposedCheckpoint: CheckpointNumber(5) })); + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(4) })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber: CheckpointNumber(5), + blockNumber: BlockNumber(5), + slotNumber: SlotNumber(10), + }), + ); l2BlockSource.getCheckpointData.mockResolvedValue(undefined); l2BlockSource.getProposedCheckpointData.mockResolvedValue( makeProposedCheckpointData(CheckpointNumber(5), SlotNumber(10)), @@ -1470,7 +1488,14 @@ describe('aztec node', () => { }); it('throws BadRequestError when "proposed" resolves to a proposed entry and includeAttestations is requested', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposedCheckpoint: CheckpointNumber(5) })); + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(4) })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber: CheckpointNumber(5), + blockNumber: BlockNumber(5), + slotNumber: SlotNumber(10), + }), + ); l2BlockSource.getCheckpointData.mockResolvedValue(undefined); l2BlockSource.getProposedCheckpointData.mockResolvedValue( makeProposedCheckpointData(CheckpointNumber(5), SlotNumber(10)), @@ -1504,7 +1529,14 @@ describe('aztec node', () => { describe('fallback semantics', () => { it('getCheckpoint("proposed") returns the projected proposed entry when one exists at the proposed-tip number', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposedCheckpoint: CheckpointNumber(2) })); + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(1) })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber: CheckpointNumber(2), + blockNumber: BlockNumber(2), + slotNumber: SlotNumber(5), + }), + ); l2BlockSource.getCheckpointData.mockResolvedValue(undefined); const proposed = makeProposedCheckpointData(CheckpointNumber(2), SlotNumber(5)); l2BlockSource.getProposedCheckpointData.mockResolvedValue(proposed); @@ -1515,10 +1547,9 @@ describe('aztec node', () => { }); it('getCheckpoint("proposed") returns the latest confirmed checkpoint when no proposed entry exists', async () => { - // When no proposed entry exists, the proposedCheckpoint tip falls back to the confirmed tip. - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ proposedCheckpoint: CheckpointNumber(3), checkpointed: CheckpointNumber(3) }), - ); + // When no proposed checkpoint leads the frontier, the proposed tip falls back to the confirmed tip. + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(3) })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue(undefined); const confirmed = makeCheckpointData(CheckpointNumber(3)); l2BlockSource.getCheckpointData.mockResolvedValue(confirmed); @@ -1636,19 +1667,23 @@ describe('aztec node', () => { }); describe('getCheckpointNumber', () => { - it('returns the proposed checkpoint number from proposedCheckpoint tip', async () => { - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ proposedCheckpoint: CheckpointNumber(7), checkpointed: CheckpointNumber(5) }), + it('returns the leading proposed checkpoint number', async () => { + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(5) })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber: CheckpointNumber(7), + blockNumber: BlockNumber(7), + slotNumber: SlotNumber(7), + }), ); const result = await node.getCheckpointNumber('proposed'); expect(result).toEqual(CheckpointNumber(7)); }); - it('returns the proposedCheckpoint tip number when it equals the confirmed checkpoint (fallback already baked in)', async () => { - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ proposedCheckpoint: CheckpointNumber(5), checkpointed: CheckpointNumber(5) }), - ); + it('falls back to the confirmed checkpoint number when no proposed checkpoint leads it', async () => { + l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(5) })); + l2BlockSource.getProposedCheckpoint.mockResolvedValue(undefined); const result = await node.getCheckpointNumber('proposed'); expect(result).toEqual(CheckpointNumber(5)); @@ -1691,7 +1726,6 @@ describe('aztec node', () => { return { proposed: blockId(args.proposed), checkpointed: tipId(args.checkpointed), - proposedCheckpoint: tipId(args.checkpointed), proven: tipId(args.proven), finalized: tipId(args.finalized), }; diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 21cdf1101744..bd8f7b20c9a4 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -250,9 +250,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb return status.syncSummary; } - public async getChainTips(): Promise { - const { proposed, checkpointed, proven, finalized } = await this.blockSource.getL2Tips(); - return { proposed, checkpointed, proven, finalized }; + public getChainTips(): Promise { + return this.blockSource.getL2Tips(); } public getL1Constants() { @@ -289,7 +288,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb case 'checkpointed': return tips.checkpointed.checkpoint.number; case 'proposed': - return tips.proposedCheckpoint.checkpoint.number; + return await this.getProposedCheckpointNumber(tips.checkpointed.checkpoint.number); case 'proven': return tips.proven.checkpoint.number; case 'finalized': @@ -297,6 +296,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb } } + /** + * Resolves the `'proposed'` checkpoint frontier (latest proposed-but-not-yet-L1-confirmed + * checkpoint), falling back to the checkpointed frontier when no proposal leads it. + */ + private async getProposedCheckpointNumber(checkpointedCheckpointNumber: CheckpointNumber): Promise { + const proposedCheckpoint = await this.blockSource.getProposedCheckpoint(); + return proposedCheckpoint?.tip.checkpoint.number ?? checkpointedCheckpointNumber; + } + private isChainTip(value: unknown): value is ChainTip { return value === 'proposed' || value === 'checkpointed' || value === 'proven' || value === 'finalized'; } @@ -361,7 +369,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const tips = await this.blockSource.getL2Tips(); switch (param) { case 'proposed': - return { number: tips.proposedCheckpoint.checkpoint.number }; + return { number: await this.getProposedCheckpointNumber(tips.checkpointed.checkpoint.number) }; case 'checkpointed': return { number: tips.checkpointed.checkpoint.number }; case 'proven': @@ -1641,19 +1649,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const coinbase = EthAddress.ZERO; const feeRecipient = AztecAddress.ZERO; + // Resolve the proposed-checkpoint frontier (latest proposed checkpoint that leads the checkpointed + // tip, falling back to the checkpointed tip when none leads it). The leading case carries its header + // slot in the payload, so no extra block fetch is needed to derive the slot. + const proposedCheckpoint = await this.blockSource.getProposedCheckpoint(); + const proposedCheckpointBlockNumber = proposedCheckpoint?.tip.block.number ?? l2Tips.checkpointed.block.number; + const proposedCheckpointNumber = proposedCheckpoint?.tip.checkpoint.number ?? l2Tips.checkpointed.checkpoint.number; + // Define the slot for simulation as the max of the next L1 timestamp slot, the slot after the proposed // checkpoint, and the latest proposed block's slot. - const proposedCheckpointBlockData = await this.blockSource.getBlockData({ - number: l2Tips.proposedCheckpoint.block.number, - }); - const proposedCheckpointSlot = proposedCheckpointBlockData?.header.getSlot(); + const proposedCheckpointSlot = + proposedCheckpoint?.data.header.slotNumber ?? + (await this.blockSource.getBlockData({ number: proposedCheckpointBlockNumber }))?.header.getSlot(); let slotAfterProposedCheckpoint: SlotNumber | undefined; if (proposedCheckpointSlot !== undefined) { slotAfterProposedCheckpoint = SlotNumber.fromBigInt(BigInt(proposedCheckpointSlot) + 1n); } let latestProposedBlockSlot: SlotNumber | undefined; - if (l2Tips.proposed.number > l2Tips.proposedCheckpoint.block.number) { + if (l2Tips.proposed.number > proposedCheckpointBlockNumber) { latestProposedBlockSlot = ( await this.blockSource.getBlockData({ number: l2Tips.proposed.number }) )?.header.getSlot(); @@ -1690,11 +1704,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb // the world state tree so simulation can take them into account. We detect if the next block would // start a new checkpoint by checking if the proposed checkpoint's block number matches the latest block number, // which means the next block would be the first block of the next checkpoint. - const targetCheckpoint = CheckpointNumber( - (l2Tips.proposedCheckpoint.checkpoint.number ?? CheckpointNumber.ZERO) + 1, - ); + const targetCheckpoint = CheckpointNumber(proposedCheckpointNumber + 1); const nextCheckpointMessages: Fr[] | undefined = - l2Tips.proposedCheckpoint.block.number === l2Tips.proposed.number + proposedCheckpointBlockNumber === l2Tips.proposed.number ? await this.l1ToL2MessageSource.getL1ToL2Messages(targetCheckpoint).catch(err => { if (isErrorClass(err, L1ToL2MessagesNotReadyError)) { this.log.warn( @@ -1716,7 +1728,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb if (nextCheckpointMessages !== undefined) { this.log.debug( `Appending ${nextCheckpointMessages.length} L1-to-L2 messages to the world state tree for the next checkpoint`, - { checkpointNumber: l2Tips.proposedCheckpoint.checkpoint.number + 1 }, + { checkpointNumber: targetCheckpoint }, ); await appendL1ToL2MessagesToTree(merkleTreeFork, nextCheckpointMessages); } diff --git a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts index 863e08e69bbc..b6a7da0e593c 100644 --- a/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts +++ b/yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts @@ -319,7 +319,6 @@ describe('L1Publisher integration', () => { checkpointed: tipId, proven: tipId, finalized: tipId, - proposedCheckpoint: tipId, }; }, getBlockNumber(): Promise { diff --git a/yarn-project/pxe/src/block_synchronizer/block_stream_source.ts b/yarn-project/pxe/src/block_synchronizer/block_stream_source.ts index cbd6ed780cb2..8948ef621dae 100644 --- a/yarn-project/pxe/src/block_synchronizer/block_stream_source.ts +++ b/yarn-project/pxe/src/block_synchronizer/block_stream_source.ts @@ -11,10 +11,7 @@ export function blockStreamSourceFromAztecNode( node: AztecNode, ): Pick { return { - getL2Tips: async () => { - const tips = await node.getChainTips(); - return { ...tips, proposedCheckpoint: tips.checkpointed }; - }, + getL2Tips: () => node.getChainTips(), async getBlockData(query: BlockQuery): Promise { const response = await node.getBlock(query); diff --git a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts index 243175965897..906ec4a5424a 100644 --- a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts @@ -409,7 +409,10 @@ export class AutomineSequencer { await this.deps.ethCheatCodes.setNextBlockTimestamp(slotBoundaryTs); } - const tips = await this.deps.l2BlockSource.getL2Tips(); + const [tips, proposedCheckpoint] = await Promise.all([ + this.deps.l2BlockSource.getL2Tips(), + this.deps.l2BlockSource.getProposedCheckpoint(), + ]); const syncedToBlockNumber = tips.proposed.number; // Ensure world state has processed the archiver's tip before forking. Without this, @@ -419,7 +422,8 @@ export class AutomineSequencer { await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber)); const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1); - const checkpointNumber = CheckpointNumber(tips.proposedCheckpoint.checkpoint.number + 1); + const parentCheckpointNumber = proposedCheckpoint?.tip.checkpoint.number ?? tips.checkpointed.checkpoint.number; + const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1); const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants); this.log.verbose(`Building automine checkpoint`, { diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts index f1da2c37dcee..89978ee6e45c 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.test.ts @@ -258,10 +258,6 @@ describe('CheckpointProposalJob', () => { block: { number: BlockNumber.ZERO, hash: 'block-hash' }, checkpoint: { number: CheckpointNumber.ZERO, hash: 'checkpointed-ckpt-hash' }, }, - proposedCheckpoint: { - block: { number: BlockNumber.ZERO, hash: 'block-hash' }, - checkpoint: { number: CheckpointNumber.ZERO, hash: 'proposed-ckpt-hash' }, - }, proven: { block: { number: BlockNumber.ZERO, hash: 'proven-hash' }, checkpoint: { number: CheckpointNumber.ZERO, hash: 'proven-ckpt-hash' }, @@ -1093,10 +1089,6 @@ describe('CheckpointProposalJob', () => { hash: opts.checkpointedHash ?? parentCheckpointHash, }, }, - proposedCheckpoint: { - block: { number: BlockNumber(1), hash: 'block-hash' }, - checkpoint: { number: CheckpointNumber(1), hash: parentCheckpointHash }, - }, proven: { block: { number: BlockNumber.ZERO, hash: 'proven-hash' }, checkpoint: { number: CheckpointNumber.ZERO, hash: 'proven-ckpt-hash' }, diff --git a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts index 776929074d3c..2acf3e4cdf4c 100644 --- a/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/checkpoint_proposal_job.timing.test.ts @@ -458,10 +458,6 @@ describe('CheckpointProposalJob Timing Tests', () => { block: { number: BlockNumber.ZERO, hash: '' }, checkpoint: { number: CheckpointNumber(checkpointNumber - 1), hash: '' }, }, - proposedCheckpoint: { - block: { number: BlockNumber.ZERO, hash: '' }, - checkpoint: { number: CheckpointNumber(checkpointNumber - 1), hash: '' }, - }, proven: { block: { number: BlockNumber.ZERO, hash: '' }, checkpoint: { number: CheckpointNumber(0), hash: '' }, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index e60344fd8c61..ed43f228b12a 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -26,10 +26,11 @@ import { L2Block, type L2BlockSink, type L2BlockSource, + type ProposedCheckpoint, type ProposedCheckpointSink, type ValidateCheckpointNegativeResult, } from '@aztec/stdlib/block'; -import { Checkpoint, type ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; +import { Checkpoint } from '@aztec/stdlib/checkpoint'; import type { ChainConfig } from '@aztec/stdlib/config'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; @@ -314,10 +315,6 @@ describe('sequencer', () => { getBlockNumber: mockFn().mockResolvedValue(lastBlockNumber), getL2Tips: mockFn().mockResolvedValue({ proposed: { number: lastBlockNumber, hash }, - proposedCheckpoint: { - block: { number: lastBlockNumber, hash }, - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, - }, checkpointed: { block: { number: lastBlockNumber, hash }, checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, @@ -337,16 +334,13 @@ describe('sequencer', () => { getCheckpointsData: mockFn().mockResolvedValue([]), getSyncedL2SlotNumber: mockFn().mockResolvedValue(SlotNumber(Number.MAX_SAFE_INTEGER)), getProposedCheckpointData: mockFn().mockResolvedValue(undefined), + getProposedCheckpoint: mockFn().mockResolvedValue(undefined), }); l1ToL2MessageSource = mock({ getL1ToL2Messages: () => Promise.resolve(Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(Fr.ZERO)), getL2Tips: mockFn().mockResolvedValue({ proposed: { number: lastBlockNumber, hash }, - proposedCheckpoint: { - block: { number: lastBlockNumber, hash }, - checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, - }, checkpointed: { block: { number: lastBlockNumber, hash }, checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, @@ -1273,7 +1267,7 @@ describe('sequencer', () => { await setupSingleTxBlock(); // Override to non-genesis state so checkSync doesn't take the genesis path. - // proposedCheckpoint is set with checkpoint number 1 > checkpointed tip 0, so hasProposedCheckpoint is true. + // The proposed checkpoint has number 1 > checkpointed tip 0, so hasProposedCheckpoint is true. const nonGenesisHash = Fr.random().toString(); const proposedCheckpointHash = Fr.random().toString(); worldState.status.mockResolvedValue({ @@ -1288,10 +1282,6 @@ describe('sequencer', () => { } satisfies WorldStateSynchronizerStatus); const tipsWithBlock1 = { proposed: { number: BlockNumber(1), hash: nonGenesisHash }, - proposedCheckpoint: { - block: { number: BlockNumber(1), hash: nonGenesisHash }, - checkpoint: { number: CheckpointNumber(1), hash: proposedCheckpointHash }, - }, checkpointed: { block: { number: BlockNumber(1), hash: nonGenesisHash }, checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, @@ -1317,16 +1307,22 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpointData.mockResolvedValue({ - checkpointNumber: CheckpointNumber(1), - header: CheckpointHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: BlockNumber(1), - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - } satisfies ProposedCheckpointData); + l2BlockSource.getProposedCheckpoint.mockResolvedValue({ + tip: { + block: { number: BlockNumber(1), hash: nonGenesisHash }, + checkpoint: { number: CheckpointNumber(1), hash: proposedCheckpointHash }, + }, + data: { + checkpointNumber: CheckpointNumber(1), + header: CheckpointHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: BlockNumber(1), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, + }, + }); await sequencer.work(); @@ -1356,10 +1352,6 @@ describe('sequencer', () => { } satisfies WorldStateSynchronizerStatus); const tips = { proposed: { number: BlockNumber(3), hash: nonGenesisHash }, - proposedCheckpoint: { - block: { number: BlockNumber(2), hash: nonGenesisHash }, - checkpoint: { number: CheckpointNumber(2), hash: proposedCheckpointHash }, - }, checkpointed: { block: { number: BlockNumber(1), hash: nonGenesisHash }, checkpoint: { number: CheckpointNumber(1), hash: checkpointedHash }, @@ -1385,8 +1377,12 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(3), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpointData.mockResolvedValue({ - checkpointNumber: CheckpointNumber(2), + l2BlockSource.getProposedCheckpoint.mockResolvedValue({ + tip: { + block: { number: BlockNumber(2), hash: nonGenesisHash }, + checkpoint: { number: CheckpointNumber(2), hash: proposedCheckpointHash }, + }, + data: { checkpointNumber: CheckpointNumber(2) }, } as any); await sequencer.work(); @@ -1425,10 +1421,6 @@ describe('sequencer', () => { } satisfies WorldStateSynchronizerStatus); const tipsWithBlock1 = { proposed: { number: BlockNumber(1), hash: nonGenesisHash }, - proposedCheckpoint: { - block: { number: BlockNumber(1), hash: nonGenesisHash }, - checkpoint: { number: CheckpointNumber(1), hash: proposedCheckpointHash }, - }, checkpointed: { block: { number: BlockNumber(1), hash: nonGenesisHash }, checkpoint: { number: CheckpointNumber.ZERO, hash: GENESIS_CHECKPOINT_HEADER_HASH.toString() }, @@ -1454,16 +1446,22 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpointData.mockResolvedValue({ - checkpointNumber: CheckpointNumber(1), - header: CheckpointHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: BlockNumber(1), - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - } satisfies ProposedCheckpointData); + l2BlockSource.getProposedCheckpoint.mockResolvedValue({ + tip: { + block: { number: BlockNumber(1), hash: nonGenesisHash }, + checkpoint: { number: CheckpointNumber(1), hash: proposedCheckpointHash }, + }, + data: { + checkpointNumber: CheckpointNumber(1), + header: CheckpointHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: BlockNumber(1), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, + }, + }); await sequencer.work(); @@ -1498,18 +1496,17 @@ describe('sequencer', () => { describe('checkSync orphan-block guard', () => { // Mocks all sync sources so checkSync passes its earlier equality checks and reaches the orphan // guard, with the world-state tip at `blockNumber` (in `blockCheckpointNumber`) while the - // checkpointed and proposed-checkpoint tips sit at the given checkpoint numbers. + // checkpointed tip sits at `checkpointedCheckpointNumber`. The leading proposed checkpoint (if any) + // is supplied as the single atomic `getProposedCheckpoint` snapshot. const setupSyncedToBlock = (opts: { blockNumber: BlockNumber; blockSlot: SlotNumber; blockCheckpointNumber: CheckpointNumber; checkpointedCheckpointNumber: CheckpointNumber; - proposedCheckpointTipNumber: CheckpointNumber; - proposedCheckpointData: ProposedCheckpointData | undefined; + proposedCheckpoint: ProposedCheckpoint | undefined; }) => { const hash = Fr.random().toString(); const checkpointHash = Fr.random().toString(); - const proposedCheckpointHash = Fr.random().toString(); worldState.status.mockResolvedValue({ state: WorldStateRunningState.IDLE, syncSummary: { @@ -1522,10 +1519,6 @@ describe('sequencer', () => { } satisfies WorldStateSynchronizerStatus); const tips = { proposed: { number: opts.blockNumber, hash }, - proposedCheckpoint: { - block: { number: opts.blockNumber, hash }, - checkpoint: { number: opts.proposedCheckpointTipNumber, hash: proposedCheckpointHash }, - }, checkpointed: { block: { number: opts.blockNumber, hash }, checkpoint: { number: opts.checkpointedCheckpointNumber, hash: checkpointHash }, @@ -1551,20 +1544,19 @@ describe('sequencer', () => { checkpointNumber: opts.blockCheckpointNumber, indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpointData.mockResolvedValue(opts.proposedCheckpointData); + l2BlockSource.getProposedCheckpoint.mockResolvedValue(opts.proposedCheckpoint); }; it('returns undefined and logs debug while waiting for a matching proposed checkpoint', async () => { - // Local tip is a block at checkpoint 3, but the checkpointed and proposed-checkpoint tips are - // still at checkpoint 2 and no proposed checkpoint 3 exists: an orphan block-only tip whose - // enclosing checkpoint has not materialized into the archiver. + // Local tip is a block at checkpoint 3, but the checkpointed tip is still at checkpoint 2 and no + // proposed checkpoint 3 exists: an orphan block-only tip whose enclosing checkpoint has not + // materialized into the archiver. setupSyncedToBlock({ blockNumber: BlockNumber(3), blockSlot: SlotNumber(3), blockCheckpointNumber: CheckpointNumber(3), checkpointedCheckpointNumber: CheckpointNumber(2), - proposedCheckpointTipNumber: CheckpointNumber(2), - proposedCheckpointData: undefined, + proposedCheckpoint: undefined, }); const warnSpy = jest.spyOn(sequencer.getLogger(), 'warn'); const debugSpy = jest.spyOn(sequencer.getLogger(), 'debug'); @@ -1578,8 +1570,7 @@ describe('sequencer', () => { expect.objectContaining({ blockCheckpointNumber: CheckpointNumber(3), checkpointedCheckpointNumber: CheckpointNumber(2), - proposedCheckpointTipNumber: CheckpointNumber(2), - proposedCheckpointDataNumber: undefined, + proposedCheckpointTipNumber: undefined, }), ); }); @@ -1590,17 +1581,22 @@ describe('sequencer', () => { blockSlot: SlotNumber(3), blockCheckpointNumber: CheckpointNumber(3), checkpointedCheckpointNumber: CheckpointNumber(2), - proposedCheckpointTipNumber: CheckpointNumber(3), - proposedCheckpointData: { - checkpointNumber: CheckpointNumber(3), - header: CheckpointHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: BlockNumber(3), - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - } satisfies ProposedCheckpointData, + proposedCheckpoint: { + tip: { + block: { number: BlockNumber(3), hash: Fr.random().toString() }, + checkpoint: { number: CheckpointNumber(3), hash: Fr.random().toString() }, + }, + data: { + checkpointNumber: CheckpointNumber(3), + header: CheckpointHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: BlockNumber(3), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, + }, + }, }); const result = await sequencer.checkSyncForTest({ ts: 1000n, slot: SlotNumber(2) }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index cd2e66d6f4f2..6eeeca06bb7c 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -805,17 +805,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })), + this.l2BlockSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })), this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block), this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })), this.l2BlockSource.getPendingChainValidationStatus(), - this.l2BlockSource.getProposedCheckpointData(), + this.l2BlockSource.getProposedCheckpoint(), ] as const); - const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] = + const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpoint] = syncedBlocks; + const proposedCheckpointData = proposedCheckpoint?.data; const result = worldState.hash === l2Tips.proposed.hash && @@ -852,16 +851,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter l2Tips.checkpointed.checkpoint.number && - (l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber || - proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber) + proposedCheckpoint?.tip.checkpoint.number !== blockData.checkpointNumber ) { const logCtx = { blockCheckpointNumber: blockData.checkpointNumber, checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number, - proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number, - proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber, + proposedCheckpointTipNumber: proposedCheckpoint?.tip.checkpoint.number, blockNumber: blockData.header.getBlockNumber(), blockSlot: blockData.header.getSlot(), syncedL2Slot, @@ -872,27 +871,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter l2Tips.checkpointed.checkpoint.number; - - // The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots - // (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver - // write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but - // no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that - // window — the next checkSync tick will see a coherent snapshot. - if ( - hasProposedCheckpoint && - (!proposedCheckpointData || - proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number) - ) { - this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, { - proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number, - checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number, - proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber, - syncedL2Slot, - ...args, - }); - return undefined; - } + const hasProposedCheckpoint = proposedCheckpoint !== undefined; // Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building // The checkpoint number to build is derived as blockData.checkpointNumber + 1 diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index fe3a6175cef4..e0153b6fdfac 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -247,6 +247,15 @@ export interface L2BlockSource { */ getProposedCheckpointData(query?: ProposedCheckpointQuery): Promise; + /** + * Returns the latest proposed (not-yet-L1-confirmed) checkpoint that leads the checkpointed + * frontier, together with its derived chain tip, in a single atomic store read. The tip is + * derived entirely from the payload — checkpoint number, header hash, and last block + * (`startBlock + blockCount - 1`) — so callers never need a second read to reconcile tip and + * payload. Returns `undefined` when no proposed checkpoint exists beyond the checkpointed tip. + */ + getProposedCheckpoint(): Promise; + /** Force a sync. */ syncImmediate(): Promise; @@ -327,36 +336,29 @@ export interface L2BlockSourceEventEmitter extends L2BlockSource { } /** - * Identifier for L2 block tags. Internal counterpart to {@link BlockTag} that exposes - * the additional `proposedCheckpoint` value (used for the optimistic chain tip on the - * archiver side) and omits `latest` (which is an alias for `proposed` accepted only at - * the public RPC surface). + * Identifier for L2 block tags. Internal counterpart to {@link BlockTag} that omits `latest` + * (which is an alias for `proposed` accepted only at the public RPC surface). * * - proposed: Latest block proposed on L2. - * - proposedCheckpoint: Latest block in the most recent proposed checkpoint (archiver-internal). * - checkpointed: Latest block whose enclosing checkpoint has been published on L1. * - proven: Latest block whose enclosing checkpoint has been proven on L1. * - finalized: Latest block whose proving L1 transaction has reached L1 finality. - * - * TODO(palla): Remove `proposedCheckpoint` and unify with `proposed`. */ -export type L2BlockTag = 'proposed' | 'proposedCheckpoint' | 'checkpointed' | 'proven' | 'finalized'; +export type L2BlockTag = 'proposed' | 'checkpointed' | 'proven' | 'finalized'; /** Tips of the L2 chain. */ export type L2Tips = { proposed: L2BlockId; checkpointed: L2TipId; - proposedCheckpoint: L2TipId; proven: L2TipId; finalized: L2TipId; }; /** - * Tips of the L2 chain as tracked by a local provider (world-state, l2-tips-store). Omits - * `proposedCheckpoint`, which is degenerate in local stores (always equal to `checkpointed`) and - * is only meaningful on the archiver side via {@link L2BlockSource}. + * Tips of the L2 chain as tracked by a local provider (world-state, l2-tips-store). Identical to + * {@link L2Tips}; the alias is retained for call sites that document a local-only provenance. */ -export type LocalL2Tips = Omit; +export type LocalL2Tips = L2Tips; export const GENESIS_CHECKPOINT_HEADER_HASH = CheckpointHeader.empty().hash(); @@ -367,6 +369,13 @@ export type CheckpointId = { number: CheckpointNumber; hash: string }; export type L2TipId = { block: L2BlockId; checkpoint: CheckpointId }; +/** + * A proposed (not-yet-L1-confirmed) checkpoint paired with its derived chain tip. Returned by + * {@link L2BlockSource.getProposedCheckpoint} as a single atomic read so the `tip` and `data` + * are always a coherent snapshot of the same proposed checkpoint. + */ +export type ProposedCheckpoint = { tip: L2TipId; data: ProposedCheckpointData }; + /** Creates an L2 block id */ export function makeL2BlockId(number: BlockNumber, hash?: string): L2BlockId { if (number !== 0 && !hash) { @@ -390,7 +399,7 @@ const L2CheckpointIdSchema = z.object({ hash: z.string(), }); -const L2TipIdSchema = z.object({ +export const L2TipIdSchema = z.object({ block: L2BlockIdSchema, checkpoint: L2CheckpointIdSchema, }); @@ -398,7 +407,6 @@ const L2TipIdSchema = z.object({ export const L2TipsSchema = z.object({ proposed: L2BlockIdSchema, checkpointed: L2TipIdSchema, - proposedCheckpoint: L2TipIdSchema, proven: L2TipIdSchema, finalized: L2TipIdSchema, }); diff --git a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts index a123e9160682..28f7cb9d76cf 100644 --- a/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts +++ b/yarn-project/stdlib/src/block/l2_block_stream/l2_block_stream.test.ts @@ -61,14 +61,7 @@ describe('L2BlockStream', () => { }); /** Sets the remote tips. All tips default to 0 except latest. */ - const setRemoteTips = ( - latest_: number, - checkpointed_?: number, - proven?: number, - finalized?: number, - proposedCheckpoint_?: number, - ) => { - proposedCheckpoint_ = proposedCheckpoint_ ?? 0; + const setRemoteTips = (latest_: number, checkpointed_?: number, proven?: number, finalized?: number) => { checkpointed_ = checkpointed_ ?? 0; proven = proven ?? 0; finalized = finalized ?? 0; @@ -77,7 +70,6 @@ describe('L2BlockStream', () => { blockSource.getL2Tips.mockResolvedValue({ proposed: { number: BlockNumber(latest), hash: makeHash(latest) }, checkpointed: makeTipId(checkpointed_), - proposedCheckpoint: makeTipId(proposedCheckpoint_), proven: makeTipId(proven), finalized: makeTipId(finalized), }); diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index db0f41c3468d..4a03703718c7 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -18,6 +18,7 @@ import { type CheckpointQuery, type CheckpointsQuery, type L2Tips, + type ProposedCheckpoint, type ProposedCheckpointQuery, } from '../block/l2_block_source.js'; import type { ValidateCheckpointResult } from '../block/validate_block_result.js'; @@ -153,7 +154,6 @@ describe('ArchiverApiSchema', () => { expect(result).toEqual({ proposed: { number: 1, hash: `0x01` }, checkpointed: expectedTipId, - proposedCheckpoint: expectedTipId, proven: expectedTipId, finalized: expectedTipId, }); @@ -258,6 +258,26 @@ describe('ArchiverApiSchema', () => { }); }); + it('getProposedCheckpoint', async () => { + const result = await context.client.getProposedCheckpoint(); + expect(result).toEqual({ + tip: { + block: { number: 1, hash: `0x01` }, + checkpoint: { number: 1, hash: `0x01` }, + }, + data: { + checkpointNumber: 1, + header: expect.any(CheckpointHeader), + archive: expect.any(AppendOnlyTreeSnapshot), + checkpointOutHash: expect.any(Fr), + blockCount: 1, + startBlock: 1, + totalManaUsed: 1n, + feeAssetPriceModifier: 1n, + }, + }); + }); + it('getPendingChainValidationStatus', async () => { const result = await context.client.getPendingChainValidationStatus(); expect(result).toEqual({ valid: true }); @@ -378,6 +398,24 @@ class MockArchiver implements ArchiverApi { feeAssetPriceModifier: 1n, }); } + getProposedCheckpoint(): Promise { + return Promise.resolve({ + tip: { + block: { number: BlockNumber(1), hash: `0x01` }, + checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, + }, + data: { + checkpointNumber: CheckpointNumber(1), + header: CheckpointHeader.random(), + archive: AppendOnlyTreeSnapshot.random(), + checkpointOutHash: Fr.random(), + blockCount: 1, + startBlock: BlockNumber(1), + totalManaUsed: 1n, + feeAssetPriceModifier: 1n, + }, + }); + } syncImmediate() { return Promise.resolve(); } @@ -477,7 +515,6 @@ class MockArchiver implements ArchiverApi { return Promise.resolve({ proposed: { number: BlockNumber(1), hash: `0x01` }, checkpointed: tipId, - proposedCheckpoint: tipId, proven: tipId, finalized: tipId, }); diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 2b9ec63db52f..7edf696be516 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -11,6 +11,7 @@ import { CheckpointQuerySchema, CheckpointsQuerySchema, type L2BlockSource, + L2TipIdSchema, L2TipsSchema, ProposedCheckpointQuerySchema, } from '../block/l2_block_source.js'; @@ -141,6 +142,10 @@ export const ArchiverApiSchema: ApiSchemaFor = { input: z.tuple([optional(ProposedCheckpointQuerySchema)]), output: ProposedCheckpointDataSchema.optional(), }), + getProposedCheckpoint: z.function({ + input: z.tuple([]), + output: z.object({ tip: L2TipIdSchema, data: ProposedCheckpointDataSchema }).optional(), + }), syncImmediate: z.function({ input: z.tuple([]), output: z.void() }), isPendingChainInvalid: z.function({ input: z.tuple([]), output: z.boolean() }), getPendingChainValidationStatus: z.function({ input: z.tuple([]), output: ValidateCheckpointResultSchema }), diff --git a/yarn-project/stdlib/src/interfaces/chain_tips.ts b/yarn-project/stdlib/src/interfaces/chain_tips.ts index fde42b09a136..5836fea8747e 100644 --- a/yarn-project/stdlib/src/interfaces/chain_tips.ts +++ b/yarn-project/stdlib/src/interfaces/chain_tips.ts @@ -2,11 +2,8 @@ import { z } from 'zod'; import { type L2BlockTag, type L2Tips, L2TipsSchema } from '../block/l2_block_source.js'; -/** - * Public chain-tip selectors usable in RPC requests. - * Omits internal-only tags (e.g. `proposedCheckpoint`) from {@link L2BlockTag}. - */ -export type ChainTip = Exclude; +/** Public chain-tip selectors usable in RPC requests. */ +export type ChainTip = L2BlockTag; export const ChainTipSchema = z.union([ z.literal('proposed'), @@ -15,10 +12,7 @@ export const ChainTipSchema = z.union([ z.literal('finalized'), ]) satisfies z.ZodType; -/** - * Tips of the L2 chain. - * Omits the sequencer-internal `proposedCheckpoint` from the public RPC surface. - */ -export type ChainTips = Omit; +/** Tips of the L2 chain. */ +export type ChainTips = L2Tips; -export const ChainTipsSchema = L2TipsSchema.omit({ proposedCheckpoint: true }); +export const ChainTipsSchema = L2TipsSchema; diff --git a/yarn-project/stdlib/src/interfaces/prover-node.test.ts b/yarn-project/stdlib/src/interfaces/prover-node.test.ts index 770412e760f9..a6a832ed72a5 100644 --- a/yarn-project/stdlib/src/interfaces/prover-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/prover-node.test.ts @@ -45,7 +45,6 @@ describe('ProvingNodeApiSchema', () => { expect(result).toEqual({ proposed: { number: 1, hash: `0x01` }, checkpointed: expectedTipId, - proposedCheckpoint: expectedTipId, proven: expectedTipId, finalized: expectedTipId, }); @@ -76,7 +75,6 @@ class MockProverNode implements ProverNodeApi { return Promise.resolve({ proposed: { number: BlockNumber(1), hash: `0x01` }, checkpointed: tipId, - proposedCheckpoint: tipId, proven: tipId, finalized: tipId, }); diff --git a/yarn-project/stdlib/src/tests/factories.ts b/yarn-project/stdlib/src/tests/factories.ts index 0198fabd4bb4..298efb24082a 100644 --- a/yarn-project/stdlib/src/tests/factories.ts +++ b/yarn-project/stdlib/src/tests/factories.ts @@ -1745,10 +1745,6 @@ export function makeL2Tips( block: { number: bn, hash }, checkpoint: { number: cpn, hash: cph }, }, - proposedCheckpoint: { - block: { number: bn, hash }, - checkpoint: { number: cpn, hash: cph }, - }, proven: { block: { number: bn, hash }, checkpoint: { number: cpn, hash: cph }, diff --git a/yarn-project/txe/src/state_machine/archiver.ts b/yarn-project/txe/src/state_machine/archiver.ts index ab3b92562876..c91789f7d3bf 100644 --- a/yarn-project/txe/src/state_machine/archiver.ts +++ b/yarn-project/txe/src/state_machine/archiver.ts @@ -92,7 +92,6 @@ export class TXEArchiver extends ArchiverDataSourceBase { proven: tipId, finalized: tipId, checkpointed: tipId, - proposedCheckpoint: tipId, }; } From 5a80dfae9a1c5be9b83ce853b9bed311002f24f7 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 11 Jun 2026 22:29:52 -0300 Subject: [PATCH 2/3] refactor: address review on proposedCheckpoint removal (A-978) - Drop the node RPC 'proposed' checkpoint tag from getCheckpointNumber and getCheckpoint. The proposed-but-unconfirmed checkpoint frontier is an archiver-internal pipelining concept, not part of the public chain-tip surface, and was unused externally. Removes the getProposedCheckpointNumber helper. Breaking RPC change. - getProposedCheckpoint() now returns ProposedCheckpointData instead of { tip, data }. No consumer used the tip hashes; callers derive the last block from startBlock + blockCount - 1. Drops the extra block-store hash read while keeping the atomic leading-frontier read. - Unify the duplicate ChainTip/ChainTips/ChainTip(s)Schema aliases: block-level call sites use L2BlockTag/L2Tips/L2TipsSchema/BlockTagWithoutLatestSchema directly; checkpoint-level selectors use the new CheckpointTag/CheckpointTagSchema. --- .../archiver/src/archiver-sync.test.ts | 6 +- .../archiver/src/modules/data_source_base.ts | 3 +- .../archiver/src/store/block_store.test.ts | 15 +-- .../archiver/src/store/block_store.ts | 24 +--- .../archiver/src/test/mock_l2_block_source.ts | 3 +- .../register_node_rpc_handlers.test.ts | 14 +-- .../aztec-node/src/aztec-node/server.test.ts | 118 ++++-------------- .../aztec-node/src/aztec-node/server.ts | 45 +++---- .../epochs_missed_l1_publish.test.ts | 4 +- .../epochs_orphan_block_prune.test.ts | 4 +- .../sequencer/automine/automine_sequencer.ts | 2 +- .../src/sequencer/sequencer.test.ts | 82 ++++-------- .../src/sequencer/sequencer.ts | 14 +-- .../stdlib/src/block/l2_block_source.ts | 21 ++-- .../stdlib/src/interfaces/archiver.test.ts | 47 +++---- .../stdlib/src/interfaces/archiver.ts | 3 +- .../stdlib/src/interfaces/aztec-node.test.ts | 10 +- .../stdlib/src/interfaces/aztec-node.ts | 35 +++--- .../stdlib/src/interfaces/chain_tips.ts | 22 ++-- .../src/interfaces/checkpoint_parameter.ts | 6 +- 20 files changed, 163 insertions(+), 315 deletions(-) diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index 55ec29951fe1..2ac943fefdcd 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -2079,9 +2079,11 @@ describe('Archiver Sync', () => { const tips = await archiver.getL2Tips(); const proposedCheckpointResult = await archiver.getProposedCheckpoint(); expect(proposedCheckpointResult).toBeDefined(); - expect(proposedCheckpointResult!.tip.checkpoint.number).toEqual(CheckpointNumber(2)); + expect(proposedCheckpointResult!.checkpointNumber).toEqual(CheckpointNumber(2)); expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); - expect(proposedCheckpointResult!.tip.block.number).toBeGreaterThan(tips.checkpointed.block.number); + const proposedCheckpointLastBlock = + proposedCheckpointResult!.startBlock + proposedCheckpointResult!.blockCount - 1; + expect(proposedCheckpointLastBlock).toBeGreaterThan(tips.checkpointed.block.number); }, 15_000); it('prunes blocks and clears stale pending checkpoint when slot ends', async () => { diff --git a/yarn-project/archiver/src/modules/data_source_base.ts b/yarn-project/archiver/src/modules/data_source_base.ts index 41f51f153a4e..3fd70ff8eeca 100644 --- a/yarn-project/archiver/src/modules/data_source_base.ts +++ b/yarn-project/archiver/src/modules/data_source_base.ts @@ -21,7 +21,6 @@ import { type CheckpointsQuery, L2Block, type L2Tips, - type ProposedCheckpoint, type ProposedCheckpointQuery, } from '@aztec/stdlib/block'; import { @@ -289,7 +288,7 @@ export abstract class ArchiverDataSourceBase return this.stores.blocks.getProposedCheckpointBySlot(query.slot); } - public getProposedCheckpoint(): Promise { + public getProposedCheckpoint(): Promise { return this.stores.blocks.getProposedCheckpoint(); } diff --git a/yarn-project/archiver/src/store/block_store.test.ts b/yarn-project/archiver/src/store/block_store.test.ts index efabed86a00f..98b2ce63bedc 100644 --- a/yarn-project/archiver/src/store/block_store.test.ts +++ b/yarn-project/archiver/src/store/block_store.test.ts @@ -2637,7 +2637,7 @@ describe('BlockStore', () => { expect(await blockStore.getProposedCheckpoint()).toBeUndefined(); }); - it('returns the leading proposed checkpoint with a tip derived from its payload', async () => { + it('returns the leading proposed checkpoint payload', async () => { // Add checkpoint 1 const checkpoint1 = makePublishedCheckpoint( await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 }), @@ -2666,14 +2666,11 @@ describe('BlockStore', () => { const proposedCheckpoint = await blockStore.getProposedCheckpoint(); expect(proposedCheckpoint).toBeDefined(); - // Tip is derived from the payload: last block = startBlock + blockCount - 1, checkpoint number - // and header hash from the stored proposed checkpoint. - expect(proposedCheckpoint!.tip.block.number).toBe(BlockNumber(2)); - expect(proposedCheckpoint!.tip.block.hash).toBe((await block2.hash()).toString()); - expect(proposedCheckpoint!.tip.checkpoint.number).toBe(CheckpointNumber(2)); - expect(proposedCheckpoint!.tip.checkpoint.hash).toBe(header.hash().toString()); - expect(proposedCheckpoint!.data.checkpointNumber).toBe(CheckpointNumber(2)); - expect(proposedCheckpoint!.data.totalManaUsed).toBe(100n); + // Callers derive the tip from the payload: last block = startBlock + blockCount - 1. + expect(proposedCheckpoint!.checkpointNumber).toBe(CheckpointNumber(2)); + expect(proposedCheckpoint!.startBlock).toBe(BlockNumber(2)); + expect(proposedCheckpoint!.blockCount).toBe(1); + expect(proposedCheckpoint!.totalManaUsed).toBe(100n); }); }); diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index e419a9755e1c..ea6ceddc5a90 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -17,7 +17,6 @@ import { L2Block, type L2TipId, type L2Tips, - type ProposedCheckpoint, type ValidateCheckpointResult, deserializeValidateCheckpointResult, serializeValidateCheckpointResult, @@ -866,13 +865,13 @@ export class BlockStore { } /** - * Returns the latest proposed checkpoint that leads the checkpointed frontier, paired with its - * derived chain tip, in a single read-only transaction so the tip and payload are a coherent + * Returns the payload of the latest proposed checkpoint that leads the checkpointed frontier, in + * a single read-only transaction so the leading-frontier check and the payload are a coherent * snapshot. Returns undefined when no proposed checkpoint exists beyond the latest confirmed - * checkpoint. The tip's block number is `startBlock + blockCount - 1` and its block hash is read - * from the block store; the checkpoint hash is derived from the stored header. + * checkpoint. Callers derive the proposed tip from the payload (last block is + * `startBlock + blockCount - 1`). */ - async getProposedCheckpoint(): Promise { + async getProposedCheckpoint(): Promise { return await this.db.transactionAsync(async () => { const [entry] = await toArray(this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 })); if (entry === undefined) { @@ -882,18 +881,7 @@ export class BlockStore { if (entry[0] <= latestCheckpointNumber) { return undefined; } - const data = this.convertToProposedCheckpointData(entry[1]); - const blockNumber = BlockNumber(data.startBlock + data.blockCount - 1); - const blockStorage = await this.#blocks.getAsync(blockNumber); - if (!blockStorage) { - throw new BlockNotFoundError(blockNumber); - } - const blockHash = BlockHash.fromBuffer(blockStorage.blockHash).toString(); - const tip: L2TipId = { - block: { number: blockNumber, hash: blockHash }, - checkpoint: { number: data.checkpointNumber, hash: data.header.hash().toString() }, - }; - return { tip, data }; + return this.convertToProposedCheckpointData(entry[1]); }); } 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 74e78c736493..39b907b011db 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -27,7 +27,6 @@ import { L2Block, type L2BlockSource, type L2Tips, - type ProposedCheckpoint, type ProposedCheckpointQuery, type ValidateCheckpointResult, } from '@aztec/stdlib/block'; @@ -594,7 +593,7 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(undefined); } - getProposedCheckpoint(): Promise { + getProposedCheckpoint(): Promise { return Promise.resolve(undefined); } diff --git a/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts b/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts index dd71b0e627ce..d35a9d531ddc 100644 --- a/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/register_node_rpc_handlers.test.ts @@ -6,19 +6,15 @@ import { createNamespacedSafeJsonRpcServer, startHttpRpcServer, } from '@aztec/foundation/json-rpc/server'; -import { - AztecNodeAdminApiSchema, - AztecNodeApiSchema, - AztecNodeDebugApiSchema, - type ChainTips, -} from '@aztec/stdlib/interfaces/client'; +import type { L2Tips } from '@aztec/stdlib/block'; +import { AztecNodeAdminApiSchema, AztecNodeApiSchema, AztecNodeDebugApiSchema } from '@aztec/stdlib/interfaces/client'; import { P2PApiSchema } from '@aztec/stdlib/interfaces/server'; import type { ApiSchemaFor } from '@aztec/stdlib/schemas'; import { registerAztecNodeRpcHandlers } from './register_node_rpc_handlers.js'; import type { AztecNodeService } from './server.js'; -type GetChainTipsOnly = { getChainTips(): Promise }; +type GetChainTipsOnly = { getChainTips(): Promise }; const GetChainTipsOnlySchema: ApiSchemaFor = { getChainTips: AztecNodeApiSchema.getChainTips, @@ -28,7 +24,7 @@ const p2p = {}; const mockNode = { getP2P: () => p2p, - getChainTips(): Promise { + getChainTips(): Promise { const tipId = { block: { number: BlockNumber(1), hash: `0x01` }, checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, @@ -90,7 +86,7 @@ describe('registerAztecNodeRpcHandlers', () => { headers: { 'content-type': 'application/json' }, body: jsonStringify({ jsonrpc: '2.0', id: 1, method: 'node_getChainTips', params: [] }), }); - const body = (await response.json()) as { result: ChainTips }; + const body = (await response.json()) as { result: L2Tips }; expect(body.result).toEqual(expected); httpServer.close(); diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index 0fac83ed1914..a797ac7c4e32 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -31,7 +31,6 @@ import { L2Block, type L2BlockSource, type L2Tips, - type ProposedCheckpoint, } from '@aztec/stdlib/block'; import type { CheckpointData, ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; import type { ContractDataSource } from '@aztec/stdlib/contract'; @@ -1412,27 +1411,21 @@ describe('aztec node', () => { }; } - /** Builds a ProposedCheckpoint stub (the atomic leading-proposed-checkpoint snapshot). */ + /** Builds the payload of the atomic leading-proposed-checkpoint read (last block = startBlock). */ function makeProposedCheckpoint(args: { checkpointNumber: CheckpointNumber; blockNumber: BlockNumber; slotNumber: SlotNumber; - }): ProposedCheckpoint { + }): ProposedCheckpointData { return { - tip: { - block: { number: args.blockNumber, hash: '' }, - checkpoint: { number: args.checkpointNumber, hash: '' }, - }, - data: { - checkpointNumber: args.checkpointNumber, - header: CheckpointHeader.random({ slotNumber: args.slotNumber }), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: args.blockNumber, - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - }, + checkpointNumber: args.checkpointNumber, + header: CheckpointHeader.random({ slotNumber: args.slotNumber }), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: args.blockNumber, + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, }; } @@ -1470,40 +1463,6 @@ describe('aztec node', () => { } describe('throw guards', () => { - it('throws BadRequestError when "proposed" resolves to a proposed entry and includeL1PublishInfo is requested', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(4) })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( - makeProposedCheckpoint({ - checkpointNumber: CheckpointNumber(5), - blockNumber: BlockNumber(5), - slotNumber: SlotNumber(10), - }), - ); - l2BlockSource.getCheckpointData.mockResolvedValue(undefined); - l2BlockSource.getProposedCheckpointData.mockResolvedValue( - makeProposedCheckpointData(CheckpointNumber(5), SlotNumber(10)), - ); - - await expect(node.getCheckpoint('proposed', { includeL1PublishInfo: true })).rejects.toThrow(BadRequestError); - }); - - it('throws BadRequestError when "proposed" resolves to a proposed entry and includeAttestations is requested', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(4) })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( - makeProposedCheckpoint({ - checkpointNumber: CheckpointNumber(5), - blockNumber: BlockNumber(5), - slotNumber: SlotNumber(10), - }), - ); - l2BlockSource.getCheckpointData.mockResolvedValue(undefined); - l2BlockSource.getProposedCheckpointData.mockResolvedValue( - makeProposedCheckpointData(CheckpointNumber(5), SlotNumber(10)), - ); - - await expect(node.getCheckpoint('proposed', { includeAttestations: true })).rejects.toThrow(BadRequestError); - }); - it('throws BadRequestError when number lookup resolves to a proposed entry and includeL1PublishInfo is requested', async () => { l2BlockSource.getCheckpointData.mockResolvedValue(undefined); l2BlockSource.getProposedCheckpointData.mockResolvedValue( @@ -1528,36 +1487,6 @@ describe('aztec node', () => { }); describe('fallback semantics', () => { - it('getCheckpoint("proposed") returns the projected proposed entry when one exists at the proposed-tip number', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(1) })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( - makeProposedCheckpoint({ - checkpointNumber: CheckpointNumber(2), - blockNumber: BlockNumber(2), - slotNumber: SlotNumber(5), - }), - ); - l2BlockSource.getCheckpointData.mockResolvedValue(undefined); - const proposed = makeProposedCheckpointData(CheckpointNumber(2), SlotNumber(5)); - l2BlockSource.getProposedCheckpointData.mockResolvedValue(proposed); - - const result = await node.getCheckpoint('proposed'); - expect(result).toBeDefined(); - expect(result!.number).toEqual(CheckpointNumber(2)); - }); - - it('getCheckpoint("proposed") returns the latest confirmed checkpoint when no proposed entry exists', async () => { - // When no proposed checkpoint leads the frontier, the proposed tip falls back to the confirmed tip. - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(3) })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue(undefined); - const confirmed = makeCheckpointData(CheckpointNumber(3)); - l2BlockSource.getCheckpointData.mockResolvedValue(confirmed); - - const result = await node.getCheckpoint('proposed'); - expect(result).toBeDefined(); - expect(result!.number).toEqual(CheckpointNumber(3)); - }); - it('getCheckpoint({ number }) returns the confirmed entry when one exists', async () => { const confirmed = makeCheckpointData(CheckpointNumber(3)); l2BlockSource.getCheckpointData.mockResolvedValue(confirmed); @@ -1667,26 +1596,23 @@ describe('aztec node', () => { }); describe('getCheckpointNumber', () => { - it('returns the leading proposed checkpoint number', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(5) })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( - makeProposedCheckpoint({ - checkpointNumber: CheckpointNumber(7), - blockNumber: BlockNumber(7), - slotNumber: SlotNumber(7), - }), + beforeEach(() => { + l2BlockSource.getL2Tips.mockResolvedValue( + makeTips({ checkpointed: CheckpointNumber(5), proven: CheckpointNumber(3), finalized: CheckpointNumber(2) }), ); + }); - const result = await node.getCheckpointNumber('proposed'); - expect(result).toEqual(CheckpointNumber(7)); + it('returns the checkpointed number by default', async () => { + expect(await node.getCheckpointNumber()).toEqual(CheckpointNumber(5)); + expect(await node.getCheckpointNumber('checkpointed')).toEqual(CheckpointNumber(5)); }); - it('falls back to the confirmed checkpoint number when no proposed checkpoint leads it', async () => { - l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ checkpointed: CheckpointNumber(5) })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue(undefined); + it('returns the proven checkpoint number', async () => { + expect(await node.getCheckpointNumber('proven')).toEqual(CheckpointNumber(3)); + }); - const result = await node.getCheckpointNumber('proposed'); - expect(result).toEqual(CheckpointNumber(5)); + it('returns the finalized checkpoint number', async () => { + expect(await node.getCheckpointNumber('finalized')).toEqual(CheckpointNumber(2)); }); }); diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index bd8f7b20c9a4..3680293d3abc 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -70,6 +70,7 @@ import { type CommitteeAttestation, type DataInBlock, type L2BlockSource, + type L2BlockTag, type L2Tips, type NormalizedBlockParameter, inspectBlockParameter, @@ -98,11 +99,10 @@ import type { BlockIncludeOptions, BlockResponse, BlocksIncludeOptions, - ChainTip, - ChainTips, CheckpointIncludeOptions, CheckpointParameter, CheckpointResponse, + CheckpointTag, GetTxByHashOptions, PeerInfo, ProposalsForSlot, @@ -250,7 +250,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb return status.syncSummary; } - public getChainTips(): Promise { + public getChainTips(): Promise { return this.blockSource.getL2Tips(); } @@ -274,21 +274,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb return this.blockSource.getCheckpointsData(query); } - public async getBlockNumber(tip?: ChainTip): Promise { + public async getBlockNumber(tip?: L2BlockTag): Promise { if (tip === undefined || tip === 'proposed') { return this.blockSource.getBlockNumber(); } return (await this.blockSource.getBlockNumber({ tag: tip })) ?? BlockNumber.ZERO; } - public async getCheckpointNumber(tip?: ChainTip): Promise { + public async getCheckpointNumber(tip?: CheckpointTag): Promise { const tips = await this.blockSource.getL2Tips(); switch (tip) { case undefined: case 'checkpointed': return tips.checkpointed.checkpoint.number; - case 'proposed': - return await this.getProposedCheckpointNumber(tips.checkpointed.checkpoint.number); case 'proven': return tips.proven.checkpoint.number; case 'finalized': @@ -296,17 +294,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb } } - /** - * Resolves the `'proposed'` checkpoint frontier (latest proposed-but-not-yet-L1-confirmed - * checkpoint), falling back to the checkpointed frontier when no proposal leads it. - */ - private async getProposedCheckpointNumber(checkpointedCheckpointNumber: CheckpointNumber): Promise { - const proposedCheckpoint = await this.blockSource.getProposedCheckpoint(); - return proposedCheckpoint?.tip.checkpoint.number ?? checkpointedCheckpointNumber; - } - - private isChainTip(value: unknown): value is ChainTip { - return value === 'proposed' || value === 'checkpointed' || value === 'proven' || value === 'finalized'; + private isCheckpointTag(value: unknown): value is CheckpointTag { + return value === 'checkpointed' || value === 'proven' || value === 'finalized'; } /** @@ -354,10 +343,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb /** * Resolves a {@link CheckpointParameter} into a concrete `{ number }` or `{ slot }` query. * - * Tag-based parameters (`'proposed'`, `'checkpointed'`, `'proven'`, `'finalized'`) are - * translated up-front to the corresponding tip's checkpoint number via {@link L2BlockSource.getL2Tips}. - * After resolution the unified {@link getCheckpoint} flow can perform a single - * confirmed→proposed lookup against either store. + * Tag-based parameters (`'checkpointed'`, `'proven'`, `'finalized'`) are translated up-front to the + * corresponding tip's checkpoint number via {@link L2BlockSource.getL2Tips}. After resolution the + * unified {@link getCheckpoint} flow can perform a single confirmed→proposed lookup against either + * store. */ private async resolveCheckpointParameter( param: CheckpointParameter, @@ -365,11 +354,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb if (typeof param === 'number') { return { number: param as CheckpointNumber }; } - if (this.isChainTip(param)) { + if (this.isCheckpointTag(param)) { const tips = await this.blockSource.getL2Tips(); switch (param) { - case 'proposed': - return { number: await this.getProposedCheckpointNumber(tips.checkpointed.checkpoint.number) }; case 'checkpointed': return { number: tips.checkpointed.checkpoint.number }; case 'proven': @@ -1653,13 +1640,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb // tip, falling back to the checkpointed tip when none leads it). The leading case carries its header // slot in the payload, so no extra block fetch is needed to derive the slot. const proposedCheckpoint = await this.blockSource.getProposedCheckpoint(); - const proposedCheckpointBlockNumber = proposedCheckpoint?.tip.block.number ?? l2Tips.checkpointed.block.number; - const proposedCheckpointNumber = proposedCheckpoint?.tip.checkpoint.number ?? l2Tips.checkpointed.checkpoint.number; + const proposedCheckpointBlockNumber = proposedCheckpoint + ? BlockNumber(proposedCheckpoint.startBlock + proposedCheckpoint.blockCount - 1) + : l2Tips.checkpointed.block.number; + const proposedCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? l2Tips.checkpointed.checkpoint.number; // Define the slot for simulation as the max of the next L1 timestamp slot, the slot after the proposed // checkpoint, and the latest proposed block's slot. const proposedCheckpointSlot = - proposedCheckpoint?.data.header.slotNumber ?? + proposedCheckpoint?.header.slotNumber ?? (await this.blockSource.getBlockData({ number: proposedCheckpointBlockNumber }))?.header.getSlot(); let slotAfterProposedCheckpoint: SlotNumber | undefined; if (proposedCheckpointSlot !== undefined) { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts index fb5aa4afd1ce..d7cfa7be8bd6 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_missed_l1_publish.test.ts @@ -12,8 +12,8 @@ import { retryUntil } from '@aztec/foundation/retry'; import { bufferToHex } from '@aztec/foundation/string'; import { timeoutPromise } from '@aztec/foundation/timer'; import { type L2Block, L2BlockSourceEvents } from '@aztec/stdlib/block'; +import type { L2Tips } from '@aztec/stdlib/block'; import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; -import type { ChainTips } from '@aztec/stdlib/interfaces/server'; import { jest } from '@jest/globals'; import { privateKeyToAccount } from 'viem/accounts'; @@ -178,7 +178,7 @@ describe('e2e_epochs/epochs_missed_l1_publish', () => { // We capture the L2 tips synchronously inside the handler — the archiver has already removed // the pruned blocks at emit time, so this snapshot reflects the rolled-back state before any // new pipelined block can be applied. - type PruneObservation = { slotNumber: SlotNumber; blocks: L2Block[]; tipsAtPrune: ChainTips }; + type PruneObservation = { slotNumber: SlotNumber; blocks: L2Block[]; tipsAtPrune: L2Tips }; const prunePromises: Promise[] = nodes.map( (node, idx) => new Promise(resolve => { diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts index dbf1a088b277..cca27d9b2b23 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_orphan_block_prune.test.ts @@ -11,8 +11,8 @@ import { retryUntil } from '@aztec/foundation/retry'; import { bufferToHex } from '@aztec/foundation/string'; import { timeoutPromise } from '@aztec/foundation/timer'; import { type L2Block, L2BlockSourceEvents } from '@aztec/stdlib/block'; +import type { L2Tips } from '@aztec/stdlib/block'; import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers'; -import type { ChainTips } from '@aztec/stdlib/interfaces/server'; import { jest } from '@jest/globals'; import { privateKeyToAccount } from 'viem/accounts'; @@ -157,7 +157,7 @@ describe('e2e_epochs/epochs_orphan_block_prune', () => { // Subscribe to the prune event on every node before sequencers start, so we never miss it. We capture the chain // tips asynchronously inside the handler for log context, but do not assert on them — by the time the snapshot is // read, P2's rebuild may already have landed. - type PruneObservation = { slotNumber: SlotNumber; blocks: L2Block[]; tipsAtPrune: ChainTips }; + type PruneObservation = { slotNumber: SlotNumber; blocks: L2Block[]; tipsAtPrune: L2Tips }; const prunePromises: Promise[] = nodes.map( (node, idx) => new Promise(resolve => { diff --git a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts index 906ec4a5424a..c8d790387fd1 100644 --- a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts @@ -422,7 +422,7 @@ export class AutomineSequencer { await this.deps.worldState.syncImmediate(BlockNumber(syncedToBlockNumber)); const nextBlockNumber = BlockNumber(syncedToBlockNumber + 1); - const parentCheckpointNumber = proposedCheckpoint?.tip.checkpoint.number ?? tips.checkpointed.checkpoint.number; + const parentCheckpointNumber = proposedCheckpoint?.checkpointNumber ?? tips.checkpointed.checkpoint.number; const checkpointNumber = CheckpointNumber(parentCheckpointNumber + 1); const targetEpoch = getEpochAtSlot(SlotNumber(targetSlot), this.deps.l1Constants); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index ed43f228b12a..1c77c3a3247a 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -26,11 +26,10 @@ import { L2Block, type L2BlockSink, type L2BlockSource, - type ProposedCheckpoint, type ProposedCheckpointSink, type ValidateCheckpointNegativeResult, } from '@aztec/stdlib/block'; -import { Checkpoint } from '@aztec/stdlib/checkpoint'; +import { Checkpoint, type ProposedCheckpointData } from '@aztec/stdlib/checkpoint'; import type { ChainConfig } from '@aztec/stdlib/config'; import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers'; import { GasFees } from '@aztec/stdlib/gas'; @@ -1269,7 +1268,6 @@ describe('sequencer', () => { // Override to non-genesis state so checkSync doesn't take the genesis path. // The proposed checkpoint has number 1 > checkpointed tip 0, so hasProposedCheckpoint is true. const nonGenesisHash = Fr.random().toString(); - const proposedCheckpointHash = Fr.random().toString(); worldState.status.mockResolvedValue({ state: WorldStateRunningState.IDLE, syncSummary: { @@ -1308,20 +1306,14 @@ describe('sequencer', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); l2BlockSource.getProposedCheckpoint.mockResolvedValue({ - tip: { - block: { number: BlockNumber(1), hash: nonGenesisHash }, - checkpoint: { number: CheckpointNumber(1), hash: proposedCheckpointHash }, - }, - data: { - checkpointNumber: CheckpointNumber(1), - header: CheckpointHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: BlockNumber(1), - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - }, + checkpointNumber: CheckpointNumber(1), + header: CheckpointHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: BlockNumber(1), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, }); await sequencer.work(); @@ -1338,7 +1330,6 @@ describe('sequencer', () => { // Confirmed checkpoint is 1, pending is 2, proposed tip is in checkpoint 3. // So sequencer would try to build checkpoint 4, which exceeds the 1-deep pipeline limit. const nonGenesisHash = Fr.random().toString(); - const proposedCheckpointHash = Fr.random().toString(); const checkpointedHash = Fr.random().toString(); worldState.status.mockResolvedValue({ state: WorldStateRunningState.IDLE, @@ -1377,13 +1368,7 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(3), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpoint.mockResolvedValue({ - tip: { - block: { number: BlockNumber(2), hash: nonGenesisHash }, - checkpoint: { number: CheckpointNumber(2), hash: proposedCheckpointHash }, - }, - data: { checkpointNumber: CheckpointNumber(2) }, - } as any); + l2BlockSource.getProposedCheckpoint.mockResolvedValue({ checkpointNumber: CheckpointNumber(2) } as any); await sequencer.work(); @@ -1408,7 +1393,6 @@ describe('sequencer', () => { // Set up a pipelined parent (pending override = parentCheckpointNumber = 1). const nonGenesisHash = Fr.random().toString(); - const proposedCheckpointHash = Fr.random().toString(); worldState.status.mockResolvedValue({ state: WorldStateRunningState.IDLE, syncSummary: { @@ -1447,20 +1431,14 @@ describe('sequencer', () => { indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); l2BlockSource.getProposedCheckpoint.mockResolvedValue({ - tip: { - block: { number: BlockNumber(1), hash: nonGenesisHash }, - checkpoint: { number: CheckpointNumber(1), hash: proposedCheckpointHash }, - }, - data: { - checkpointNumber: CheckpointNumber(1), - header: CheckpointHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: BlockNumber(1), - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - }, + checkpointNumber: CheckpointNumber(1), + header: CheckpointHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: BlockNumber(1), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, }); await sequencer.work(); @@ -1503,7 +1481,7 @@ describe('sequencer', () => { blockSlot: SlotNumber; blockCheckpointNumber: CheckpointNumber; checkpointedCheckpointNumber: CheckpointNumber; - proposedCheckpoint: ProposedCheckpoint | undefined; + proposedCheckpoint: ProposedCheckpointData | undefined; }) => { const hash = Fr.random().toString(); const checkpointHash = Fr.random().toString(); @@ -1582,20 +1560,14 @@ describe('sequencer', () => { blockCheckpointNumber: CheckpointNumber(3), checkpointedCheckpointNumber: CheckpointNumber(2), proposedCheckpoint: { - tip: { - block: { number: BlockNumber(3), hash: Fr.random().toString() }, - checkpoint: { number: CheckpointNumber(3), hash: Fr.random().toString() }, - }, - data: { - checkpointNumber: CheckpointNumber(3), - header: CheckpointHeader.empty(), - archive: AppendOnlyTreeSnapshot.empty(), - checkpointOutHash: Fr.ZERO, - startBlock: BlockNumber(3), - blockCount: 1, - totalManaUsed: 0n, - feeAssetPriceModifier: 0n, - }, + checkpointNumber: CheckpointNumber(3), + header: CheckpointHeader.empty(), + archive: AppendOnlyTreeSnapshot.empty(), + checkpointOutHash: Fr.ZERO, + startBlock: BlockNumber(3), + blockCount: 1, + totalManaUsed: 0n, + feeAssetPriceModifier: 0n, }, }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 6eeeca06bb7c..6d2bf9914b9d 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -812,9 +812,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter TypedEventEmitter l2Tips.checkpointed.checkpoint.number && - proposedCheckpoint?.tip.checkpoint.number !== blockData.checkpointNumber + proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber ) { const logCtx = { blockCheckpointNumber: blockData.checkpointNumber, checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number, - proposedCheckpointTipNumber: proposedCheckpoint?.tip.checkpoint.number, + proposedCheckpointTipNumber: proposedCheckpointData?.checkpointNumber, blockNumber: blockData.header.getBlockNumber(), blockSlot: blockData.header.getSlot(), syncedL2Slot, @@ -871,7 +871,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter; /** - * Returns the latest proposed (not-yet-L1-confirmed) checkpoint that leads the checkpointed - * frontier, together with its derived chain tip, in a single atomic store read. The tip is - * derived entirely from the payload — checkpoint number, header hash, and last block - * (`startBlock + blockCount - 1`) — so callers never need a second read to reconcile tip and - * payload. Returns `undefined` when no proposed checkpoint exists beyond the checkpointed tip. + * Returns the payload of the latest proposed (not-yet-L1-confirmed) checkpoint that leads the + * checkpointed frontier, in a single atomic store read. Returns `undefined` when no proposed + * checkpoint exists beyond the checkpointed tip. Callers derive the proposed tip from the payload + * (checkpoint number, and last block `startBlock + blockCount - 1`); unlike + * {@link getProposedCheckpointData}, this applies the leading-frontier filter. */ - getProposedCheckpoint(): Promise; + getProposedCheckpoint(): Promise; /** Force a sync. */ syncImmediate(): Promise; @@ -369,13 +369,6 @@ export type CheckpointId = { number: CheckpointNumber; hash: string }; export type L2TipId = { block: L2BlockId; checkpoint: CheckpointId }; -/** - * A proposed (not-yet-L1-confirmed) checkpoint paired with its derived chain tip. Returned by - * {@link L2BlockSource.getProposedCheckpoint} as a single atomic read so the `tip` and `data` - * are always a coherent snapshot of the same proposed checkpoint. - */ -export type ProposedCheckpoint = { tip: L2TipId; data: ProposedCheckpointData }; - /** Creates an L2 block id */ export function makeL2BlockId(number: BlockNumber, hash?: string): L2BlockId { if (number !== 0 && !hash) { @@ -399,7 +392,7 @@ const L2CheckpointIdSchema = z.object({ hash: z.string(), }); -export const L2TipIdSchema = z.object({ +const L2TipIdSchema = z.object({ block: L2BlockIdSchema, checkpoint: L2CheckpointIdSchema, }); diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 4a03703718c7..467125bcd991 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -18,7 +18,6 @@ import { type CheckpointQuery, type CheckpointsQuery, type L2Tips, - type ProposedCheckpoint, type ProposedCheckpointQuery, } from '../block/l2_block_source.js'; import type { ValidateCheckpointResult } from '../block/validate_block_result.js'; @@ -261,20 +260,14 @@ describe('ArchiverApiSchema', () => { it('getProposedCheckpoint', async () => { const result = await context.client.getProposedCheckpoint(); expect(result).toEqual({ - tip: { - block: { number: 1, hash: `0x01` }, - checkpoint: { number: 1, hash: `0x01` }, - }, - data: { - checkpointNumber: 1, - header: expect.any(CheckpointHeader), - archive: expect.any(AppendOnlyTreeSnapshot), - checkpointOutHash: expect.any(Fr), - blockCount: 1, - startBlock: 1, - totalManaUsed: 1n, - feeAssetPriceModifier: 1n, - }, + checkpointNumber: 1, + header: expect.any(CheckpointHeader), + archive: expect.any(AppendOnlyTreeSnapshot), + checkpointOutHash: expect.any(Fr), + blockCount: 1, + startBlock: 1, + totalManaUsed: 1n, + feeAssetPriceModifier: 1n, }); }); @@ -398,22 +391,16 @@ class MockArchiver implements ArchiverApi { feeAssetPriceModifier: 1n, }); } - getProposedCheckpoint(): Promise { + getProposedCheckpoint(): Promise { return Promise.resolve({ - tip: { - block: { number: BlockNumber(1), hash: `0x01` }, - checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, - }, - data: { - checkpointNumber: CheckpointNumber(1), - header: CheckpointHeader.random(), - archive: AppendOnlyTreeSnapshot.random(), - checkpointOutHash: Fr.random(), - blockCount: 1, - startBlock: BlockNumber(1), - totalManaUsed: 1n, - feeAssetPriceModifier: 1n, - }, + checkpointNumber: CheckpointNumber(1), + header: CheckpointHeader.random(), + archive: AppendOnlyTreeSnapshot.random(), + checkpointOutHash: Fr.random(), + blockCount: 1, + startBlock: BlockNumber(1), + totalManaUsed: 1n, + feeAssetPriceModifier: 1n, }); } syncImmediate() { diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 7edf696be516..6cd56b62a600 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -11,7 +11,6 @@ import { CheckpointQuerySchema, CheckpointsQuerySchema, type L2BlockSource, - L2TipIdSchema, L2TipsSchema, ProposedCheckpointQuerySchema, } from '../block/l2_block_source.js'; @@ -144,7 +143,7 @@ export const ArchiverApiSchema: ApiSchemaFor = { }), getProposedCheckpoint: z.function({ input: z.tuple([]), - output: z.object({ tip: L2TipIdSchema, data: ProposedCheckpointDataSchema }).optional(), + output: ProposedCheckpointDataSchema.optional(), }), syncImmediate: z.function({ input: z.tuple([]), output: z.void() }), isPendingChainInvalid: z.function({ input: z.tuple([]), output: z.boolean() }), diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts index bd404091f483..fb23d9d56458 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.test.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.test.ts @@ -22,7 +22,7 @@ import { AztecAddress } from '../aztec-address/index.js'; import type { BlockData } from '../block/block_data.js'; import type { DataInBlock } from '../block/in_block.js'; import { BlockHash, type BlockParameter } from '../block/index.js'; -import type { CheckpointsQuery } from '../block/l2_block_source.js'; +import type { CheckpointsQuery, L2BlockTag, L2Tips } from '../block/l2_block_source.js'; import type { CheckpointData } from '../checkpoint/checkpoint_data.js'; import { type ContractClassPublic, @@ -64,7 +64,7 @@ import type { AllowedElement } from './allowed_element.js'; import { MAX_RPC_LEN } from './api_limit.js'; import { type AztecNode, AztecNodeApiSchema, type GetTxByHashOptions } from './aztec-node.js'; import type { BlockIncludeOptions, BlockResponse, BlocksIncludeOptions } from './block_response.js'; -import type { ChainTip, ChainTips } from './chain_tips.js'; +import type { CheckpointTag } from './chain_tips.js'; import type { CheckpointParameter } from './checkpoint_parameter.js'; import type { CheckpointIncludeOptions, CheckpointResponse } from './checkpoint_response.js'; import type { SequencerConfig } from './configs.js'; @@ -596,7 +596,7 @@ class MockAztecNode implements AztecNode { }); } - getChainTips(): Promise { + getChainTips(): Promise { const tipId = { block: { number: BlockNumber(1), hash: `0x01` }, checkpoint: { number: CheckpointNumber(1), hash: `0x01` }, @@ -765,10 +765,10 @@ class MockAztecNode implements AztecNode { getMaxPriorityFees(): Promise { return Promise.resolve(GasFees.empty()); } - getBlockNumber(_tip?: ChainTip): Promise { + getBlockNumber(_tip?: L2BlockTag): Promise { return Promise.resolve(BlockNumber(1)); } - getCheckpointNumber(_tip?: ChainTip): Promise { + getCheckpointNumber(_tip?: CheckpointTag): Promise { return Promise.resolve(CheckpointNumber(1)); } isReady(): Promise { diff --git a/yarn-project/stdlib/src/interfaces/aztec-node.ts b/yarn-project/stdlib/src/interfaces/aztec-node.ts index 99ee4d88a88c..62baf29ecb06 100644 --- a/yarn-project/stdlib/src/interfaces/aztec-node.ts +++ b/yarn-project/stdlib/src/interfaces/aztec-node.ts @@ -23,9 +23,15 @@ import { z } from 'zod'; import type { AztecAddress } from '../aztec-address/index.js'; import { type BlockData, BlockDataSchema } from '../block/block_data.js'; import { BlockHash } from '../block/block_hash.js'; -import { type BlockParameter, BlockParameterSchema } from '../block/block_parameter.js'; +import { type BlockParameter, BlockParameterSchema, BlockTagWithoutLatestSchema } from '../block/block_parameter.js'; import { type DataInBlock, dataInBlockSchemaFor } from '../block/in_block.js'; -import { type CheckpointsQuery, CheckpointsQuerySchema } from '../block/l2_block_source.js'; +import { + type CheckpointsQuery, + CheckpointsQuerySchema, + type L2BlockTag, + type L2Tips, + L2TipsSchema, +} from '../block/l2_block_source.js'; import { type CheckpointData, CheckpointDataSchema } from '../checkpoint/checkpoint_data.js'; import { type ContractClassPublic, @@ -80,7 +86,7 @@ import { type BlocksIncludeOptions, BlocksIncludeOptionsSchema, } from './block_response.js'; -import { type ChainTip, ChainTipSchema, type ChainTips, ChainTipsSchema } from './chain_tips.js'; +import { type CheckpointTag, CheckpointTagSchema } from './chain_tips.js'; import { type CheckpointParameter, CheckpointParameterSchema } from './checkpoint_parameter.js'; import { type CheckpointIncludeOptions, @@ -232,23 +238,20 @@ export interface AztecNode { ): Promise; /** - * Returns the block number at a given chain tip, or the latest proposed block number when + * Returns the block number at a given block tag, or the latest proposed block number when * `tip` is omitted. */ - getBlockNumber(tip?: ChainTip): Promise; + getBlockNumber(tip?: L2BlockTag): Promise; /** - * Returns the checkpoint number at a given chain tip, or the latest checkpoint number when - * `tip` is omitted. - * - * @remarks **Semantic foot-gun**: block-side `'proposed'` means "latest proposed block" (chain - * head), but checkpoint-side `'proposed'` means "latest confirmed checkpoint" — pre-L1-confirm - * checkpoints are not exposed over RPC. `'checkpointed'` on the checkpoint side is equivalent. + * Returns the checkpoint number at a given checkpoint tag, or the latest checkpointed number when + * `tip` is omitted. The proposed-but-unconfirmed checkpoint frontier is archiver-internal and not + * exposed over RPC, so `'proposed'` is not a valid checkpoint tag (see {@link CheckpointTag}). */ - getCheckpointNumber(tip?: ChainTip): Promise; + getCheckpointNumber(tip?: CheckpointTag): Promise; /** Returns the tips of the L2 chain. */ - getChainTips(): Promise; + getChainTips(): Promise; /** Returns the rollup constants for the current chain. */ getL1Constants(): Promise; @@ -622,11 +625,11 @@ export const AztecNodeApiSchema: ApiSchemaFor = { output: L2ToL1MembershipWitnessSchema.optional(), }), - getBlockNumber: z.function({ input: z.tuple([optional(ChainTipSchema)]), output: BlockNumberSchema }), + getBlockNumber: z.function({ input: z.tuple([optional(BlockTagWithoutLatestSchema)]), output: BlockNumberSchema }), - getCheckpointNumber: z.function({ input: z.tuple([optional(ChainTipSchema)]), output: CheckpointNumberSchema }), + getCheckpointNumber: z.function({ input: z.tuple([optional(CheckpointTagSchema)]), output: CheckpointNumberSchema }), - getChainTips: z.function({ input: z.tuple([]), output: ChainTipsSchema }), + getChainTips: z.function({ input: z.tuple([]), output: L2TipsSchema }), getL1Constants: z.function({ input: z.tuple([]), output: L1RollupConstantsSchema }), diff --git a/yarn-project/stdlib/src/interfaces/chain_tips.ts b/yarn-project/stdlib/src/interfaces/chain_tips.ts index 5836fea8747e..45ccb6f313c7 100644 --- a/yarn-project/stdlib/src/interfaces/chain_tips.ts +++ b/yarn-project/stdlib/src/interfaces/chain_tips.ts @@ -1,18 +1,16 @@ import { z } from 'zod'; -import { type L2BlockTag, type L2Tips, L2TipsSchema } from '../block/l2_block_source.js'; +/** + * Public checkpoint-tip selectors usable in RPC requests. + * + * `'proposed'` is intentionally excluded: the proposed-but-unconfirmed checkpoint frontier is an + * archiver-internal pipelining concept, not part of the public chain-tip surface. Select the + * proposed *block* tip with a block tag (`L2BlockTag`) instead. + */ +export type CheckpointTag = 'checkpointed' | 'proven' | 'finalized'; -/** Public chain-tip selectors usable in RPC requests. */ -export type ChainTip = L2BlockTag; - -export const ChainTipSchema = z.union([ - z.literal('proposed'), +export const CheckpointTagSchema = z.union([ z.literal('checkpointed'), z.literal('proven'), z.literal('finalized'), -]) satisfies z.ZodType; - -/** Tips of the L2 chain. */ -export type ChainTips = L2Tips; - -export const ChainTipsSchema = L2TipsSchema; +]) satisfies z.ZodType; diff --git a/yarn-project/stdlib/src/interfaces/checkpoint_parameter.ts b/yarn-project/stdlib/src/interfaces/checkpoint_parameter.ts index 0c50a436d753..c7bd38738818 100644 --- a/yarn-project/stdlib/src/interfaces/checkpoint_parameter.ts +++ b/yarn-project/stdlib/src/interfaces/checkpoint_parameter.ts @@ -2,18 +2,18 @@ import { CheckpointNumberSchema, SlotNumberSchema } from '@aztec/foundation/bran import { z } from 'zod'; -import { ChainTipSchema } from './chain_tips.js'; +import { CheckpointTagSchema } from './chain_tips.js'; /** * Selector for a checkpoint in RPC calls. * * Accepts a numeric checkpoint number (or `{ number }`), a slot number (`{ slot }`), - * or a chain-tip name (e.g. `'proposed'`, `'proven'`). + * or a checkpoint-tip name (e.g. `'checkpointed'`, `'proven'`, `'finalized'`). */ export const CheckpointParameterSchema = z.union([ z.object({ number: CheckpointNumberSchema }).strict(), z.object({ slot: SlotNumberSchema }).strict(), - ChainTipSchema, + CheckpointTagSchema, CheckpointNumberSchema, ]); From 8f92338fd06fb66b9c352e9068e0dac38b3e7ef6 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 11 Jun 2026 22:49:45 -0300 Subject: [PATCH 3/3] refactor: collapse getProposedCheckpoint into getProposedCheckpointData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getProposedCheckpoint() was redundant with getProposedCheckpointData(). A proposed checkpoint can only be stored with a number beyond the confirmed frontier (addProposedCheckpoint enforces sequentiality) and is deleted on confirmation (addCheckpoints / promoteProposedToCheckpointed), so getLastProposedCheckpoint — what getProposedCheckpointData() returns with no query — is always the leading entry. The leading-frontier filter was defensive-only and unreachable. Drop getProposedCheckpoint from the L2BlockSource interface, ArchiverApiSchema, BlockStore, data_source_base, and the mock. The sequencer checkSync, automine, and node simulatePublicCalls now read getProposedCheckpointData(); they already derive the proposed tip from the payload, so nothing else changes. --- .../archiver/src/archiver-sync.test.ts | 4 +-- .../archiver/src/modules/data_source_base.ts | 4 --- .../archiver/src/store/block_store.test.ts | 10 +++---- .../archiver/src/store/block_store.ts | 21 --------------- .../archiver/src/test/mock_l2_block_source.ts | 4 --- .../aztec-node/src/aztec-node/server.test.ts | 10 +++---- .../aztec-node/src/aztec-node/server.ts | 8 +++--- .../sequencer/automine/automine_sequencer.ts | 2 +- .../src/sequencer/sequencer.test.ts | 11 ++++---- .../src/sequencer/sequencer.ts | 8 +++--- .../stdlib/src/block/l2_block_source.ts | 14 +++------- .../stdlib/src/interfaces/archiver.test.ts | 26 ------------------- .../stdlib/src/interfaces/archiver.ts | 4 --- 13 files changed, 30 insertions(+), 96 deletions(-) diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index 2ac943fefdcd..f75422e67168 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -2077,7 +2077,7 @@ describe('Archiver Sync', () => { // Proposed checkpoint should lead the checkpointed tip const tips = await archiver.getL2Tips(); - const proposedCheckpointResult = await archiver.getProposedCheckpoint(); + const proposedCheckpointResult = await archiver.getProposedCheckpointData(); expect(proposedCheckpointResult).toBeDefined(); expect(proposedCheckpointResult!.checkpointNumber).toEqual(CheckpointNumber(2)); expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); @@ -2148,7 +2148,7 @@ describe('Archiver Sync', () => { // Proposed checkpoint should be cleared, so no proposed checkpoint leads the checkpointed tip expect(await archiverStore.blocks.getLastProposedCheckpoint()).toBeUndefined(); - expect(await archiver.getProposedCheckpoint()).toBeUndefined(); + expect(await archiver.getProposedCheckpointData()).toBeUndefined(); }, 15_000); }); diff --git a/yarn-project/archiver/src/modules/data_source_base.ts b/yarn-project/archiver/src/modules/data_source_base.ts index 3fd70ff8eeca..eb2beb5dedb4 100644 --- a/yarn-project/archiver/src/modules/data_source_base.ts +++ b/yarn-project/archiver/src/modules/data_source_base.ts @@ -288,10 +288,6 @@ export abstract class ArchiverDataSourceBase return this.stores.blocks.getProposedCheckpointBySlot(query.slot); } - public getProposedCheckpoint(): Promise { - return this.stores.blocks.getProposedCheckpoint(); - } - public getTxEffect(txHash: TxHash): Promise { return this.stores.blocks.getTxEffect(txHash); } diff --git a/yarn-project/archiver/src/store/block_store.test.ts b/yarn-project/archiver/src/store/block_store.test.ts index 98b2ce63bedc..31dbb3fe47a6 100644 --- a/yarn-project/archiver/src/store/block_store.test.ts +++ b/yarn-project/archiver/src/store/block_store.test.ts @@ -2625,16 +2625,16 @@ describe('BlockStore', () => { }); }); - describe('getProposedCheckpoint', () => { - it('returns undefined when no pending checkpoint leads the checkpointed frontier', async () => { - // Add checkpoint 1 with blocks 1-3 + describe('getLastProposedCheckpoint', () => { + it('returns undefined when there is no pending checkpoint', async () => { + // Add checkpoint 1 with blocks 1-3; confirmation deletes any matching proposed entry. const checkpoint1 = makePublishedCheckpoint( await Checkpoint.random(CheckpointNumber(1), { numBlocks: 3, startBlockNumber: 1 }), 10, ); await blockStore.addCheckpoints([checkpoint1]); - expect(await blockStore.getProposedCheckpoint()).toBeUndefined(); + expect(await blockStore.getLastProposedCheckpoint()).toBeUndefined(); }); it('returns the leading proposed checkpoint payload', async () => { @@ -2664,7 +2664,7 @@ describe('BlockStore', () => { feeAssetPriceModifier: 50n, }); - const proposedCheckpoint = await blockStore.getProposedCheckpoint(); + const proposedCheckpoint = await blockStore.getLastProposedCheckpoint(); expect(proposedCheckpoint).toBeDefined(); // Callers derive the tip from the payload: last block = startBlock + blockCount - 1. expect(proposedCheckpoint!.checkpointNumber).toBe(CheckpointNumber(2)); diff --git a/yarn-project/archiver/src/store/block_store.ts b/yarn-project/archiver/src/store/block_store.ts index ea6ceddc5a90..c1615ef601e5 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -864,27 +864,6 @@ export class BlockStore { return undefined; } - /** - * Returns the payload of the latest proposed checkpoint that leads the checkpointed frontier, in - * a single read-only transaction so the leading-frontier check and the payload are a coherent - * snapshot. Returns undefined when no proposed checkpoint exists beyond the latest confirmed - * checkpoint. Callers derive the proposed tip from the payload (last block is - * `startBlock + blockCount - 1`). - */ - async getProposedCheckpoint(): Promise { - return await this.db.transactionAsync(async () => { - const [entry] = await toArray(this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 })); - if (entry === undefined) { - return undefined; - } - const latestCheckpointNumber = await this.getLatestCheckpointNumber(); - if (entry[0] <= latestCheckpointNumber) { - return undefined; - } - return this.convertToProposedCheckpointData(entry[1]); - }); - } - /** * Evicts all pending checkpoints with checkpoint number >= fromNumber. * Used for divergent-mined-checkpoint cleanup: when L1 mines checkpoint N with a different archive, 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 39b907b011db..a9cee47f922f 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -593,10 +593,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { return Promise.resolve(undefined); } - getProposedCheckpoint(): Promise { - return Promise.resolve(undefined); - } - /** Returns checkpoints whose slot falls within the given epoch. */ private getCheckpointsInEpoch(epochNumber: EpochNumber): Checkpoint[] { const epochDuration = DefaultL1ContractsConfig.aztecEpochDuration; diff --git a/yarn-project/aztec-node/src/aztec-node/server.test.ts b/yarn-project/aztec-node/src/aztec-node/server.test.ts index a797ac7c4e32..24d6df5b158f 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -745,7 +745,7 @@ describe('aztec node', () => { const proposedCheckpointBlockNumber = BlockNumber(9); const targetSlot = SlotNumber(10); l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: proposedCheckpointBlockNumber })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( + l2BlockSource.getProposedCheckpointData.mockResolvedValue( makeProposedCheckpoint({ checkpointNumber, blockNumber: proposedCheckpointBlockNumber, @@ -780,7 +780,7 @@ describe('aztec node', () => { const proposedCheckpointBlockNumber = BlockNumber(9); const targetSlot = SlotNumber(12); l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: proposedCheckpointBlockNumber })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( + l2BlockSource.getProposedCheckpointData.mockResolvedValue( makeProposedCheckpoint({ checkpointNumber, blockNumber: proposedCheckpointBlockNumber, @@ -815,7 +815,7 @@ describe('aztec node', () => { const latestProposedBlockNumber = BlockNumber(12); const targetSlot = SlotNumber(12); l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: latestProposedBlockNumber })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( + l2BlockSource.getProposedCheckpointData.mockResolvedValue( makeProposedCheckpoint({ checkpointNumber, blockNumber: proposedCheckpointBlockNumber, @@ -855,7 +855,7 @@ describe('aztec node', () => { const latestProposedBlockNumber = BlockNumber(12); const targetSlot = SlotNumber(13); l2BlockSource.getL2Tips.mockResolvedValue(makeTips({ proposed: latestProposedBlockNumber })); - l2BlockSource.getProposedCheckpoint.mockResolvedValue( + l2BlockSource.getProposedCheckpointData.mockResolvedValue( makeProposedCheckpoint({ checkpointNumber, blockNumber: proposedCheckpointBlockNumber, @@ -896,7 +896,7 @@ describe('aztec node', () => { l2BlockSource.getL2Tips.mockResolvedValue( makeTips({ proposed: proposedCheckpointBlockNumber, checkpointed: checkpointNumber }), ); - l2BlockSource.getProposedCheckpoint.mockResolvedValue(undefined); + l2BlockSource.getProposedCheckpointData.mockResolvedValue(undefined); l2BlockSource.getBlockData.mockResolvedValue( makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(0), checkpointNumber), ); diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 3680293d3abc..ae752673bc0d 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1636,10 +1636,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const coinbase = EthAddress.ZERO; const feeRecipient = AztecAddress.ZERO; - // Resolve the proposed-checkpoint frontier (latest proposed checkpoint that leads the checkpointed - // tip, falling back to the checkpointed tip when none leads it). The leading case carries its header - // slot in the payload, so no extra block fetch is needed to derive the slot. - const proposedCheckpoint = await this.blockSource.getProposedCheckpoint(); + // Resolve the proposed-checkpoint frontier (latest proposed checkpoint, which leads the + // checkpointed tip, falling back to the checkpointed tip when none exists). The proposed payload + // carries its header slot, so no extra block fetch is needed to derive the slot. + const proposedCheckpoint = await this.blockSource.getProposedCheckpointData(); const proposedCheckpointBlockNumber = proposedCheckpoint ? BlockNumber(proposedCheckpoint.startBlock + proposedCheckpoint.blockCount - 1) : l2Tips.checkpointed.block.number; diff --git a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts index c8d790387fd1..c50eeddc6562 100644 --- a/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/automine/automine_sequencer.ts @@ -411,7 +411,7 @@ export class AutomineSequencer { const [tips, proposedCheckpoint] = await Promise.all([ this.deps.l2BlockSource.getL2Tips(), - this.deps.l2BlockSource.getProposedCheckpoint(), + this.deps.l2BlockSource.getProposedCheckpointData(), ]); const syncedToBlockNumber = tips.proposed.number; diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index 1c77c3a3247a..2e6b5d04c067 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -333,7 +333,6 @@ describe('sequencer', () => { getCheckpointsData: mockFn().mockResolvedValue([]), getSyncedL2SlotNumber: mockFn().mockResolvedValue(SlotNumber(Number.MAX_SAFE_INTEGER)), getProposedCheckpointData: mockFn().mockResolvedValue(undefined), - getProposedCheckpoint: mockFn().mockResolvedValue(undefined), }); l1ToL2MessageSource = mock({ @@ -1305,7 +1304,7 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpoint.mockResolvedValue({ + l2BlockSource.getProposedCheckpointData.mockResolvedValue({ checkpointNumber: CheckpointNumber(1), header: CheckpointHeader.empty(), archive: AppendOnlyTreeSnapshot.empty(), @@ -1368,7 +1367,7 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(3), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpoint.mockResolvedValue({ checkpointNumber: CheckpointNumber(2) } as any); + l2BlockSource.getProposedCheckpointData.mockResolvedValue({ checkpointNumber: CheckpointNumber(2) } as any); await sequencer.work(); @@ -1430,7 +1429,7 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(1), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpoint.mockResolvedValue({ + l2BlockSource.getProposedCheckpointData.mockResolvedValue({ checkpointNumber: CheckpointNumber(1), header: CheckpointHeader.empty(), archive: AppendOnlyTreeSnapshot.empty(), @@ -1475,7 +1474,7 @@ describe('sequencer', () => { // Mocks all sync sources so checkSync passes its earlier equality checks and reaches the orphan // guard, with the world-state tip at `blockNumber` (in `blockCheckpointNumber`) while the // checkpointed tip sits at `checkpointedCheckpointNumber`. The leading proposed checkpoint (if any) - // is supplied as the single atomic `getProposedCheckpoint` snapshot. + // is supplied via `getProposedCheckpointData`. const setupSyncedToBlock = (opts: { blockNumber: BlockNumber; blockSlot: SlotNumber; @@ -1522,7 +1521,7 @@ describe('sequencer', () => { checkpointNumber: opts.blockCheckpointNumber, indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpoint.mockResolvedValue(opts.proposedCheckpoint); + l2BlockSource.getProposedCheckpointData.mockResolvedValue(opts.proposedCheckpoint); }; it('returns undefined and logs debug while waiting for a matching proposed checkpoint', async () => { diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 6d2bf9914b9d..ba58e27a2893 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -809,7 +809,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter p2p.syncedToL2Block), this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })), this.l2BlockSource.getPendingChainValidationStatus(), - this.l2BlockSource.getProposedCheckpoint(), + this.l2BlockSource.getProposedCheckpointData(), ] as const); const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] = @@ -850,9 +850,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter l2Tips.checkpointed.checkpoint.number && proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber diff --git a/yarn-project/stdlib/src/block/l2_block_source.ts b/yarn-project/stdlib/src/block/l2_block_source.ts index bd04d8270658..34089220669c 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -241,21 +241,15 @@ export interface L2BlockSource { /** * Looks up a proposed (archiver-internal, not-yet-L1-confirmed) checkpoint. - * Returns the latest proposed entry when called with no args or `{ tag: 'proposed' }`. + * Returns the latest proposed entry when called with no args or `{ tag: 'proposed' }`; since a + * proposed entry can only be stored with a checkpoint number beyond the confirmed frontier (and is + * deleted on confirmation), the latest entry is always the leading one. Callers derive the proposed + * tip from the payload (checkpoint number, and last block `startBlock + blockCount - 1`). * With `{ number }` or `{ slot }`, returns the matching entry or undefined. * Never falls back to confirmed checkpoints. */ getProposedCheckpointData(query?: ProposedCheckpointQuery): Promise; - /** - * Returns the payload of the latest proposed (not-yet-L1-confirmed) checkpoint that leads the - * checkpointed frontier, in a single atomic store read. Returns `undefined` when no proposed - * checkpoint exists beyond the checkpointed tip. Callers derive the proposed tip from the payload - * (checkpoint number, and last block `startBlock + blockCount - 1`); unlike - * {@link getProposedCheckpointData}, this applies the leading-frontier filter. - */ - getProposedCheckpoint(): Promise; - /** Force a sync. */ syncImmediate(): Promise; diff --git a/yarn-project/stdlib/src/interfaces/archiver.test.ts b/yarn-project/stdlib/src/interfaces/archiver.test.ts index 467125bcd991..c1e2631cfad6 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -257,20 +257,6 @@ describe('ArchiverApiSchema', () => { }); }); - it('getProposedCheckpoint', async () => { - const result = await context.client.getProposedCheckpoint(); - expect(result).toEqual({ - checkpointNumber: 1, - header: expect.any(CheckpointHeader), - archive: expect.any(AppendOnlyTreeSnapshot), - checkpointOutHash: expect.any(Fr), - blockCount: 1, - startBlock: 1, - totalManaUsed: 1n, - feeAssetPriceModifier: 1n, - }); - }); - it('getPendingChainValidationStatus', async () => { const result = await context.client.getPendingChainValidationStatus(); expect(result).toEqual({ valid: true }); @@ -391,18 +377,6 @@ class MockArchiver implements ArchiverApi { feeAssetPriceModifier: 1n, }); } - getProposedCheckpoint(): Promise { - return Promise.resolve({ - checkpointNumber: CheckpointNumber(1), - header: CheckpointHeader.random(), - archive: AppendOnlyTreeSnapshot.random(), - checkpointOutHash: Fr.random(), - blockCount: 1, - startBlock: BlockNumber(1), - totalManaUsed: 1n, - feeAssetPriceModifier: 1n, - }); - } syncImmediate() { return Promise.resolve(); } diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index 6cd56b62a600..2b9ec63db52f 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -141,10 +141,6 @@ export const ArchiverApiSchema: ApiSchemaFor = { input: z.tuple([optional(ProposedCheckpointQuerySchema)]), output: ProposedCheckpointDataSchema.optional(), }), - getProposedCheckpoint: z.function({ - input: z.tuple([]), - output: ProposedCheckpointDataSchema.optional(), - }), syncImmediate: z.function({ input: z.tuple([]), output: z.void() }), isPendingChainInvalid: z.function({ input: z.tuple([]), output: z.boolean() }), getPendingChainValidationStatus: z.function({ input: z.tuple([]), output: ValidateCheckpointResultSchema }),