Skip to content

fix: harden PushQC against stale QCs and unverified headers#2910

Open
wen-coding wants to merge 4 commits intomainfrom
wen/fix_commitqc_verify
Open

fix: harden PushQC against stale QCs and unverified headers#2910
wen-coding wants to merge 4 commits intomainfrom
wen/fix_commitqc_verify

Conversation

@wen-coding
Copy link
Contributor

@wen-coding wen-coding commented Feb 17, 2026

Summary

  • Guard QC insertion and ctrl.Updated() with needQC to prevent stale QCs from advancing nextQC or triggering spurious updates.
  • Match blocks against stored (already verified) QC headers instead of the unverified incoming QC's headers. Uses per-block stored QC lookup to handle ranges spanning multiple QCs.
  • Cap block insertion loop at inner.nextQC to prevent nil-pointer dereference when a malicious QC's range extends beyond stored QCs.

Test plan

  • TestPushQCStaleQCDoesNotCorruptState: pushes a stale malicious QC with blocks extending beyond stored range. Verifies no panic (min cap), no state corruption (QCs, blocks, nextQC intact), and subsequent valid QC is accepted.
  • TestPushQCIgnoresBlocksMatchingUnverifiedHeaders: pushes a tampered QC (same range, different headers) with blocks matching the tampered headers. Verifies fake blocks are rejected and real blocks are accepted afterward.
  • Both tests verified to catch their respective bugs when the fix is reverted.

@github-actions
Copy link

github-actions bot commented Feb 17, 2026

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedFeb 19, 2026, 8:30 PM

@codecov
Copy link

codecov bot commented Feb 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 48.37%. Comparing base (7f57cdb) to head (d5053e8).
⚠️ Report is 1 commits behind head on main.

❗ There is a different number of reports uploaded between BASE (7f57cdb) and HEAD (d5053e8). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (7f57cdb) HEAD (d5053e8)
sei-chain 1 0
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #2910       +/-   ##
===========================================
- Coverage   57.35%   48.37%    -8.98%     
===========================================
  Files        2095      671     -1424     
  Lines      172870    50611   -122259     
===========================================
- Hits        99142    24484    -74658     
+ Misses      64842    23980    -40862     
+ Partials     8886     2147     -6739     
Flag Coverage Δ
sei-chain ?
sei-cosmos 48.19% <ø> (+<0.01%) ⬆️
sei-db 68.42% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 1547 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

func (s *State) PushQC(ctx context.Context, qc *types.FullCommitQC, blocks []*types.Block) error {
// Wait until QC is needed.
gr := qc.QC().GlobalRange()
needQC, err := func() (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if that's not too much trouble, I still think that it would be useful to skip verifying the QC if it is not needed. Then the fix would look as follows: if we already have the qc, then just drop the unverified qc and use the one we already have instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can do that, but why prefer skipping verification? Is QC verification very expensive?

@wen-coding wen-coding force-pushed the wen/fix_commitqc_verify branch from 64f5001 to 03bcfb9 Compare February 18, 2026 18:31
@wen-coding wen-coding changed the title Fix: always verify incoming CommitQCs in PushQC fix(data): guard PushQC insert with needQC to prevent stale QCs from advancing state Feb 18, 2026
@wen-coding wen-coding force-pushed the wen/fix_commitqc_verify branch from 03bcfb9 to b0b7d32 Compare February 18, 2026 18:42
…ing state

The insert section used `inner.nextQC < gr.Next` which allowed a stale
overlapping QC (where gr.First < inner.nextQC < gr.Next) to advance
nextQC even when verification was skipped. Replace with the existing
needQC guard so that only the next expected QC is inserted. Also move
ctrl.Updated() after the mutation rather than before.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wen-coding wen-coding changed the title fix(data): guard PushQC insert with needQC to prevent stale QCs from advancing state fix: guard PushQC insert with needQC to prevent stale QCs from advancing state Feb 18, 2026
@wen-coding wen-coding force-pushed the wen/fix_commitqc_verify branch from b0b7d32 to bfe9965 Compare February 18, 2026 18:46
@wen-coding wen-coding requested a review from pompon0 February 18, 2026 19:02

// Stale QC is silently ignored (verification is skipped, insert guard prevents
// advancing nextQC). No error is returned.
require.NoError(t, state.PushQC(ctx, maliciousQC, nil))
Copy link
Contributor

Choose a reason for hiding this comment

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

the fact that the maliciousQC is ignored here is an implementation detail. Both error and no error are acceptable results. What you should test against is that the state did not change: call QC() on all previous heights - it should return the correct QC (and perhaps check the nextQC, since the api does not expose it currently in any way).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point, changed.


// Verify state is not corrupted: the next valid QC should still be accepted.
qc2, blocks2 := TestCommitQC(rng, committee, keys, utils.Some(qc1.QC()))
require.NoError(t, state.PushQC(ctx, qc2, blocks2))
Copy link
Contributor

Choose a reason for hiding this comment

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

in particular this call returning no error does not guarantee that the qc2 was not ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed.

Copy link
Contributor

@pompon0 pompon0 left a comment

Choose a reason for hiding this comment

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

lgtm, added some comments wrt the test.

- Don't assert on malicious PushQC return value (implementation detail)
- Verify all previously pushed QCs are intact via state.QC()
- Check nextQC cursor did not advance
- Verify qc2 is actually visible after push
- Use correct leader key and OrPanic1 for merge compatibility

Co-authored-by: Cursor <cursoragent@cursor.com>
@wen-coding wen-coding requested a review from arajasek February 19, 2026 18:14
@wen-coding wen-coding enabled auto-merge (squash) February 19, 2026 19:30
@wen-coding wen-coding disabled auto-merge February 19, 2026 19:50
@wen-coding wen-coding enabled auto-merge (squash) February 19, 2026 19:50
When needQC is false (duplicate/stale QC), block header matching was
using the unverified incoming QC's headers. Now uses per-block stored
QC lookup with min(gr.Next, inner.nextQC) cap to prevent OOB access
when the incoming range extends beyond stored QCs.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wen-coding wen-coding changed the title fix: guard PushQC insert with needQC to prevent stale QCs from advancing state fix: harden PushQC against stale QCs and unverified headers Feb 19, 2026
@wen-coding wen-coding disabled auto-merge February 19, 2026 20:30
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.

3 participants

Comments