Skip to content

blockchain: separate proposal and finalized header validation - #1039

Draft
2dvorak wants to merge 2 commits into
kaiachain:devfrom
2dvorak:fix/validate-proposal-header
Draft

blockchain: separate proposal and finalized header validation#1039
2dvorak wants to merge 2 commits into
kaiachain:devfrom
2dvorak:fix/validate-proposal-header

Conversation

@2dvorak

@2dvorak 2dvorak commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Proposed changes

  • One entry point served two callers: the import path passes a finalized header, the consensus path a proposal, which has no committed seals yet.
  • Splits it in two: ValidateHeader (all rules) and ValidateProposalHeader (all rules except the committed-seal ones).
  • Author authorization now runs on both paths, backend.Verify uses the proposal entry point and no longer tolerates ErrEmptyCommittedSeals.

Depends on 1022, will be rebased after merge.

Types of changes

  • 🐛 Bug fix
  • ✨ Non-hardfork changes (node upgrade not required)
  • 💥 Hardfork / consensus-breaking changes
  • 🧪 Test improvements
  • 🧰 CI / build tool
  • ♻️ Chore / Refactor / Non-functional changes

Checklist

  • 📖 I have read the CONTRIBUTING GUIDELINES doc
  • 📝 I have signed in the PR comment I have read the CLA Document and I hereby sign the CLA in first time contribute after having read CLA
  • 🟢 Lint and unit tests pass locally with my changes ($ make test)

Related issues

Further comments

hyunsooda and others added 2 commits August 7, 2026 15:21
verifySeals required the header author to equal the final round's proposer once
permissionless is active. A locked proposal is re-proposed verbatim in later
rounds, so its author seal stays with the proposer that built it while
sendPreprepare stamps the current round into the header. The round byte sits in
the vanity that SigHash and HeaderHash zero out, so that rewrite leaves both
the author seal and the block hash intact, which is also why the lock still
matches. A block that survived a round change therefore commits with an author
the final round's schedule does not name, and every node refuses it on import.
The live path never noticed: a proposal carries no committed seals, so
verifySeals returns ErrEmptyCommittedSeals before the check and Verify ignores
that error. Since the round-change timeout grows by 2^round seconds, the height
cannot proceed until the original proposer's turn comes around again.

Require the author to be a member of the round's committee instead. That is the
one constraint the committed seals do not already carry: the block hash covers
the proposer seal, so 2f+1 seals bind the author, and header.Round integrity
comes from the round-bound committed-seal preimage. Pre-permissionless has
always checked membership, so both branches now agree.

Constraint: a locked proposal is re-proposed verbatim, so the header pairs the
  final round with an earlier round's author seal
Rejected: accept any author whose address appears as the proposer of rounds
  0..R | R+1 proposer lookups on a peer-supplied round, and it forbids nothing
  the committee check allows
Rejected: drop the author check outright | leaves the header author
  unconstrained for the first time, and GetRewardAddress then returns empty for
  an unstaked author, which skips Rewardbase validation
Confidence: high
Scope-risk: narrow
Directive: the membership check must precede countValidCommittedSeals, which
  consumes committeeSet
Not-tested: a round change driven end to end through the live consensus core

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Header validation had one entry point for two callers with different inputs: the
import path passes a finalized header, the consensus path a proposal, which
carries no committed seals yet. The seal checks therefore had to be tolerated
rather than skipped, and the author check sat behind them.

Give the callers their own entry points:

  ValidateHeader          - finalized header, all rules
  ValidateProposalHeader  - proposal, all rules except the committed-seal ones

Both share validateHeader(header, parent, withSeals). Author authorization runs
either way and moves into authorizeAuthor(), which also returns the set the
committed seals are counted against so both use the same one. backend.Verify
calls ValidateProposalHeader and no longer tolerates ErrEmptyCommittedSeals.

Proposal validation no longer inspects committed seals at all; any present in a
proposal are ignored. Commit overwrites them and HeaderHash excludes them, so
they cannot affect the block identity that was voted on.
@2dvorak 2dvorak self-assigned this Aug 14, 2026
@2dvorak 2dvorak added the do not merge Do not merge just yet label Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge Do not merge just yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants