Skip to content

fix(spv): harden DIFF1 proof-header computation#4186

Open
piotr-roslaniec wants to merge 2 commits into
mainfrom
fix/spv-diff1-proof-headers-hardening
Open

fix(spv): harden DIFF1 proof-header computation#4186
piotr-roslaniec wants to merge 2 commits into
mainfrom
fix/spv-diff1-proof-headers-hardening

Conversation

@piotr-roslaniec

@piotr-roslaniec piotr-roslaniec commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

fix(spv): harden DIFF1 proof-header computation

Follow-up to #4039, which introduced the DIFF1-aware SPV proof-header walk in getProofInfo. A multi-agent review of that change surfaced edge-case, observability, and test-quality findings. This PR addresses them; the core algorithm from #4039 is unchanged.

Changes

  • Skip predicate now matches the Bridge exactly. Minimum-difficulty (DIFF1) headers are detected by exact target equality (header.Target() == minDifficultyTarget), mirroring the Bridge's target == MIN_DIFFICULTY_TARGET, instead of testing computed difficulty == 1 (which covers a superset of the canonical DIFF1 target and could otherwise let the maintainer assemble a proof the Bridge rejects).
  • Distinct skip signalling. getProofInfo returns a typed proofSkipReason, so the caller emits a dedicated log and metric for outside relay range (transient) versus exceeded max headers (potentially permanent, since the proof window is anchored at a fixed start block), instead of collapsing both into one generic warning. New counters: spv_proof_skipped_outside_relay_range_total, spv_proof_skipped_exceeded_max_headers_total.
  • maxProofHeaders bound documented, including its fixed-window limitation.
  • totalDifficultyRequired computed once when the requested difficulty is bound, rather than on every header iteration.
  • Removed the unused difficultyEpochLength constant.

Testing

  • Fixed swapped current/previous epoch-difficulty arguments in the test setup (they were stored inverted; the suite still passed, so it was not asserting the intended state).
  • Extended TestGetProofInfo: current-epoch binding on an asymmetric epoch span, an interior DIFF1 header being accounted for after the decisive header, the maxProofHeaders off-by-one boundary (decisive header exactly at vs. just past the bound), and the chain tip reached before any decisive header is bound.
  • go build ./..., go vet, staticcheck (-SA1019), and gofmt -l are clean; go test ./pkg/maintainer/spv passes (30 tests).

Summary by CodeRabbit

  • Bug Fixes

    • Improved SPV proof handling by distinguishing proofs skipped for relay-range conditions from those exceeding the maximum header limit.
    • Added safeguards for minimum-difficulty headers and more accurately determines when proofs can be completed.
    • Enhanced logging and metrics for skipped proofs.
  • Tests

    • Expanded coverage for proof boundaries, confirmation calculations, difficulty transitions, and header traversal edge cases.

Address review findings on the DIFF1-aware proof header walk:

- Match the Bridge's skip predicate exactly by comparing the decoded
  header target to the minimum-difficulty target, instead of testing
  computed difficulty == 1 (a superset of the canonical DIFF1 target).
- Distinguish the two skip causes: getProofInfo now returns a typed
  proofSkipReason so the caller emits a dedicated log and metric for
  'outside relay range' (transient) versus 'exceeded max headers'
  (potentially permanent), instead of one generic warning.
- Document the maxProofHeaders bound and its fixed-window limitation.
- Compute the required total difficulty once, when the requested
  difficulty is bound, rather than on every header iteration.
- Remove the unused difficultyEpochLength constant.
- Fix swapped current/previous epoch difficulty args in the test setup
  and extend coverage: current-epoch binding on an asymmetric span,
  interior DIFF1 header accounting, the header-bound off-by-one, and
  chain tip reached before a decisive header.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@piotr-roslaniec, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ef888778-2026-42ab-a51c-3bcff545636a

📥 Commits

Reviewing files that changed from the base of the PR and between 8414560 and ed12d9f.

📒 Files selected for processing (2)
  • pkg/maintainer/spv/spv.go
  • pkg/maintainer/spv/spv_test.go
📝 Walkthrough

Walkthrough

SPV proof assembly now returns explicit skip reasons for relay-range mismatches and header-bound limits. Transaction proving handles these outcomes separately, while tests cover updated confirmation, difficulty, boundary, and chain-tip behavior.

Changes

SPV proof skip classification

Layer / File(s) Summary
Proof info classification
pkg/maintainer/spv/spv.go
getProofInfo returns structured skip reasons, enforces a maximum header count, recognizes DIFF1 headers by target, and reports difficulty-epoch mismatches.
Proof skip handling
pkg/maintainer/spv/spv.go
proveTransactions logs and counts relay-range and header-bound skips separately before applying confirmation and submission checks.
Header-walking test coverage
pkg/maintainer/spv/spv_test.go
Tests validate skip reasons, confirmation counts, epoch binding, DIFF1 handling, header boundaries, and chain-tip termination.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant proveTransactions
  participant getProofInfo
  participant BitcoinHeaders
  participant Metrics
  proveTransactions->>getProofInfo: request proof confirmations
  getProofInfo->>BitcoinHeaders: inspect header chain and targets
  BitcoinHeaders-->>getProofInfo: header difficulty and position
  getProofInfo-->>proveTransactions: confirmations and skipReason
  proveTransactions->>Metrics: count classified proof skips
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: hardening DIFF1 proof-header computation in SPV.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/spv-diff1-proof-headers-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
pkg/maintainer/spv/spv.go (1)

259-296: 🩺 Stability & Availability | 🔵 Trivial

Consider log/compute cost for permanently-stuck transactions.

Since proofStartBlock is anchored to the transaction's confirmation block and never shifts across cycles, a transaction that hits proofSkipExceededMaxHeaders will hit it again every future proving cycle (re-walking up to 144 headers and re-emitting an Errorf each time), since the comment itself notes it "may be permanently unprovable." Over time this could produce persistent Error-level log/alert noise for a condition the maintainer can't resolve on its own.

Worth considering a dedup/rate-limit (e.g., log once per transaction, or downgrade to periodic Warnf) for this specific case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/maintainer/spv/spv.go` around lines 259 - 296, The
proofSkipExceededMaxHeaders handling should avoid repeated expensive scans and
Errorf log noise for the same permanently stuck transaction. Update the
proving-cycle state around proofStartBlock and the proofSkipExceededMaxHeaders
branch so each transaction is deduplicated or rate-limited, while preserving the
existing skip metric and behavior; use a periodic Warnf or equivalent for
subsequent occurrences.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/maintainer/spv/spv.go`:
- Around line 259-296: The proofSkipExceededMaxHeaders handling should avoid
repeated expensive scans and Errorf log noise for the same permanently stuck
transaction. Update the proving-cycle state around proofStartBlock and the
proofSkipExceededMaxHeaders branch so each transaction is deduplicated or
rate-limited, while preserving the existing skip metric and behavior; use a
periodic Warnf or equivalent for subsequent occurrences.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fe1dd57b-7373-41e6-859c-1db6ed143889

📥 Commits

Reviewing files that changed from the base of the PR and between 5245f66 and 8414560.

📒 Files selected for processing (2)
  • pkg/maintainer/spv/spv.go
  • pkg/maintainer/spv/spv_test.go

Address review findings on the DIFF1-aware proof header walk:

- Add a regression test pinning the skip predicate to exact target
  equality: a header with Difficulty()==1 but a target below the
  minimum-difficulty target must not be skipped. The test fails if the
  predicate regresses to computed-difficulty comparison.
- Add coverage for proveTransactions' per-skip-reason handling,
  asserting that a skip never submits a proof, an assemblable proof is
  submitted, and the expected metric counter is incremented.
- Guard the skip-reason switch with an explicit default so an
  unexpected reason surfaces as an error instead of silently falling
  through to proof submission.
- Derive minDifficultyTarget from compact bits via CompactToBig,
  dropping the duplicated hex literal and discarded ok.
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.

1 participant