Skip to content

[new-protocol] seed parent proposal from anchor leaf so leader can propose after restart#4440

Merged
imabdulbasit merged 2 commits into
mainfrom
ab/fix-state-seeding-after-restart
Jun 5, 2026
Merged

[new-protocol] seed parent proposal from anchor leaf so leader can propose after restart#4440
imabdulbasit merged 2 commits into
mainfrom
ab/fix-state-seeding-after-restart

Conversation

@imabdulbasit
Copy link
Copy Markdown
Contributor

No description provided.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 5, 2026

Claude finished @imabdulbasit's task in 7m 6s —— View job


Code Review: PR #4440

  • Read repository guidelines (CLAUDE.md)
  • Read existing review comments
  • Analyze PR diff and changed files
  • Review code quality, correctness, and security
  • Post review findings

Summary

This PR generalizes seed_genesis into seed_parent, allowing the coordinator to seed consensus state from the anchor leaf (not just genesis) so a leader can propose after a non-genesis restart. The changes are well-structured: the Consensus method is cleanly generalized, the coordinator now constructs a faithful proposal from the anchor leaf's actual fields, the redundant double seed_state call is removed, and set_view is correctly added to advance beyond genesis.

Findings

1. View mismatch between cert1 and parent_proposal (important — see inline comment)

seed_parent stores both the cert and proposal at cert1.view_number(), but the proposal carries view_number = anchor_view. The HotShotInitializer comment says high_qc "should be for a view greater than inner's view number for the non genesis case." If cert1.view_number() > anchor_view, the commitment check in maybe_propose (consensus.rs:1201) will fail silently because proposal_commitment derives a leaf at anchor_view while cert1.data.leaf_commit commits to a leaf at cert1.view_number().

If there's an invariant in the new protocol that guarantees these are always equal, a debug_assert_eq!(cert1.view_number(), anchor_view) would be valuable documentation.

2. next_epoch_justify_qc always None (minor — see inline comment)

The ProposalQuorumProposalWrapper conversion hardcodes next_epoch_justify_qc: None, but this field contributes to the Leaf2 commitment. If the anchor leaf at an epoch boundary has a non-None value, proposal_commitment won't match cert1.data.leaf_commit. Likely not triggered in practice, but worth noting.

3. No test for the non-genesis restart path

The test changes are mechanical renames (seed_genesisseed_parent). Since the PR specifically fixes restart-after-genesis behavior, a test that seeds at a non-genesis view and verifies the leader can propose would increase confidence.

What looks good

  • Clean generalization of seed_genesisseed_parent — the method is now view-agnostic
  • Constructing the proposal from the anchor leaf's actual fields (justify_qc, upgrade_certificate, view_change_evidence, next_drb_result) instead of hardcoding them to None — this makes the seeded proposal faithful to what actually happened
  • Removing the redundant double seed_state call (the old workaround is no longer needed)
  • Adding set_view(anchor_view, anchor_epoch) — necessary for non-genesis restart

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Comment on lines +142 to +170
let cert1 = initializer.high_qc.clone();
let parent_proposal = message::Proposal {
block_header: anchor_leaf.block_header().clone(),
view_number: anchor_view,
epoch: anchor_epoch,
justify_qc: anchor_leaf.justify_qc(),
next_epoch_justify_qc: None,
upgrade_certificate: None,
view_change_evidence: None,
next_drb_result: None,
upgrade_certificate: anchor_leaf.upgrade_certificate(),
view_change_evidence: anchor_leaf
.view_change_evidence
.clone()
.and_then(|e| match e {
hotshot_types::data::ViewChangeEvidence2::Timeout(tc) => Some(tc),
hotshot_types::data::ViewChangeEvidence2::ViewSync(_) => None,
}),
next_drb_result: anchor_leaf.next_drb_result,
state_cert: None,
};

let mut state_manager = StateManager::new(
Arc::new(initializer.instance_state.clone()),
upgrade_lock.clone(),
);
state_manager.seed_state(
initializer.anchor_leaf.view_number(),
initializer.anchor_state.clone(),
initializer.anchor_leaf.clone(),
);
// The synthetic genesis proposal has a non-null justify_qc (the genesis
// cert1) so the leaf derived from it has a different commitment than
// the anchor leaf produced by `Leaf2::genesis`. `request_header` for
// view 1 looks up the parent state by the *proposal's* leaf
// commitment, so seed the same state under that commitment too.
state_manager.seed_state(
ViewNumber::genesis(),
anchor_view,
initializer.anchor_state.clone(),
Leaf2::from(genesis_proposal.clone()),
anchor_leaf.clone(),
);
consensus.seed_genesis(genesis_cert1, genesis_proposal);
consensus.seed_parent(cert1, parent_proposal);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question (correctness): seed_parent stores both the cert and proposal at cert1.view_number(), but the proposal's own view_number field is anchor_view. If cert1.view_number() != anchor_view — which can happen when high_qc is ahead of the anchor leaf (the HotShotInitializer comment says "It should be for a view greater than inner's view number for the non genesis case") — then the commitment check in maybe_propose (consensus.rs:1201) will fail:

} else if proposal_commitment(proposal) != parent_cert.data.leaf_commit {
    warn!("stored proposal at parent_view does not match parent cert's leaf_commit; refusing to propose with mismatched parent");
    return;

proposal_commitment derives a leaf from the proposal (which carries view_number = anchor_view), while cert1.data.leaf_commit is the commitment of whatever leaf cert1 actually certified (at cert1.view_number()). If these views differ, the commitments will differ and the leader silently refuses to propose.

Is there an invariant guaranteeing cert1.view_number() == anchor_view in the new protocol context? If so, a debug_assert or comment documenting this would help future readers. If not, this may need to be handled.

view_number: anchor_view,
epoch: anchor_epoch,
justify_qc: anchor_leaf.justify_qc(),
next_epoch_justify_qc: None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit (correctness at epoch boundaries): next_epoch_justify_qc is hardcoded to None here, and the Proposal -> QuorumProposalWrapper conversion also hardcodes it to None. Since next_epoch_justify_qc contributes to the Leaf2 commitment (when with_epoch = true, see data.rs:1755), this means proposal_commitment(parent_proposal) won't match anchor_leaf.commit() if the anchor_leaf has a non-None next_epoch_justify_qc.

This likely doesn't matter if the anchor_leaf is never at an epoch boundary when this code path runs, but worth keeping in mind.

);
consensus.seed_genesis(genesis_cert1, genesis_proposal);
consensus.seed_parent(cert1, parent_proposal);
consensus.set_view(anchor_view, anchor_epoch);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good addition — previously current_view was stuck at genesis after seed_genesis, which would prevent the node from proposing at the correct view after a non-genesis restart.

Comment on lines +275 to 286
/// Seed a parent certificate and proposal so the leader of the *next* view
/// can propose without any external bootstrap injection.
/// Sets the locked certificate and current epoch. After calling this, a
/// subsequent `apply` that triggers `maybe_propose` will find the
/// parent cert and proposal it needs.
pub fn seed_genesis(&mut self, genesis_cert1: Certificate1<T>, genesis_proposal: Proposal<T>) {
self.current_epoch = Some(genesis_proposal.epoch);
self.certs
.insert(ViewNumber::genesis(), genesis_cert1.clone());
self.locked_cert = Some(genesis_cert1);
self.proposals
.insert(ViewNumber::genesis(), genesis_proposal);
pub fn seed_parent(&mut self, cert1: Certificate1<T>, proposal: Proposal<T>) {
let view = cert1.view_number();
self.current_epoch = Some(proposal.epoch);
self.certs.insert(view, cert1.clone());
self.locked_cert = Some(cert1);
self.proposals.insert(view, proposal);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The generalization from seed_genesis to seed_parent looks clean. One thing the doc could mention: this method assumes cert1.view_number() matches proposal.view_number (i.e., the cert certifies the leaf derived from this proposal). If they can legitimately differ, the method will store the proposal under a view key that doesn't match its own view_number field, which will trip the commitment check in maybe_propose.

@imabdulbasit imabdulbasit enabled auto-merge (squash) June 5, 2026 13:29
@imabdulbasit imabdulbasit disabled auto-merge June 5, 2026 13:37
@imabdulbasit imabdulbasit merged commit 403f2e6 into main Jun 5, 2026
133 of 137 checks passed
@imabdulbasit imabdulbasit deleted the ab/fix-state-seeding-after-restart branch June 5, 2026 13:38
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

imabdulbasit added a commit that referenced this pull request Jun 5, 2026
…l from anchor leaf so leader can propose after restart (#4442)

[new-protocol] seed parent proposal from anchor leaf so leader can propose after restart (#4440)

* seed parent proposal from anchor leaf so leader can propose after restart

* use proposal view number

(cherry picked from commit 403f2e6)

Co-authored-by: Abdul Basit <45506001+imabdulbasit@users.noreply.github.com>
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.

2 participants