Skip to content

fix: cross-validate ExtendedCommit against LastCommit in blocksync (backport #2884)#2930

Merged
rootulp merged 2 commits intov0.39.x-celestiafrom
mergify/bp/v0.39.x-celestia/pr-2884
Apr 17, 2026
Merged

fix: cross-validate ExtendedCommit against LastCommit in blocksync (backport #2884)#2930
rootulp merged 2 commits intov0.39.x-celestiafrom
mergify/bp/v0.39.x-celestia/pr-2884

Conversation

@mergify
Copy link
Copy Markdown
Contributor

@mergify mergify Bot commented Apr 16, 2026

Motivated by https://dashboard.hackenproof.com/manager/companies/celestia/celestia/reports/CELESTIA-238 even though that report is N/A because the attack doesn't really work as described.

Summary

  • Cross-validate the ExtendedCommit against the validator set during block sync before persisting to the blockstore
  • Use state.Validators.VerifyCommit() on extCommit.ToCommit() to validate all signatures and addresses match the validator set, matching how upstream CometBFT handles this
  • Add tests demonstrating that a corrupted ExtendedCommit with a same-length but wrong ValidatorAddress bypasses existing validation (ValidateBasic, EnsureExtensions) and causes a panic on consensus restart via reconstructLastCommitToExtendedVoteSet

Background

During block sync, the blocksync reactor validates second.LastCommit via VerifyCommitLight but saves the separately-received extCommit without cross-validating it. A malicious peer could provide a valid block and LastCommit but a corrupted ExtendedCommit (e.g. with wrong ValidatorAddress fields). The corrupted ExtendedCommit would pass EnsureExtensions (which only checks extension presence) and be persisted to the blockstore. On consensus restart, reconstructLastCommit would load the poisoned ExtendedCommit and panic in addSigsToVoteSet due to the address mismatch.

Approach

Call state.Validators.VerifyCommit(chainID, firstID, first.Height, extCommit.ToCommit()) before persisting the ExtendedCommit. This validates that every non-absent signature's ValidatorAddress matches the validator at that index and verifies all cryptographic signatures. On any mismatch, the peer is disconnected.

An earlier approach compared extCommit.ToCommit().Hash() against second.LastCommit.Hash(), but this produced false positives because these two commits can legitimately contain different vote subsets (the proposer of second may have collected different 2/3+ signatures than the peer who provided extCommit). The VerifyCommit approach avoids this by validating the ExtendedCommit against the validator set directly, without comparing against second.LastCommit.

Test plan

  • TestReportedAttack_PrependByteToValidatorAddress — confirms the originally reported 0xFF-prepend attack is already caught during deserialization
  • TestSameLengthAddressCorruption_PanicsOnRestart — proves the real validation gap: a same-length address corruption bypasses all pre-existing checks and causes a panic on vote set reconstruction
  • TestVerifyCommitDetectsAddressCorruption — proves VerifyCommit detects the corruption
  • Existing blocksync tests pass

🤖 Generated with Claude Code


This is an automatic backport of pull request #2884 done by Mergify.


Open with Devin

…2884)

Motivated by
https://dashboard.hackenproof.com/manager/companies/celestia/celestia/reports/CELESTIA-238
even though that report is N/A because the attack doesn't really work as
described.

## Summary

- Cross-validate the ExtendedCommit against the validator set during
block sync before persisting to the blockstore
- Use `state.Validators.VerifyCommit()` on `extCommit.ToCommit()` to
validate all signatures and addresses match the validator set, matching
how upstream CometBFT handles this
- Add tests demonstrating that a corrupted ExtendedCommit with a
same-length but wrong ValidatorAddress bypasses existing validation
(`ValidateBasic`, `EnsureExtensions`) and causes a panic on consensus
restart via `reconstructLastCommit` → `ToExtendedVoteSet`

### Background

During block sync, the blocksync reactor validates `second.LastCommit`
via `VerifyCommitLight` but saves the separately-received `extCommit`
without cross-validating it. A malicious peer could provide a valid
block and LastCommit but a corrupted ExtendedCommit (e.g. with wrong
ValidatorAddress fields). The corrupted ExtendedCommit would pass
`EnsureExtensions` (which only checks extension presence) and be
persisted to the blockstore. On consensus restart,
`reconstructLastCommit` would load the poisoned ExtendedCommit and panic
in `addSigsToVoteSet` due to the address mismatch.

### Approach

Call `state.Validators.VerifyCommit(chainID, firstID, first.Height,
extCommit.ToCommit())` before persisting the ExtendedCommit. This
validates that every non-absent signature's `ValidatorAddress` matches
the validator at that index and verifies all cryptographic signatures.
On any mismatch, the peer is disconnected.

An earlier approach compared `extCommit.ToCommit().Hash()` against
`second.LastCommit.Hash()`, but this produced false positives because
these two commits can legitimately contain different vote subsets (the
proposer of `second` may have collected different 2/3+ signatures than
the peer who provided `extCommit`). The `VerifyCommit` approach avoids
this by validating the ExtendedCommit against the validator set
directly, without comparing against `second.LastCommit`.

## Test plan

- [x] `TestReportedAttack_PrependByteToValidatorAddress` — confirms the
originally reported 0xFF-prepend attack is already caught during
deserialization
- [x] `TestSameLengthAddressCorruption_PanicsOnRestart` — proves the
real validation gap: a same-length address corruption bypasses all
pre-existing checks and causes a panic on vote set reconstruction
- [x] `TestVerifyCommitDetectsAddressCorruption` — proves `VerifyCommit`
detects the corruption
- [x] Existing blocksync tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit f98262f)
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +1 to +3
- `[blocksync]` Cross-validate ExtendedCommit against LastCommit before
persisting during block sync to prevent a malicious peer from injecting a
corrupted ExtendedCommit that causes a panic on consensus restart.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changelog filename missing required issue/PR number prefix

Both CONTRIBUTING.md and CLAUDE.md mandate that changelog entries follow the naming format {issue-or-pr-number}-{description}.md. The file .changelog/unreleased/bug-fixes/blocksync-extcommit-validation.md is missing the PR number prefix — it should be 2930-blocksync-extcommit-validation.md (for PR #2930).

Rule references

From CONTRIBUTING.md: "Every fix, improvement, feature, or breaking change should be made in a pull-request that includes a file .changelog/unreleased/${category}/${issue-or-pr-number}-${description}.md"

From CLAUDE.md: "Every PR needs a changelog entry at .changelog/unreleased/{category}/{issue-or-pr-number}-{description}.md"

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@rootulp rootulp self-assigned this Apr 17, 2026
@rootulp rootulp merged commit ad11d38 into v0.39.x-celestia Apr 17, 2026
22 checks passed
@rootulp rootulp deleted the mergify/bp/v0.39.x-celestia/pr-2884 branch April 17, 2026 15:58
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