Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
28 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
e10f91d
fix(platform-wallet): report unspent UTXO count in RehydrationTopolog…
lklimek Jun 24, 2026
5367471
feat(platform-wallet)!: layered opt-in secret protection (Tier-2 pass…
Claudius-Maginificent Jun 26, 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
57 changes: 47 additions & 10 deletions packages/rs-platform-wallet-storage/SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ 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"
}
Expand Down Expand Up @@ -105,9 +105,10 @@ erDiagram
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 account_index PK "owning account; also the value the read returns"
TEXT pool_type PK "external | internal | absent | absent_hardened"
INTEGER derivation_index PK
TEXT address UK "bech32 / Base58 address string"
INTEGER used "0 | 1"
}
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -403,12 +404,47 @@ finalized. Rows are removed when the transaction becomes confirmed.

### `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)`.
A live-fed indexed read-cache the UTXO writer joins to resolve a UTXO's
`account_index` by address. The authoritative manifest is
`account_address_pools` (kept complete and in-band by the
`core_bridge` emitter); this table is the fast B-tree probe in front of it.
Fed by exactly one source:

- **Live `addresses_derived` events** — written before UTXOs in the same
transaction so the writer sees fresh rows.

UTXO resolution for an unspent UTXO:

1. **Cache hit** — resolve from this table.
2. **Cache miss, manifest hit** — fall back to `account_address_pools`
(the in-band snapshot is applied earlier in the same tx). Resolved.
3. **Miss in both** — a genuinely undeclared address (not ours, or an SPV
gap-limit edge). The writer **skips** it (with a `warn`) so one
unresolvable row never aborts a whole flush; its balance re-warms once
the address is later derived.

(The spent-only synthetic-row path is exempt: a spent row uses an inert
`account_index` placeholder and is excluded from reads.)

A live `addresses_derived` entry whose address is absent from the manifest
is a **fatal** `DerivedIndexInvariantViolated` — the emitter must attach
the pool snapshot in-band with every derivation, so this can only fire on
an emitter bug, never on a benign gap.

> The non-ECDSA pool gap (BLS/EdDSA addresses are dropped from the event
> projection, so they never produce an `addresses_derived` entry) cannot
> manifest here: only ECDSA Standard/CoinJoin External/Internal addresses
> are ever classified `Received`/`Change`, so a non-ECDSA address can never
> be a `new_utxos` UTXO address. This is an upstream classifier property
> (`key-wallet` `account_checker`), not enforceable at the storage layer.

- PK: `(wallet_id, account_type, account_index, pool_type, derivation_index)` — the BIP32
leaf identity (one row per derived address).
- `UNIQUE(wallet_id, address)` — the read-index invariant (one
account_index per address); its index also backs the address lookup, so
no separate index is needed. `address` is a derived attribute, never a
key, so every collision surfaces loud.
- FK: `wallet_id → wallets(wallet_id) ON DELETE CASCADE`.
- Index: `idx_core_derived_addresses_addr(wallet_id, address)`.

### `core_sync_state`

Expand Down Expand Up @@ -590,7 +626,7 @@ before the address exists.

## Enum-domain CHECK constraints

Seven TEXT columns carry a `CHECK (col IN (...))` across five enum
Eight 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
Expand All @@ -604,6 +640,7 @@ fifth (`contacts.state`) is a synthetic lifecycle label naming which
| `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` |
| `core_derived_addresses` | `pool_type` | `sqlite::schema::accounts::POOL_TYPE_LABELS` |
| `asset_locks` | `status` | `sqlite::schema::asset_locks::ASSET_LOCK_STATUS_LABELS` |
| `contacts` | `state` | `sqlite::schema::contacts::CONTACT_STATE_LABELS` |

Expand Down
24 changes: 0 additions & 24 deletions packages/rs-platform-wallet-storage/migrations/V001__initial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);

Comment thread
lklimek marked this conversation as resolved.
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