Skip to content

Phase3 trusted block root - #9917

Open
aarishnaiyer wants to merge 13 commits into
sigp:unstablefrom
aarishnaiyer:phase3-trusted-block-root
Open

aarishnaiyer wants to merge 13 commits into
sigp:unstablefrom
aarishnaiyer:phase3-trusted-block-root

Conversation

@aarishnaiyer

Copy link
Copy Markdown

Adds a checkpoint-sync-url-block-root flag to Lighthouse's checkpoint sync. When provided alongside --checkpoint-sync-url, the downloaded checkpoint block's canonical root is verified against the trusted block root before the node initializes from it. If they don't match, the node refuses to start and logs an error.

This is the first step toward decentralized checkpoint sync in Lighthouse. Currently, checkpoint sync downloads a BeaconState and block from a single trusted URL with no verification. A user who knows the trusted block root out-of-band (from a friend, a block explorer, or their own LC sync) can now pass it via this flag to ensure the checkpoint provider is serving the correct chain.

The flag requires --checkpoint-sync-url to be set. The block root must be 0x-prefixed hex.

This is Phase 3 of the Decentralized CL Checkpoint Sync EPF project.

@aarishnaiyer
aarishnaiyer changed the base branch from stable to unstable August 25, 2026 23:19
@michaelsproul

Copy link
Copy Markdown
Member

Maybe we could combine this/alias this with the --wss-checkpoint flag?

@aarishnaiyer

aarishnaiyer commented Aug 26, 2026 •

Copy link
Copy Markdown
Author

Maybe we could combine this/alias this with the --wss-checkpoint flag?

Thanks for the suggestion! The main difference is that --wss-checkpoint is used for ongoing verification after sync while this flag is for verifying the downloaded state before initializing. But you're right that they overlap. Would it make sense to reuse --wss-checkpoint's block root to also verify the checkpoint download when --checkpoint-sync-url is set?

@aarishnaiyer

Copy link
Copy Markdown
Author

updated to reuse --wss-checkpoint's block root for this verification. removed the separate flag

@aarishnaiyer
aarishnaiyer force-pushed the phase3-trusted-block-root branch from 7e970e9 to a6c6b28 Compare September 7, 2026 20:55
@aarishnaiyer

Copy link
Copy Markdown
Author

Hey Michael, working on the BeaconStatePartsByRange endpoint for Phase 3 of the decentralized checkpoint sync project. To build ListSummary and BeaconStatePart from a live BeaconState I need the raw items root of each list before length gets mixed in (generalized index 2). Checked milhouse's public API and there's no accessor for it yet. Would a small public getter be the right approach, or is there another way to get at it? Also noticed PR #92 on milhouse which looks related.

@aarishnaiyer
aarishnaiyer marked this pull request as ready for review October 6, 2026 23:22
The previous uniform items-per-chunk stride didn't produce valid
single-subtree Merkle proofs against the actual progressive-list tree
structure, since levels start at non-round cumulative offsets (0, 1,
5, 21, 85, ...). As per Etan's message: the first chunk folds together
every level that fits completely within items_per_chunk, and every
chunk after that is a plain round items_per_chunk-sized slice, since
each later level is an exact power-of-two multiple of items_per_chunk.

Also fixes ChunkLayout::locate to clip the final chunk of a field to
its actual length, which the old uniform-stride version never did.

@etan-status etan-status left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Given that you have a chunking implementation, would you mind measuring the size of the snapshot on a real mainnet BeaconState?

Depending on the size, it might be worth splitting up the summary some more if it's big (i.e., update to https://hackmd.io/@etan-status/decentralized-cl-sync based on the measurements).

// If --wss-checkpoint was provided, verify the downloaded block root matches it
// before initializing the chain from the checkpoint state.
if let Some(wss_checkpoint) = config.chain.weak_subjectivity_checkpoint {
if block_root != wss_checkpoint.root {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hmm, given that --wss-checkpoint was already used before, is this compatible with its earlier use?
e.g., if the user passes a checkpoint that's slightly older than the freshest one, would it still work?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Also, in PR description --checkpoint-sync-url is mentioned, is that still applicable?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No Im still using --wss-checkpoint as the source of the root, that part’s unchanged. The gap is in how I compare it: so my check does flat equality against the freshest downloaded block, so it would reject a checkpoint that’s slightly older than the tip, something the existing verify_weak_subjectivity_checkpoint already tolerates by walking backward through block roots when the checkpoint’s epoch is older than the current finalized one. I’ll rework the comparison to match that existing backward-walk behavior instead of a flat equality check.

Comment on lines +455 to +456
// If --wss-checkpoint was provided, verify the downloaded block root matches it
// before initializing the chain from the checkpoint state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The checkpoint state also has to be validated. i.e., have to verify that the block header's state_root matches the state's root, and that both of them are referring to the same slot / block.

Given that you only add new logic here, I wonder if this is a problem related to the PR, or already there before.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah thats checked, this predates the PR. The state and block are fetched independently a few lines above with no cross-check between them already there. My change only adds the root-vs---wss-checkpoint comparison after that, I didn’t touch the fetch logic itself. It's definitely worth fixing, but it’s a separate, existing gap, not something I introduced. Happy to pick it up as a follow-up, or fold it into this PR if you’d rather bundle it.

Comment on lines +128 to +131
/// The fold itself is computed in leaf units (dividing out [`Self::packing_factor`]) and
/// converted back to items at the end, since the real tree's levels are leaf-sized, not
/// item-sized (see [`Self::packing_factor`]'s docs). For unpacked fields (`packing_factor ==
/// 1`) this is a no-op and matches the original item-unit fold exactly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yep! multiple leaves can get packed into chunks, so the packing factor is important.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants