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..f75422e67168 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -2075,11 +2075,15 @@ 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.getProposedCheckpointData(); + expect(proposedCheckpointResult).toBeDefined(); + expect(proposedCheckpointResult!.checkpointNumber).toEqual(CheckpointNumber(2)); expect(tips.checkpointed.checkpoint.number).toEqual(CheckpointNumber(1)); - expect(tips.proposedCheckpoint.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 () => { @@ -2142,11 +2146,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.getProposedCheckpointData()).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/store/block_store.test.ts b/yarn-project/archiver/src/store/block_store.test.ts index d7b3eb65d62e..31dbb3fe47a6 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,26 +2625,19 @@ describe('BlockStore', () => { }); }); - describe('L2TipsCache proposedCheckpoint', () => { - it('returns proposedCheckpoint equal to checkpointed when no pending exists', 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]); - 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.getLastProposedCheckpoint()).toBeUndefined(); }); - it('returns proposedCheckpoint ahead of checkpointed when pending is set', async () => { + it('returns the leading proposed checkpoint payload', async () => { // Add checkpoint 1 const checkpoint1 = makePublishedCheckpoint( await Checkpoint.random(CheckpointNumber(1), { numBlocks: 1, startBlockNumber: 1 }), @@ -2663,21 +2654,23 @@ 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.getLastProposedCheckpoint(); + expect(proposedCheckpoint).toBeDefined(); + // 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 1901119016c4..c1615ef601e5 100644 --- a/yarn-project/archiver/src/store/block_store.ts +++ b/yarn-project/archiver/src/store/block_store.ts @@ -1167,14 +1167,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 +1196,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 +1281,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 +1292,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 +1303,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..a9cee47f922f 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -55,7 +55,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 +163,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 +179,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 +245,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 +359,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 +397,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 +415,6 @@ export class MockL2BlockSource implements L2BlockSource, ContractDataSource { checkpointed: makeTipId(checkpointedBlockId), proven: makeTipId(provenBlockId), finalized: makeTipId(finalizedBlockId), - proposedCheckpoint: makeTipId(proposedCheckpointBlockId), }; } 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 8d84ab641270..24d6df5b158f 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.test.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.test.ts @@ -744,16 +744,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.getProposedCheckpointData.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 +765,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 +779,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.getProposedCheckpointData.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber, + blockNumber: proposedCheckpointBlockNumber, + slotNumber: SlotNumber(9), }), ); - l2BlockSource.getBlockData.mockResolvedValue( - makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(9), checkpointNumber), - ); mockNextL1Slot(targetSlot); globalVariablesBuilder.buildCheckpointGlobalVariables.mockResolvedValue({ chainId, @@ -804,7 +800,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 +814,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.getProposedCheckpointData.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 +838,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 +854,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.getProposedCheckpointData.mockResolvedValue( + makeProposedCheckpoint({ + checkpointNumber, + blockNumber: proposedCheckpointBlockNumber, + slotNumber: SlotNumber(9), }), ); l2BlockSource.getBlockData.mockResolvedValue(undefined); @@ -887,7 +876,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 +891,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.getProposedCheckpointData.mockResolvedValue(undefined); l2BlockSource.getBlockData.mockResolvedValue( makeSimulationBlockData(proposedCheckpointBlockNumber, SlotNumber(0), checkpointNumber), ); @@ -1405,9 +1393,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 +1405,30 @@ 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 the payload of the atomic leading-proposed-checkpoint read (last block = startBlock). */ + function makeProposedCheckpoint(args: { + checkpointNumber: CheckpointNumber; + blockNumber: BlockNumber; + slotNumber: SlotNumber; + }): ProposedCheckpointData { + return { + 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( @@ -1459,26 +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({ proposedCheckpoint: CheckpointNumber(5) })); - 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({ proposedCheckpoint: CheckpointNumber(5) })); - 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( @@ -1503,30 +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({ proposedCheckpoint: CheckpointNumber(2) })); - 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 entry exists, the proposedCheckpoint tip falls back to the confirmed tip. - l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ proposedCheckpoint: CheckpointNumber(3), checkpointed: CheckpointNumber(3) }), - ); - 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); @@ -1636,22 +1596,23 @@ describe('aztec node', () => { }); describe('getCheckpointNumber', () => { - it('returns the proposed checkpoint number from proposedCheckpoint tip', async () => { + beforeEach(() => { l2BlockSource.getL2Tips.mockResolvedValue( - makeTips({ proposedCheckpoint: CheckpointNumber(7), checkpointed: CheckpointNumber(5) }), + 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('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('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)); }); }); @@ -1691,7 +1652,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..ae752673bc0d 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,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() { @@ -275,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 tips.proposedCheckpoint.checkpoint.number; case 'proven': return tips.proven.checkpoint.number; case 'finalized': @@ -297,8 +294,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb } } - 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'; } /** @@ -346,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, @@ -357,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: tips.proposedCheckpoint.checkpoint.number }; case 'checkpointed': return { number: tips.checkpointed.checkpoint.number }; case 'proven': @@ -1641,19 +1636,27 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb const coinbase = EthAddress.ZERO; const feeRecipient = AztecAddress.ZERO; + // 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; + 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 proposedCheckpointBlockData = await this.blockSource.getBlockData({ - number: l2Tips.proposedCheckpoint.block.number, - }); - const proposedCheckpointSlot = proposedCheckpointBlockData?.header.getSlot(); + const proposedCheckpointSlot = + proposedCheckpoint?.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 +1693,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 +1717,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_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/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..c50eeddc6562 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.getProposedCheckpointData(), + ]); 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?.checkpointNumber ?? 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..2e6b5d04c067 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -314,10 +314,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() }, @@ -343,10 +339,6 @@ describe('sequencer', () => { 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,9 +1265,8 @@ 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({ state: WorldStateRunningState.IDLE, syncSummary: { @@ -1288,10 +1279,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() }, @@ -1326,7 +1313,7 @@ describe('sequencer', () => { blockCount: 1, totalManaUsed: 0n, feeAssetPriceModifier: 0n, - } satisfies ProposedCheckpointData); + }); await sequencer.work(); @@ -1342,7 +1329,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, @@ -1356,10 +1342,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,9 +1367,7 @@ describe('sequencer', () => { checkpointNumber: CheckpointNumber(3), indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpointData.mockResolvedValue({ - checkpointNumber: CheckpointNumber(2), - } as any); + l2BlockSource.getProposedCheckpointData.mockResolvedValue({ checkpointNumber: CheckpointNumber(2) } as any); await sequencer.work(); @@ -1412,7 +1392,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: { @@ -1425,10 +1404,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() }, @@ -1463,7 +1438,7 @@ describe('sequencer', () => { blockCount: 1, totalManaUsed: 0n, feeAssetPriceModifier: 0n, - } satisfies ProposedCheckpointData); + }); await sequencer.work(); @@ -1498,18 +1473,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 via `getProposedCheckpointData`. const setupSyncedToBlock = (opts: { blockNumber: BlockNumber; blockSlot: SlotNumber; blockCheckpointNumber: CheckpointNumber; checkpointedCheckpointNumber: CheckpointNumber; - proposedCheckpointTipNumber: CheckpointNumber; - proposedCheckpointData: ProposedCheckpointData | undefined; + proposedCheckpoint: ProposedCheckpointData | 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 +1496,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 +1521,19 @@ describe('sequencer', () => { checkpointNumber: opts.blockCheckpointNumber, indexWithinCheckpoint: IndexWithinCheckpoint(0), } satisfies BlockData); - l2BlockSource.getProposedCheckpointData.mockResolvedValue(opts.proposedCheckpointData); + l2BlockSource.getProposedCheckpointData.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 +1547,7 @@ describe('sequencer', () => { expect.objectContaining({ blockCheckpointNumber: CheckpointNumber(3), checkpointedCheckpointNumber: CheckpointNumber(2), - proposedCheckpointTipNumber: CheckpointNumber(2), - proposedCheckpointDataNumber: undefined, + proposedCheckpointTipNumber: undefined, }), ); }); @@ -1590,8 +1558,7 @@ describe('sequencer', () => { blockSlot: SlotNumber(3), blockCheckpointNumber: CheckpointNumber(3), checkpointedCheckpointNumber: CheckpointNumber(2), - proposedCheckpointTipNumber: CheckpointNumber(3), - proposedCheckpointData: { + proposedCheckpoint: { checkpointNumber: CheckpointNumber(3), header: CheckpointHeader.empty(), archive: AppendOnlyTreeSnapshot.empty(), @@ -1600,7 +1567,7 @@ describe('sequencer', () => { blockCount: 1, totalManaUsed: 0n, feeAssetPriceModifier: 0n, - } satisfies ProposedCheckpointData, + }, }); 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..ba58e27a2893 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -805,9 +805,7 @@ 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(), @@ -852,16 +850,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter l2Tips.checkpointed.checkpoint.number && - (l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber || - proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber) + proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber ) { const logCtx = { blockCheckpointNumber: blockData.checkpointNumber, checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number, - proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number, - proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber, + proposedCheckpointTipNumber: proposedCheckpointData?.checkpointNumber, 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 = proposedCheckpointData !== 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..34089220669c 100644 --- a/yarn-project/stdlib/src/block/l2_block_source.ts +++ b/yarn-project/stdlib/src/block/l2_block_source.ts @@ -241,7 +241,10 @@ 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. */ @@ -327,36 +330,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(); @@ -398,7 +394,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..c1e2631cfad6 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.test.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.test.ts @@ -153,7 +153,6 @@ describe('ArchiverApiSchema', () => { expect(result).toEqual({ proposed: { number: 1, hash: `0x01` }, checkpointed: expectedTipId, - proposedCheckpoint: expectedTipId, proven: expectedTipId, finalized: expectedTipId, }); @@ -477,7 +476,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/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 fde42b09a136..45ccb6f313c7 100644 --- a/yarn-project/stdlib/src/interfaces/chain_tips.ts +++ b/yarn-project/stdlib/src/interfaces/chain_tips.ts @@ -1,24 +1,16 @@ 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}. + * 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 ChainTip = Exclude; +export type CheckpointTag = 'checkpointed' | 'proven' | 'finalized'; -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. - * Omits the sequencer-internal `proposedCheckpoint` from the public RPC surface. - */ -export type ChainTips = Omit; - -export const ChainTipsSchema = L2TipsSchema.omit({ proposedCheckpoint: true }); +]) 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, ]); 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, }; }