Skip to content

fix(platform-wallet): act on swept transactions at the persistence seam - #4560

Open
romchornyi wants to merge 2 commits into
split/4406-2-storagefrom
split/4406-3-producer
Open

fix(platform-wallet): act on swept transactions at the persistence seam#4560
romchornyi wants to merge 2 commits into
split/4406-2-storagefrom
split/4406-3-producer

Conversation

@romchornyi

Copy link
Copy Markdown
Contributor

Stacked on #4559. Review only this PR's own diff; its base is split/4406-2-storage.
Third of the five PRs #4406 was split into: seam → storage → producer → Swift → Kotlin.
This is the heart of #4406, and the smallest it can be.

Issue being fixed or feature implemented

Nothing yet emits a sweep. This PR bumps the rust-dashcore pin and projects the TransactionsSwept event the bump brings with it, so the seam (#4558) and the store (#4559) finally carry the removal a losing double-spend requires.

The pin bump and the arms are one commit by construction. WalletEvent is not #[non_exhaustive] and platform has four exhaustive matches over it, so new-pin code cannot compile without the arms — and arms that did nothing would be worse than none, because upstream's removal is unconditional (wallet_checker.rs): the wallet drops the losing rows in memory, and a store that keeps them replays them at the next load.

What was done?

The pin

4db5c367 → rust-dashcore dev (21aaafed).

Worth knowing why it moves this far: v4.2-dev was pinned to a curated rebase line (chore/sync-fixes-payload-seam) that deliberately omits the whole sweep chain (#961/#962/#966/#969/#975) but carries #991's set_payload_finalizer, which masternode/update_service.rs now requires. Our previous pin had the reverse. No revision carrying both existed, so this takes dev, which carries everything.

dev also carries rust-dashcore#981, which collapses BIP-39 parsing onto one auto-detecting path. Platform's four hand-rolled "try every wordlist" helpers become that function and the call sites drop their Language argument (13 files). Unrelated to sweeps; it rides here only because the sweep chain and the payload-finalization seam this branch's base already depends on both sit above it on dev.

The producer

  • TransactionsSwept → one SweepBatch (core_bridge.rs); is_empty_no_records counts sweeps, so a sweep-only round still reaches the persister.
  • Compile-forced arms in balance_handler.rs (routes the post-removal balance snapshot — a sweep is the one event that can lower a balance) and payment_handler.rs (deliberate no-ops; the payment coupling is fix(platform-wallet): couple a sweep's payment flips to their own persistence round #4442).
  • spend_observer.rs gains sweep arms that report no observed spend: a sweep's released outpoints are coins that came back free, and the inputs it kept spent are precisely the ones it does not name, so the held set cannot be derived from the event at all.

The gate — what makes every intermediate host state safe

A backend that has not attested CORE_SWEEP_REMOVAL is not known to have applied the round's subtractive half, so its watermark is stripped before the store and the wallet faults exactly as on a rejection. Order is load-bearing: reporting the height durable first and faulting after cannot retract a height a legacy backend already committed. Such a host freezes its sync watermark on the first sweep it meets instead of diverging — fail-closed, funds-safe, and unfrozen the moment its persister ships (#4406's Swift and Kotlin PRs).

Reinstatement

A record arriving after a sweep of the same txid retracts that txid from the folded sweep, since persisters write records before replaying sweeps and would otherwise delete a row the wallet has brought back. The asset-lock half mirrors it: a sweep removes the tracked entry its funding transaction created, and AssetLockChangeSet::merge cancels a folded tombstone against a reinstating upsert (and vice versa), so no store sees an upsert/tombstone pair whose outcome depends on which it applies first.

How Has This Been Tested?

cargo test -p platform-wallet -p platform-wallet-ffi -p platform-wallet-storage — 928 + 310 + 138 pass, plus every integration suite in those crates.

Tests travelling with the change (core_bridge.rs): sweep_without_declared_capability_freezes_the_wallet_despite_a_successful_store pins the gate; sweep_names_the_dead_transactions_and_nothing_else, sweep_reaches_the_persister, merged_sweeps_stay_separate_and_ordered; transactions_swept_removes_the_tracked_asset_lock_it_funded and a_reinstating_reconstruction_folded_after_a_sweep_cancels_its_tombstone; transactions_swept_does_not_drive_payment_hooks pins the handler no-op.

Breaking Changes

None for platform's own API.

Release-timing constraint, not a merge constraint: do not cut a swift-sdk or kotlin-sdk release from a base that contains this PR but not its Swift/Kotlin counterparts. A mobile host at that base freezes its sync watermark on the first sweep it meets — funds-safe, but a user-visible stall. Between merges on v4.2-dev nothing auto-ships.

The pin bump also carries rust-dashcore#981's breaking mnemonic API; the platform-side adaptation is included here and is mechanical.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c804e43c-de48-4be2-a130-ada0f5d832ac

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@thepastaclaw

thepastaclaw commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

🕓 Ready for review — 27 ahead in queue (commit 267ecca)
Queue position: 28/37 · 2 reviews active
ETA: start ~11:50 UTC · complete ~12:54 UTC (median 1h 4m across 30 recent reviews; 2 slots)
Queued 4h 17m ago · Last checked: 2026-08-31 21:50 UTC

@romchornyi
romchornyi force-pushed the split/4406-2-storage branch from b7f2e47 to 4861a82 Compare August 31, 2026 16:47
@romchornyi
romchornyi force-pushed the split/4406-3-producer branch from 8a655b8 to cffc998 Compare August 31, 2026 16:47
@romchornyi

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Bumps the rust-dashcore pin to dev and projects the `TransactionsSwept`
event the bump brings with it. The two halves are one commit by
construction: `WalletEvent` is not `#[non_exhaustive]` and platform has
four exhaustive matches over it, so new-pin code cannot compile without
the arms — and arms that did nothing would be worse than none, because
upstream's removal is unconditional. The wallet drops the losing rows in
memory; a store that keeps them replays them at the next load and
re-creates the phantom balance the upstream fix exists to kill.

The projection is one `SweepBatch` per event, and a sweep-only round is
counted in `is_empty_no_records` so a round carrying nothing but a sweep
still reaches the persister.

The gate is what makes every intermediate host state safe. A backend
that has not attested `CORE_SWEEP_REMOVAL` is not known to have applied
the round's subtractive half, so its watermark is stripped BEFORE the
store and the wallet faults exactly as it would on a rejection —
reporting the height durable first and faulting after cannot retract a
height a legacy backend already committed. Such a host freezes its sync
watermark on the first sweep it meets instead of diverging: fail-closed,
funds-safe, and unfrozen the moment its persister ships.

A record arriving after a sweep of the same txid retracts that txid from
the folded sweep, since persisters write records before replaying sweeps
and would otherwise delete a row the wallet has brought back. The
asset-lock half mirrors it: a sweep removes the tracked entry its
funding transaction created, and `AssetLockChangeSet::merge` now cancels
a folded tombstone against a reinstating upsert (and vice versa), so no
store ever sees an upsert/tombstone pair for one outpoint whose outcome
depends on which it applies first.

The pin also carries rust-dashcore#981, which collapses BIP-39 parsing
onto one auto-detecting path. Platform's four hand-rolled
"try every wordlist" helpers are now that function, and the call sites
drop their `Language` argument. It is unrelated to sweeps and rides here
only because the sweep chain and the payload-finalization seam this
branch's base already depends on both sit above it on dev.

`spend_observer`'s two projections gain sweep arms that report no
observed spend: a sweep's released outpoints are coins that came back
free, and the inputs it kept spent are precisely the ones it does not
name, so the held set cannot be derived from the event at all.
…ouched

`cargo fmt --check --all` is a CI gate and the collapsed
`Mnemonic::from_phrase` calls left two of them wrapped.
@romchornyi
romchornyi force-pushed the split/4406-3-producer branch from cffc998 to 267ecca Compare August 31, 2026 17:31
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