Skip to content

feat(key-wallet-manager): carry the sweep winner's mined height on TransactionsSwept - #975

Open
romchornyi wants to merge 2 commits into
devfrom
feat/sweep-winner-mined-height
Open

feat(key-wallet-manager): carry the sweep winner's mined height on TransactionsSwept#975
romchornyi wants to merge 2 commits into
devfrom
feat/sweep-winner-mined-height

Conversation

@romchornyi

@romchornyi romchornyi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

WalletEvent::TransactionsSwept names the winner (superseded_by) but carries nothing about its finality context, and the winner need not be wallet-relevant — it can spend our coin while paying only external addresses, so it may never appear anywhere else in the wallet's event stream (the same asymmetry that motivated released_outpoints in #962). A consumer durably mirroring the swept spends therefore cannot tell a sweep triggered by a transaction mined in a block from one triggered by an InstantSend-locked transaction still waiting to be mined, and cannot recover the distinction from its own records.

That distinction is load-bearing downstream. dashpay/platform#4406 keeps durable observed-spent rows for the coins a sweep consumed, so a restored wallet does not re-credit them. Retiring those rows needs a finality horizon: a block-context sweep anchors the winner at a height that chainlocks, while an IS-locked winner has no mining deadline — aging its rows out by wall clock would delete a genuine hold while the conflict is still unmined. key-wallet already applies exactly this doctrine internally (wallet_checker.rs: mempool/IS-lock spends are deliberately never recorded in observed_spent_outpoints; prune_finalized_observed_spends evicts on spend_height <= min(chainlock_height, synced_height)). The emission sites already know the winner's context — the event just did not forward it.

  • Adds winner_mined_height: Option<CoreBlockHeight> to WalletEvent::TransactionsSwept.
    • The block emission site in key-wallet-manager/src/process_block.rs (process_block_for_wallets) passes Some(height) — the winner is a transaction of that very block, so the block's height is its mined height. This includes the late-block InChainLockedBlock path, where the height is the same.
    • The mempool emission site (process_mempool_transaction) passes None, which is exhaustive there: sweeps fire only for context.confirmed() || context.is_instant_send() (wallet_checker.rs), so the only off-chain trigger is an IS-locked winner, which is by definition unmined. These are the only two construction sites of the event.
  • Display for the event now includes winner_mined_height.
  • No behavioral change to the sweep itself — like feat(key-wallet-manager)!: name the outpoints a sweep releases #962, this is a contract addition surfacing what the emission site already knows.

Not the same thing as #968

#968 asks the sweep to attest wallet ownership of the loser's held inputs, which the sweep site cannot decide today (no ownership information for unclassified-ours coins) and which carries a semantics trade-off flagged open in that issue. This PR carries the winner's context, which the emission site trivially has, and decides nothing about ownership. The two are orthogonal; landing this does not implement #968. It does change #968's cost/benefit — the placeholder rows #968 exists to avoid become retirable once a block-context sweep (or a later BlockProcessed/chainlock advance) gives them a finality horizon, so the population is no longer permanent — which may be worth weighing when deciding whether #968 is still needed.

C ABI: deliberately unchanged

WalletEvent is Rust-only (Debug + Clone, no serde), so the field addition is a compile-time-visible Rust change with no wire format. In dash-spv-ffi, the dispatch's exhaustive destructure acknowledges the field (winner_mined_height: _) without forwarding it: #962 established that OnTransactionsSweptCallback has no safely-degrading insertion point — a C consumer declaring the function pointer by hand keeps compiling against a grown parameter list and reads shifted arguments. The regenerated header was diffed against the pre-change one: the callback signature is byte-for-byte identical (the field's only appearance is in the copied doc comment, which now records that the Rust event carries more than the C surface and why). If a C consumer ever needs the height, adding it must be a flagged breaking change like #962's.

Test plan

  • key-wallet-manager/src/event_tests.rs::test_block_winner_emits_swept_event_naming_the_released_outpoints — extended to assert the block-context sweep carries exactly Some(101) (the processed block's height), not None and not any other height the manager has seen.
  • New test_is_locked_mempool_winner_sweeps_with_no_mined_height — an IS-locked winner arriving via process_mempool_transaction sweeps a recorded loser and the event carries None; the pair fails if the two contexts are conflated in either direction.
  • dash-spv-ffi dispatch tests updated for the new field; both marshalling tests still pass, confirming the C-side parameter layout is untouched.
  • cargo test -p key-wallet — 663 passed. cargo test -p key-wallet-manager — 56 passed (all suites green).
  • cargo test -p dash-spv-ffi --lib — 49 passed.
  • cargo clippy --all-features --all-targets -- -D warnings — clean. cargo fmt --check — clean.
  • Not verified: no live-sync testing against a real dashd/SPV chain; the C surface is exercised through the Rust-side dispatch tests and the regenerated-header diff, not from an actual C or Swift caller.

Summary by CodeRabbit

  • New Features

    • Sweep events now report the winning transaction’s mined block height when applicable.
    • InstantSend-locked mempool sweeps indicate when no mined height is available.
    • Event details display the winning block height for improved transaction tracking.
  • Bug Fixes

    • Improved sweep-event reporting for block-confirmed and mempool transaction replacements.
    • Ensured defeated transactions and released outpoints remain accurately reported.

…ansactionsSwept

WalletEvent::TransactionsSwept names the winner (superseded_by) but not
its finality context, and the winner need not be wallet-relevant, so a
consumer durably mirroring the swept spends cannot look its height up
anywhere: it cannot tell a sweep triggered by a mined block from one
triggered by an InstantSend-locked transaction still waiting to be
mined, and so has no sound horizon for retiring those records.

Add winner_mined_height: Option<CoreBlockHeight> to the event. The
block emission site in process_block.rs passes Some(height) — the
winner is a transaction of that very block — and the mempool site
passes None, which is exhaustive there: only an IS-locked arrival
sweeps off-chain, and an IS-locked winner is by definition unmined.

The C ABI is deliberately unchanged: OnTransactionsSweptCallback has no
safely-degrading insertion point for hand-declared C consumers (#962),
so the FFI dispatch acknowledges the field without forwarding it and
the generated header is byte-for-byte identical.
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ce05523f-b391-4585-8fe3-f635f8e2c503

📥 Commits

Reviewing files that changed from the base of the PR and between 6641c2c and 9643f80.

📒 Files selected for processing (1)
  • key-wallet-manager/src/events.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

TransactionsSwept now reports the superseding transaction’s optional mined height. Block sweeps set the height, mempool sweeps use None, and the FFI callback keeps its existing ABI.

Changes

Swept transaction height

Layer / File(s) Summary
Event contract and formatting
key-wallet-manager/src/events.rs
WalletEvent::TransactionsSwept now includes winner_mined_height: Option<CoreBlockHeight>. Its display output and tests include the value.
Block and mempool propagation
key-wallet-manager/src/process_block.rs, key-wallet-manager/src/event_tests.rs
Block sweeps report the processed block height. Mempool sweeps report None. Tests validate both cases, the superseded transaction, and released outpoints.
FFI callback compatibility
dash-spv-ffi/src/callbacks.rs
The callback matches and ignores the new field. Existing C callback arguments remain unchanged. Tests cover mined and unmined winners.

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

Merge Risk: ⚪ Minimal · up to 9643f

The PR adds the sweep winner’s mined-height context without changing sweep behavior, and the supplied checks are green; no actionable merge-blocking risk remains beyond normal review.

Suggested reviewers: xdustinface

Sequence Diagram(s)

sequenceDiagram
  participant process_block
  participant WalletEvent_TransactionsSwept
  participant dash_spv_ffi_callbacks
  alt Block sweep
    process_block->>WalletEvent_TransactionsSwept: set winner_mined_height to block height
  else Mempool sweep
    process_block->>WalletEvent_TransactionsSwept: set winner_mined_height to None
  end
  WalletEvent_TransactionsSwept->>dash_spv_ffi_callbacks: dispatch existing callback arguments
  Note over dash_spv_ffi_callbacks: ignore winner_mined_height to preserve the C ABI
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes adding the sweep winner's mined height to TransactionsSwept.
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 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sweep-winner-mined-height

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[bot]
coderabbitai Bot previously approved these changes Aug 20, 2026
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.83%. Comparing base (5877d15) to head (9643f80).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #975      +/-   ##
==========================================
- Coverage   76.96%   76.83%   -0.13%     
==========================================
  Files         329      329              
  Lines       82676    82698      +22     
==========================================
- Hits        63631    63544      -87     
- Misses      19045    19154     +109     
Flag Coverage Δ
core 78.25% <ø> (ø)
ffi 50.78% <100.00%> (-1.30%) ⬇️
rpc 20.00% <ø> (ø)
spv 91.94% <ø> (+0.06%) ⬆️
wallet 79.13% <100.00%> (+0.08%) ⬆️
Files with missing lines Coverage Δ
dash-spv-ffi/src/callbacks.rs 86.61% <100.00%> (+0.03%) ⬆️
key-wallet-manager/src/events.rs 74.67% <100.00%> (+10.35%) ⬆️
key-wallet-manager/src/process_block.rs 92.30% <ø> (ø)

... and 22 files with indirect coverage changes

…ent's Display

The `Display` arm was the one line the new field reached that no test
exercised. It is not decoration: when a swept coin misbehaves on a
consumer, whether the winner was mined is the first thing the log has to
answer, and a formatter that silently dropped the field would send a
reader looking in the wrong place.

Both legs are asserted in one test because either alone is satisfiable
by a formatter that prints a constant.
@github-actions github-actions Bot removed the ready-for-review CodeRabbit has approved this PR label Aug 21, 2026
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Aug 21, 2026
@romchornyi
romchornyi requested a review from ZocoLini August 21, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants