Skip to content

Conversation

@PhilWindle
Copy link
Collaborator

@PhilWindle PhilWindle commented Dec 16, 2025

This PR focuses on 2 main things.

Firtsly updating the L2Tips definition and plumbing that through to all components that report chain tips and/or depend on components that report chain tips. The definition of L2Tips now looks like:

/** Tips of the L2 chain. */
export type L2Tips = {
  proposed: L2BlockId;
  checkpointed: L2TipId;
  proven: L2TipId;
  finalized: L2TipId;
};

/** Identifies a block by number and hash. */
export type L2BlockId = { number: BlockNumber; hash: string };

export type CheckpointId = { number: CheckpointNumber; hash: string };

export type L2TipId = { block: L2BlockId; checkpoint: CheckpointId };

This impacts components such as Archiver, P2PClient, WorldState, Sequencer and the Sentinel.

Secondly the L2BlockStream has been updated to track and emit events for the checkpointed chain.

@AztecBot
Copy link
Collaborator

AztecBot commented Dec 17, 2025

Flakey Tests

🤖 says: This CI run detected 4 tests that failed, but were tolerated due to a .test_patterns.yml entry.

\033FLAKED\033 (\0338;;http://ci.aztec-labs.com/c561df09279b4aa3�c561df09279b4aa38;;�\033):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_l1_publisher/e2e_l1_publisher.test.ts (100s) (code: 1) (\033Phil Windle\033: Lint fix)
\033FLAKED\033 (\0338;;http://ci.aztec-labs.com/3ac90ae2ad7dc7da�3ac90ae2ad7dc7da8;;�\033):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_epochs/epochs_high_tps_block_building.test.ts (285s) (code: 1) group:e2e-p2p-epoch-flakes (\033Phil Windle\033: Lint fix)
\033FLAKED\033 (\0338;;http://ci.aztec-labs.com/d286c5d848a1058a�d286c5d848a1058a8;;�\033):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_p2p/gossip_network.test.ts (477s) (code: 1) group:e2e-p2p-epoch-flakes (\033Phil Windle\033: Lint fix)
\033FLAKED\033 (\0338;;http://ci.aztec-labs.com/3352d347d3140036�3352d347d31400368;;�\033):  yarn-project/end-to-end/scripts/run_test.sh simple src/e2e_p2p/add_rollup.test.ts (601s) (code: 124) group:e2e-p2p-epoch-flakes (\033Phil Windle\033: Lint fix)

Copy link
Contributor

@spalladino spalladino left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch to the new models across all components is great, but I'd want to discuss more the change in the getTips interface.

The design I had in mind was to just add another chain tip, also tied to block numbers. So, instead of having latest, proven, finalized block number/hash, we'd have proposed, checkpointed, proven, finalized (again, all block numbers, not checkpoint numbers).

This'd mean that the block stream would still emit block-added whenever a new block was seen, and we'd get a new chain-checkpointed event (similar to chain-proven or chain-finalized) with the block number (not checkpoint number) that was checkpointed.

Adding a checkpoint number directly to L2Tips feels a bit weird to me, since we're not making it clear whether it's the latest checkpointed checkpoint or the latest proven checkpoint. And IIUC that checkpoint number is only needed in the sentinel, though it could be removed by fetching the latest checkpointed block reported by the chain-checkpointed event, then getting the block's checkpoint number, and then fetching the checkpoint.

That said, perhaps a shape for L2 tips that mixes both designs (as in the one I had in mind and the one in this PR) could be something like this? It should make things easier for any downstream component that depends on either checkpoints or blocks.

BlockId = { blockNumber, blockHash, checkpointNumber }

L2Tips = {
  proposed: Omit<BlockId, checkpointNumber>,
  checkpointed: BlockId,
  proven: BlockId,
  finalized: BlockId,
}

WDYT?

Comment on lines 586 to 587
// TODO(pw/mbps): Don't convert to legacy blocks here
const blocks: L2Block[] = (await Promise.all(newBlocks.map(x => this.getBlock(x.number)))).filter(isDefined);
//const blocks: L2Block[] = (await Promise.all(newBlocks.map(x => this.getBlock(x.number)))).filter(isDefined);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete for good :-)

if (event.type !== 'checkpoint-added') {
return;
}
const checkpointNumber = CheckpointNumber(event.checkpoint.number);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should type the CheckpointNumber in the event itself

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already is. That's an error.

return;
}
const checkpointNumber = CheckpointNumber(event.checkpoint.number);
const [checkpoint] = await this.archiver.getPublishedCheckpoints(checkpointNumber, 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to go back to the archiver to fetch the checkpoint? Can't we use the event.checkpoint directly? Or is it missing the Published info? If so, can we add it to the event instead, given all checkpoints are Published?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, the L2BlockStream works in 2 different ways. For 'blocks-added' it emits the full blocks, for other events it just emits L2BlockIds. Currently, with checkpoints, it is just emitting CheckpointIds. But yes, this could change to retrieve the checkpoint and emit the whole thing.

@PhilWindle
Copy link
Collaborator Author

The switch to the new models across all components is great, but I'd want to discuss more the change in the getTips interface.

The design I had in mind was to just add another chain tip, also tied to block numbers. So, instead of having latest, proven, finalized block number/hash, we'd have proposed, checkpointed, proven, finalized (again, all block numbers, not checkpoint numbers).

This'd mean that the block stream would still emit block-added whenever a new block was seen, and we'd get a new chain-checkpointed event (similar to chain-proven or chain-finalized) with the block number (not checkpoint number) that was checkpointed.

Adding a checkpoint number directly to L2Tips feels a bit weird to me, since we're not making it clear whether it's the latest checkpointed checkpoint or the latest proven checkpoint. And IIUC that checkpoint number is only needed in the sentinel, though it could be removed by fetching the latest checkpointed block reported by the chain-checkpointed event, then getting the block's checkpoint number, and then fetching the checkpoint.

That said, perhaps a shape for L2 tips that mixes both designs (as in the one I had in mind and the one in this PR) could be something like this? It should make things easier for any downstream component that depends on either checkpoints or blocks.

BlockId = { blockNumber, blockHash, checkpointNumber }

L2Tips = {
  proposed: Omit<BlockId, checkpointNumber>,
  checkpointed: BlockId,
  proven: BlockId,
  finalized: BlockId,
}

WDYT?

I actually started down this exact route of simply adding checkpointed: BlockId (without checkpoint number). But I was worried it would be brittle for anyone using it to track the existence of new checkpoints. Essentially it requires a flow of:

  1. Checkpoint arrives.
  2. Derive the last block number and update L2Tips.
  3. Consumer sees the change in L2Tips and the new block number.
  4. Consumer retrieves block for checkpointed block number.
  5. Consumer reads checkpoint number from block.
  6. Consumer retrieves checkpoint for checkpoint number.

I was concerned how this would hold up under e.g. prune scenarios or historic syncing when the link between block number and checkpoint number can change. I figured tracking the CheckpointId (number and hash) like we do with blocks carried less ambiguity.

However, I admit I didn't consider the semantics of whether this is 'checkpointed' or 'proven' or what. Also it leads to questions such as, should the L2BlockStream now emit checkpoint-prune events?

If we were to adopt your alternative proposal, should the BlockId contain CheckpointId instead of checkpoint number?

@spalladino
Copy link
Contributor

Also it leads to questions such as, should the L2BlockStream now emit checkpoint-prune events?

I think we can stick to block-prune, and add checkpoint info as well as block info if it helps.

Alternatively, we could have separate block-prune and checkpoint-prune to differentiate between a prune caused by a provisional set of blocks that failed to be checkpointed, and a prune caused by a missed proof or an L1 reorg. Personally I prefer a single event with both block and checkpoint info, and a reason.

If we were to adopt your alternative proposal, should the BlockId contain CheckpointId instead of checkpoint number?

In addition to, not instead of. IMO it should contain block number and hash, and checkpoint number and hash. Though I can't remember if the checkpoint hash was the same as the hash of its last block, in which case having block and checkpoint hash would be redundant.

requestResponseHandlers[ReqRespSubProtocol.TX] = txHandler.bind(this);
}

// Define the sub protocol validators - This is done within this start() method to gain a callback to the existing validateTx function
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the change to p2p_client to use the tips store caused timing differences in the tests. Those timing differences highlighted something that I think flakes quite a lot and in this branch fails all the time.

We previously initialised all req/resp sub-protocols after starting libp2p. This means that for a short period the node is 'online' but not responsive to handshakes. Libp2p seems perfectly happy with us setting up these sub-protocols without having started the service, so that's what we do now, setup sub-protocols, then start Libp2p.

@PhilWindle PhilWindle closed this Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants