Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2581a83
fix(wallet): restore persisted spent UTXO state
lklimek Sep 16, 2026
3abe4f2
chore(deps): use upstream rust-dashcore branch
lklimek Sep 16, 2026
67646b1
chore(deps): pin rust-dashcore persistence restore PR
lklimek Sep 16, 2026
c9817d9
fix(wallet)!: restore persisted core state atomically
lklimek Sep 16, 2026
f43c093
fix(wallet): enforce restored ownership and preserve coinbase maturity
lklimek Sep 16, 2026
a4bad7a
fix(wallet): pin validated restoration and monotonic lock handling
lklimek Sep 16, 2026
9729bfd
Merge branch 'v4.3-dev' into fix/platform-storage-utxo-resurrection
lklimek Sep 17, 2026
f89387c
fix(wallet): preserve account-local history and spend evidence on res…
lklimek Sep 17, 2026
3df237e
fix(wallet): update persisted-state restoration dependency
lklimek Sep 17, 2026
166662a
fix(wallet-storage): reconcile restored lock and ownership state
lklimek Sep 17, 2026
1ea4c03
test(wallet-storage): reproduce locked conflict resurrection on reload
lklimek Sep 17, 2026
9d58887
fix(wallet-storage): restore coherent wallet snapshots
lklimek Sep 17, 2026
16e3402
fix(wallet-storage): reject InstantLocks with mismatched row txids
lklimek Sep 17, 2026
abfef21
chore(wallet): restore base rust-dashcore dependency pin
lklimek Sep 18, 2026
841a4b9
fix(storage): preserve address state for legacy Core resync
lklimek Sep 18, 2026
9eddaaa
test(storage): distinguish snapshot reload from legacy resync
lklimek Sep 18, 2026
5981a8f
feat(wallet): capture coherent Core persistence snapshots
lklimek Sep 18, 2026
4e17785
fix(storage): reject stale account and provider pool snapshots
lklimek Sep 18, 2026
5a976a4
test(wallet): check snapshot capture releases manager lock
lklimek Sep 18, 2026
4b1bd0a
test(storage): verify provider snapshots and atomic concurrent reloads
lklimek Sep 18, 2026
9e6440f
test(storage): verify recovery of sparse address pool holes
lklimek Sep 18, 2026
e2b7fd4
fix(storage): persist complete Core wallet snapshots
lklimek Sep 18, 2026
649b7d9
test(storage): align recovery cases with Core snapshots
lklimek Sep 18, 2026
729efda
Merge branch 'v4.3-dev' into fix/platform-storage-utxo-resurrection
lklimek Sep 18, 2026
fc01e47
test(storage): account for shielded load query overhead
lklimek Sep 18, 2026
bdd8771
fix(storage): validate snapshot locks and index account records
lklimek Sep 18, 2026
f2c162a
Merge remote-tracking branch 'origin/v4.3-dev' into fix/platform-stor…
lklimek Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [Unreleased]

### Fixed

- Restore wallet records and coins consistently after separately persisted InstantSend locks, accept legacy outgoing contact payments, and apply saved ChainLock finality when opening a wallet.
- Preserve legacy provider-account history, reconcile InstantSend conflicts on reopen, and read wallet storage from a consistent SQLite snapshot during concurrent writes.

## [4.2.0-dev.11](https://github.com/dashpay/platform/compare/v4.2.0-dev.10...v4.2.0-dev.11) (2026-09-14)


Expand Down
47 changes: 24 additions & 23 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ grovedb-storage = { git = "https://github.com/dashpay/grovedb", rev = "a579d52fc
grovedb-version = { git = "https://github.com/dashpay/grovedb", rev = "a579d52fcf68c3cbd798c5a94e73afdc41b5f5a6" }
grovedb-epoch-based-storage-flags = { git = "https://github.com/dashpay/grovedb", rev = "a579d52fcf68c3cbd798c5a94e73afdc41b5f5a6" }
grovedb-commitment-tree = { git = "https://github.com/dashpay/grovedb", rev = "a579d52fcf68c3cbd798c5a94e73afdc41b5f5a6" }
dashcore = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
dash-network-seeds = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
dash-spv = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
key-wallet = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
key-wallet-ffi = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
key-wallet-manager = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
dash-network = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
dashcore-rpc = { git = "https://github.com/dashpay/rust-dashcore", rev = "e4208c90786a6854bd498315bcb571ef24182c15" }
dashcore = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
dash-network-seeds = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
dash-spv = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
key-wallet = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
key-wallet-ffi = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
key-wallet-manager = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
dash-network = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
dashcore-rpc = { git = "https://github.com/Claudius-Maginificent/rust-dashcore", rev = "9a8022854bbf4e2ab9bb8a77df4ba2cfecbcb45f" }
tokio-metrics = "0.5"

# Size-tuned profile for the iOS `rs-unified-sdk-ffi` staticlib, which
Expand Down
1 change: 1 addition & 0 deletions packages/rs-platform-wallet-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ apple-native-keyring-store = { version = "=1.0.0", features = ["keychain"], opti
windows-native-keyring-store = { version = "=1.0.0", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt"] }
# `test-utils` reaches `provider_key_test_wallet`, shared with
# `platform-wallet`'s own `rebuild_provider_key_account` tests — see its use
# in `sqlite/provider_accounts.rs`'s test module.
Expand Down
17 changes: 10 additions & 7 deletions packages/rs-platform-wallet-storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,16 @@ runs migrations and migrating a structurally corrupt file only deepens the
damage. Recovery also refuses `auto_backup_dir = None`, so the rescue
attempt always keeps a rollback point.

Two sites degrade under *both* policies, because their signal cannot
distinguish corruption from a healthy wallet: a used address whose owner is
not one of the wallet's funds accounts (what a masternode-operator wallet
looks like — provider accounts are not funds accounts), and a restored
address that does not resolve against its account xpub (foreign, or
legitimately sparse past the bounded-work cap). Both re-warm on the next
sync; the balance total is exact regardless.
A used address whose owner is not one of the wallet's funds accounts degrades
under both policies (provider accounts are not funds accounts). A persisted
unspent coin whose address cannot be verified against its restored account
instead fails wallet restoration. Restore a missing derivation range before
retrying; the loader cannot report an exact balance while ownership is unknown.

`load()` reads all wallets, records, coins, and sync checkpoints from one SQLite
snapshot, so another connection's commits cannot mix different wallet states.
Legacy provider records are matched after restoring provider key pools, and
saved InstantSend locks remove conflicting transactions and their descendants.

`LoadDegradation` also reports `unimplemented_rows`: rows sitting in tables
`load()` has no reader for. Those are intact, merely unread, so they never
Expand Down
23 changes: 16 additions & 7 deletions packages/rs-platform-wallet-storage/SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ erDiagram
INTEGER block_time "NULL on height-only rows and while unconfirmed"
INTEGER finalized "0 | 1; always 0 on height-only rows"
BLOB record_blob "NULL for height-only UTXO rows"
BLOB account_records_blob "per-account slices; NULL for legacy or height-only rows"
}

CORE_UTXOS {
Expand All @@ -90,7 +91,7 @@ erDiagram
INTEGER is_sweep_placeholder "1 until funding arrives"
INTEGER spent "0 | 1"
BLOB spent_in_txid "set by apply_sweep for an unresolved held input; else NULL"
INTEGER winner_mined_height "V007: sweep winner's mined height; NULL when unstamped or materialised"
INTEGER winner_mined_height "block-spend evidence; NULL when no mined spend is known"
}

CORE_INSTANT_LOCKS {
Expand Down Expand Up @@ -392,6 +393,13 @@ the disagreement costs: under `LoadPolicy::Strict` it aborts the load, under
Repairing the drifted columns is a writer's job, on the next write of that
row.

`account_records_blob` stores the account-local record slices needed by
restart restoration. Partial updates merge by account and preserve sibling
slices. Legacy rows retain a NULL blob and are reconstructed from owned input
and output addresses after the account pools are restored; ambiguous ownership
fails the wallet load. Payload-only key-account involvement is recovered through
the transaction matcher, including provider records retained after finality.

- PK: `(wallet_id, txid)`.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.
- Index: `idx_core_transactions_height(wallet_id, height)`.
Expand Down Expand Up @@ -419,18 +427,18 @@ network-final spender of a coin it knows is wallet-relevant, so its view of
link. A delivery through `spent_utxos` onto a placeholder materialises it
the same way instead of marking it in place.

`winner_mined_height` (V007) stamps that claim with the mined height of the
winner named in `spent_in_txid`, and decides the placeholder's lifetime
rather than its existence. A block-context sweep stamps the winner's own
`winner_mined_height` (V007) retains block-spend evidence, including an observed
spend with no transaction record or `spent_in_txid`. A block-context sweep stamps the winner's own
height and `collect_finalized_tombstones` evicts the row once
`min(chainlock_height, synced_height)` reaches it — upstream's
`prune_finalized_observed_spends` boundary verbatim. An InstantSend-locked
winner that is not yet mined leaves it NULL: the lock alone settles the
input, but it carries no height to key a lifetime on, so the row resolves
only through proof (the funding upsert materialising it, a later
block-context sweep re-stamping it, or a release). The funding upsert
clears the stamp, because a materialised row is the wallet's own coin held
spent and is permanently outside the collector's reach.
block-context sweep re-stamping it, or a release). Funding materialisation
preserves the stamp while the coin remains spent, so restoration retains the
block-spend evidence. Materialised rows are outside the collector's reach.
A release clears the stamp together with the spent flag.

- PK: `(wallet_id, outpoint)`.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.
Expand Down Expand Up @@ -847,3 +855,4 @@ table-rebuild migration, as V004 does.
| V016 | `V016__identity_keys_null_scope_requires_existing_identity.rs` | Recreates the `identity_keys` null-scope trigger pair (see Triggers above) to also reject a NULL-scoped key naming an identity that does not exist at all, closing the gap where V008's guard caught only the wallet-owned case. |
| V017 | `V017__identity_scan_state.rs` | Adds `identity_scan_states` (one row per wallet: the last gap-limit identity-scan verdict — `complete`, `probed_from`/`probed_through`, `unlocated_gap`) and `identity_scan_failed_indices` (indices probed without an answer, cascading from the verdict row via `wallet_id`). Purely additive; an upgraded database reads back "no verdict recorded" for every wallet until the next scan (dashpay/platform#4365). |
| V018 | `V018__identity_hard_delete.rs` | Retires identity tombstoning. Adds `cascade_children_on_identity_delete` (brooms `identity_keys` / `contacts` / `ignored_senders` / `pending_contact_crypto` by the deleted identity id, covering the rows no live FK reaches) plus its access-path indexes `idx_contacts_owner`, `idx_ignored_senders_owner`, and `idx_pending_contact_crypto_owner`; purges every already-tombstoned identity and its dependents; drops `identities.tombstoned`. |
| V019 | `V019__core_account_records.rs` | Adds nullable per-account transaction slices; legacy folded records remain readable through ownership-based reconstruction. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Preserve per-account transaction slices alongside the wallet-level record.

pub fn migration() -> String {
"ALTER TABLE core_transactions ADD COLUMN account_records_blob BLOB;".to_string()
}
7 changes: 7 additions & 0 deletions packages/rs-platform-wallet-storage/src/sqlite/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ pub enum AutoBackupOperation {
/// Errors produced by the wallet-storage SQLite backend.
#[derive(Debug, thiserror::Error)]
pub enum WalletStorageError {
/// Persisted Core lifecycle state violates the wallet restore contract.
#[error("persisted core wallet state is inconsistent")]
CoreStateRestore(#[from] key_wallet::wallet::managed_wallet_info::RestoreError),

/// File-system I/O error reaching the database or backup files.
#[error("io error")]
Io(#[from] std::io::Error),
Expand Down Expand Up @@ -749,6 +753,7 @@ impl WalletStorageError {
| Self::ProviderKeyAccountConflict { .. }
| Self::TypedPoolKeyConflict { .. }
| Self::AccountRecordInvalid { .. }
| Self::CoreStateRestore(_)
| Self::MissingAccount { .. }
| Self::AccountRejected { .. }
| Self::AssetLockEntryMismatch { .. }
Expand Down Expand Up @@ -853,6 +858,7 @@ impl WalletStorageError {
| Self::ProviderKeyAccountConflict { .. }
| Self::TypedPoolKeyConflict { .. }
| Self::AccountRecordInvalid { .. }
| Self::CoreStateRestore(_)
| Self::MissingAccount { .. }
| Self::AccountRejected { .. }
| Self::AssetLockEntryMismatch { .. }
Expand Down Expand Up @@ -930,6 +936,7 @@ impl WalletStorageError {
Self::WalletlessIdentityIndex { .. } => "walletless_identity_index",
Self::OrphanedIdentityEntry { .. } => "orphaned_identity_entry",
Self::AccountRecordInvalid { .. } => "account_record_invalid",
Self::CoreStateRestore(_) => "core_state_restore",
Self::MissingAccount { .. } => "missing_account_registration_entry",
Self::AccountRejected { .. } => "account_rejected",
Self::AccountRegistrationEntryMismatch => "account_registration_entry_mismatch",
Expand Down
Loading
Loading