Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
20b20e7
fix(platform-wallet-storage): rehydrate core_derived_addresses from p…
lklimek Jun 9, 2026
4f432c9
fix(platform-wallet-storage): repair partial-state derived-address re…
lklimek Jun 9, 2026
67d0eba
fix(platform-wallet-storage): harden core_derived_addresses with BIP3…
lklimek Jun 10, 2026
ff1208a
test(platform-wallet-storage): close core_derived_addresses coverage …
lklimek Jun 10, 2026
d8d2239
fix(platform-wallet-storage): fail loud when a pool-declared address …
lklimek Jun 11, 2026
cccd217
test(platform-wallet-storage): assert resolved account_index of the s…
lklimek Jun 11, 2026
63fea7a
fix(platform-wallet): emit in-band pool snapshot on derivation so acc…
lklimek Jun 11, 2026
f7b6136
test(platform-wallet-storage): prove in-band pool-snapshot resolution…
lklimek Jun 11, 2026
ebb4b30
refactor(platform-wallet-storage): replace pool mirror/reconcile with…
lklimek Jun 11, 2026
925dfcb
Merge branch 'merge/wallet-rehydration' into merge/wallet-core-derived
lklimek Jun 15, 2026
156bd98
fix(platform-wallet-storage): add account_index to core_derived_addre…
lklimek Jun 16, 2026
925b109
fix(platform-wallet-storage): repair label-split fallout in pool_type…
lklimek Jun 16, 2026
b450649
fix(platform-wallet): add background_generation guard to PlatformAddr…
lklimek Jun 18, 2026
1f3ea29
fix(platform-wallet): close shielded_sync generation-guard TOCTOU (lo…
lklimek Jun 18, 2026
fa4584d
docs(platform-wallet-storage): update stale doc comment on ACCOUNT_IN…
lklimek Jun 22, 2026
aed5652
docs(platform-wallet): correct CHECK-column count and port generation…
lklimek Jun 22, 2026
ca68690
test(platform-wallet): cover generation-guard restart and contacts.st…
lklimek Jun 22, 2026
8d8724e
refactor(platform-wallet)!: hardcode core UTXO account_index=0; retir…
lklimek Jun 22, 2026
e8308ed
fix(platform-wallet): persist non-default-account UTXOs under index 0…
lklimek Jun 22, 2026
ea0082e
docs(platform-wallet-storage): drop deleted-table refs from accounts.…
lklimek Jun 22, 2026
4e4b38c
docs(platform-wallet-storage): sync SCHEMA.md and stale code comments…
lklimek Jun 22, 2026
e553015
fix(platform-wallet): extend AddressPool depth on rehydration to cove…
lklimek Jun 22, 2026
bef9bda
fix(platform-wallet): bound rehydration pool-extension per-chain and …
lklimek Jun 22, 2026
4f83a07
docs(platform-wallet): document apply_persisted_core_state manifest p…
lklimek Jun 22, 2026
bde7060
Merge branch 'mb/cascade-work' into mb/cascade-3828
lklimek Jun 23, 2026
eb59f7c
fix(rs-platform-wallet): harden core-state rehydration + tidy storage…
lklimek Jun 23, 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
78 changes: 19 additions & 59 deletions packages/rs-platform-wallet-storage/SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ chain.
## What it stores — and the boundary

The persister stores **public** wallet-state material (UTXOs, transactions,
account registrations, address pools, identities, identity public keys,
contacts, asset locks, token balances, DashPay overlays, and
platform-address sync snapshots) in a SQLite database managed by
account registrations, identities, identity public keys, contacts, asset
locks, token balances, DashPay overlays, and platform-address sync
snapshots) in a SQLite database managed by
[refinery](https://crates.io/crates/refinery) migrations.

**No secrets are stored here.** Mnemonics, seeds, and raw private keys never
Expand All @@ -37,20 +37,18 @@ Any `meta_*` row whose parent object does not exist — because it was never cre

A future garbage-collection pass is expected to reap orphan metadata — rows with no live parent object older than approximately one week — but no such GC is implemented yet. Callers should not rely on orphan metadata persisting forever, nor assume it will be cleaned up promptly. `meta_global` is intentionally parentless and always survives.

The 23 tables are split into five domain diagrams below. `WALLETS` is the root anchor and appears in each diagram. For full column listings see the [Tables](#tables) section.
The 21 tables are split into five domain diagrams below. `WALLETS` is the root anchor and appears in each diagram. For full column listings see the [Tables](#tables) section.

## Diagram 1 — Core / L1 (Bitcoin/Dash layer)

Account registrations, address-pool snapshots, transactions, UTXOs, instant locks, derived addresses, and SPV sync state.
Account registrations, transactions, UTXOs, instant locks, and SPV sync state.

```mermaid
erDiagram
WALLETS ||--o{ ACCOUNT_REGISTRATIONS : "registers"
WALLETS ||--o{ ACCOUNT_ADDRESS_POOLS : "snapshots"
WALLETS ||--o{ CORE_TRANSACTIONS : "records"
WALLETS ||--o{ CORE_UTXOS : "owns"
WALLETS ||--o{ CORE_INSTANT_LOCKS : "holds"
WALLETS ||--o{ CORE_DERIVED_ADDRESSES : "derives"
WALLETS ||--o| CORE_SYNC_STATE : "tracks"
CORE_TRANSACTIONS ||--o{ CORE_UTXOS : "spends"

Expand All @@ -62,19 +60,11 @@ erDiagram

ACCOUNT_REGISTRATIONS {
BLOB wallet_id PK
TEXT account_type PK "standard | coinjoin | identity_registration | ..."
TEXT account_type PK "standard_bip44 | standard_bip32 | coinjoin | identity_registration | ..."
INTEGER account_index PK
BLOB account_xpub_bytes "bincode-encoded AccountRegistrationEntry"
}

ACCOUNT_ADDRESS_POOLS {
BLOB wallet_id PK
TEXT account_type PK
INTEGER account_index PK
TEXT pool_type PK "external | internal | absent | absent_hardened"
BLOB snapshot_blob "bincode-encoded AccountAddressPoolEntry"
}

CORE_TRANSACTIONS {
BLOB wallet_id PK
BLOB txid PK "32-byte Txid"
Expand Down Expand Up @@ -102,15 +92,6 @@ erDiagram
BLOB islock_blob "bincode-encoded InstantLock"
}

CORE_DERIVED_ADDRESSES {
BLOB wallet_id PK
TEXT account_type PK
TEXT address PK "bech32 / Base58 address string"
INTEGER account_index
TEXT derivation_path "pool_type/derivation_index"
INTEGER used "0 | 1"
}

CORE_SYNC_STATE {
BLOB wallet_id PK "one row per wallet"
INTEGER last_processed_height "NULL until first block processed"
Expand Down Expand Up @@ -364,14 +345,6 @@ lookups without blob decoding.
- PK: `(wallet_id, account_type, account_index)`.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.

### `account_address_pools`

Address-pool snapshot per `(wallet, account, pool_type)`. `pool_type` is
one of `external`, `internal`, `absent`, `absent_hardened`.

- PK: `(wallet_id, account_type, account_index, pool_type)`.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.

### `core_transactions`

One row per transaction the wallet has seen. `height`, `block_hash`, and
Expand Down Expand Up @@ -401,15 +374,6 @@ finalized. Rows are removed when the transaction becomes confirmed.
- PK: `(wallet_id, txid)`.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.

### `core_derived_addresses`

Address-to-account-index map. Written before UTXOs in the same
transaction so the UTXO writer can resolve `account_index` by address.

- PK: `(wallet_id, account_type, address)`.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.
- Index: `idx_core_derived_addresses_addr(wallet_id, address)`.

### `core_sync_state`

One row per wallet, holding monotonically-advancing SPV sync watermarks.
Expand Down Expand Up @@ -590,27 +554,24 @@ before the address exists.

## Enum-domain CHECK constraints

Seven TEXT columns carry a `CHECK (col IN (...))` across five enum
domains — `account_type` is reused in three tables. The IN-list is built
at migration time from `pub(crate) const *_LABELS` arrays declared next
to each writer function. Four domains mirror an upstream Rust enum; the
fifth (`contacts.state`) is a synthetic lifecycle label naming which
`ContactChangeSet` slot a row came from:
Four TEXT columns carry a `CHECK (col IN (...))` across four enum
domains. The IN-list is built at migration time from
`pub(crate) const *_LABELS` arrays declared next to each writer function.
Three domains mirror an upstream Rust enum; the fourth (`contacts.state`)
is a synthetic lifecycle label naming which `ContactChangeSet` slot a row
came from:

| Table | Column | Source-of-truth const |
|---|---|---|
| `wallets` | `network` | `sqlite::schema::wallets::NETWORK_LABELS` |
| `account_registrations` | `account_type` | `sqlite::schema::accounts::ACCOUNT_TYPE_LABELS` |
| `account_address_pools` | `account_type` | `sqlite::schema::accounts::ACCOUNT_TYPE_LABELS` |
| `account_address_pools` | `pool_type` | `sqlite::schema::accounts::POOL_TYPE_LABELS` |
| `core_derived_addresses` | `account_type` | `sqlite::schema::accounts::ACCOUNT_TYPE_LABELS` |
| `asset_locks` | `status` | `sqlite::schema::asset_locks::ASSET_LOCK_STATUS_LABELS` |
| `contacts` | `state` | `sqlite::schema::contacts::CONTACT_STATE_LABELS` |

The const arrays are the single source of truth shared by the writer
mapping functions (`network_to_str`, `account_type_db_label`,
`pool_type_db_label`, `status_str`, `contact_state_db_label`) and the
migration's CHECK clauses.
`status_str`, `contact_state_db_label`) and the migration's CHECK
clauses.
Per-module `*_labels_match_enum` unit tests enforce set-equality
between each const and the writer's codomain — drift (a renamed/added
upstream variant) fails the test rather than landing as silent garbage
Expand All @@ -619,10 +580,9 @@ in this document; the source files are canonical.

### Upstream-enum coupling

Three of the persisted enums live in the external `rust-dashcore`
crate (`key_wallet::Network`, `key_wallet::account::AccountType`,
`key_wallet::managed_account::address_pool::AddressPoolType`); the
fourth (`platform_wallet::wallet::asset_lock::tracked::AssetLockStatus`)
Two of the persisted enums live in the external `rust-dashcore`
crate (`key_wallet::Network`, `key_wallet::account::AccountType`); the
third (`platform_wallet::wallet::asset_lock::tracked::AssetLockStatus`)
is in-tree and carries a `# Schema coupling` rustdoc block.

Because the upstream definitions cannot be edited from this repository,
Expand All @@ -639,7 +599,7 @@ mechanisms working together:

TODO(rust-dashcore): once the upstream `key_wallet` crate is vendored
or the project gains push access there, mirror the in-tree
`AssetLockStatus` `# Schema coupling` doc block on the three upstream
`AssetLockStatus` `# Schema coupling` doc block on the two upstream
enums so a developer editing them upstream sees the constraint without
having to grep this repo.

Expand Down Expand Up @@ -683,4 +643,4 @@ having to grep this repo.

| Version | File | Description |
|---|---|---|
| V001 | `V001__initial.rs` | Full schema: all 23 tables (including the six `meta_*` per-object metadata tables), every index, and six triggers (`setnull_core_utxos_on_tx_delete` + the five `meta_*` soft-cascade triggers) |
| V001 | `V001__initial.rs` | Full schema: all 21 tables (including the six `meta_*` per-object metadata tables), every index, and six triggers (`setnull_core_utxos_on_tx_delete` + the five `meta_*` soft-cascade triggers) |
28 changes: 2 additions & 26 deletions packages/rs-platform-wallet-storage/migrations/V001__initial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
//! read back) at every connection open via `open_conn`
//! (`src/sqlite/conn.rs`).
//!
//! Enum-shaped TEXT columns (`network`, `account_type`, `pool_type`,
//! `status`, `state`) carry a `CHECK (col IN (...))` clause whose
//! Enum-shaped TEXT columns (`network`, `account_type`, `status`,
//! `state`) carry a `CHECK (col IN (...))` clause whose
//! IN-list is built from the `*_LABELS` const arrays in
//! `crate::sqlite::schema::{wallets, accounts, asset_locks,
//! contacts}`. The consts are the single source of truth shared with
Expand All @@ -51,7 +51,6 @@ pub fn migration() -> String {
let network_check = build_check_in(crate::sqlite::schema::wallets::NETWORK_LABELS);
Comment thread
lklimek marked this conversation as resolved.
Comment thread
Claudius-Maginificent marked this conversation as resolved.
let account_type_check =
build_check_in(crate::sqlite::schema::accounts::ACCOUNT_TYPE_LABELS);
let pool_type_check = build_check_in(crate::sqlite::schema::accounts::POOL_TYPE_LABELS);
let asset_lock_status_check =
build_check_in(crate::sqlite::schema::asset_locks::ASSET_LOCK_STATUS_LABELS);
let contact_state_check =
Expand Down Expand Up @@ -82,16 +81,6 @@ CREATE TABLE account_registrations (
FOREIGN KEY (wallet_id) REFERENCES wallets(wallet_id) ON DELETE CASCADE
);

CREATE TABLE account_address_pools (
wallet_id BLOB NOT NULL,
account_type TEXT NOT NULL CHECK (account_type IN {account_type_check}),
account_index INTEGER NOT NULL,
pool_type TEXT NOT NULL CHECK (pool_type IN {pool_type_check}),
snapshot_blob BLOB NOT NULL,
PRIMARY KEY (wallet_id, account_type, account_index, pool_type),
FOREIGN KEY (wallet_id) REFERENCES wallets(wallet_id) ON DELETE CASCADE
);

CREATE TABLE core_transactions (
wallet_id BLOB NOT NULL,
txid BLOB NOT NULL,
Expand Down Expand Up @@ -143,19 +132,6 @@ CREATE TABLE core_instant_locks (
FOREIGN KEY (wallet_id) REFERENCES wallets(wallet_id) ON DELETE CASCADE
);

CREATE TABLE core_derived_addresses (
wallet_id BLOB NOT NULL,
account_type TEXT NOT NULL CHECK (account_type IN {account_type_check}),
account_index INTEGER NOT NULL,
address TEXT NOT NULL,
derivation_path TEXT NOT NULL,
used INTEGER NOT NULL,
PRIMARY KEY (wallet_id, account_type, address),
FOREIGN KEY (wallet_id) REFERENCES wallets(wallet_id) ON DELETE CASCADE
);

CREATE INDEX idx_core_derived_addresses_addr ON core_derived_addresses(wallet_id, address);

CREATE TABLE core_sync_state (
wallet_id BLOB NOT NULL PRIMARY KEY,
last_processed_height INTEGER,
Expand Down
9 changes: 0 additions & 9 deletions packages/rs-platform-wallet-storage/src/sqlite/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,6 @@ pub enum WalletStorageError {
limit_bytes: usize,
},

/// An unspent UTXO named an address absent from
/// `core_derived_addresses`, so its account index can't be resolved;
/// persisting it would mis-file live funds, so the write is refused.
/// Spent-only placeholder rows tolerate a missing mapping.
#[error("unspent utxo address {address} is not in core_derived_addresses")]
UtxoAddressNotDerived { address: String },

/// `PRAGMA foreign_keys = ON` was issued on open but the read-back
/// reported the constraint enforcement is still off — the linked
/// SQLite build silently ignores the pragma (no FK support compiled
Expand Down Expand Up @@ -372,7 +365,6 @@ impl WalletStorageError {
| Self::IdentityEntryIdMismatch
| Self::AssetLockEntryMismatch { .. }
| Self::BlobTooLarge { .. }
| Self::UtxoAddressNotDerived { .. }
| Self::IntegerOverflow { .. } => false,
}
}
Expand Down Expand Up @@ -449,7 +441,6 @@ impl WalletStorageError {
Self::IdentityEntryIdMismatch => "identity_entry_id_mismatch",
Self::AssetLockEntryMismatch { .. } => "asset_lock_entry_mismatch",
Self::BlobTooLarge { .. } => "blob_too_large",
Self::UtxoAddressNotDerived { .. } => "utxo_address_not_derived",
Self::IntegerOverflow { .. } => "integer_overflow",
}
}
Expand Down
7 changes: 4 additions & 3 deletions packages/rs-platform-wallet-storage/src/sqlite/persister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,9 +1053,10 @@ fn apply_changeset_to_tx(
if !cs.account_registrations.is_empty() {
schema::accounts::apply_registrations(tx, wallet_id, &cs.account_registrations)?;
}
if !cs.account_address_pools.is_empty() {
schema::accounts::apply_pools(tx, wallet_id, &cs.account_address_pools)?;
}
// `account_address_pools` is intentionally NOT applied: UTXO attribution
// is hardcoded to the default account (index 0) in `core_state`, so the
// pool snapshot is no longer a storage input. The changeset field is kept
// for API stability and still feeds non-storage consumers.
if let Some(core) = cs.core.as_ref() {
schema::core_state::apply(tx, wallet_id, core)?;
}
Expand Down
Loading
Loading