Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
53 changes: 45 additions & 8 deletions packages/rs-platform-wallet-storage/SCHEMA.md
Original file line number Diff line number Diff line change
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"
TEXT pool_type PK "external | internal | absent | absent_hardened"
INTEGER derivation_index PK
INTEGER account_index "account-level context; the value the read returns"
TEXT address UK "bech32 / Base58 address string"
INTEGER used "0 | 1"
}

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, 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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,20 @@ 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,
pool_type TEXT NOT NULL CHECK (pool_type IN {pool_type_check}),
derivation_index INTEGER NOT NULL,
address TEXT NOT NULL,
derivation_path TEXT NOT NULL,
used INTEGER NOT NULL,
PRIMARY KEY (wallet_id, account_type, address),
-- PK is the BIP32 leaf identity. `address` is a derived attribute, not
-- a key, so every collision (within- or cross-pool) trips
-- UNIQUE(address) loud. `account_index` is account-level context (the
-- value the read returns), not a uniqueness discriminator. The UNIQUE
-- index also backs ACCOUNT_INDEX_BY_ADDRESS_SQL.
PRIMARY KEY (wallet_id, account_type, pool_type, derivation_index),
UNIQUE (wallet_id, address),
FOREIGN KEY (wallet_id) REFERENCES wallets(wallet_id) ON DELETE CASCADE
);
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated
Comment thread
Claudius-Maginificent marked this conversation as resolved.
Outdated

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
23 changes: 20 additions & 3 deletions packages/rs-platform-wallet-storage/src/sqlite/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,27 @@ pub enum WalletStorageError {
},

/// 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.
/// `core_derived_addresses`, so its account index can't be resolved.
/// Retained as a fatal-classified typed marker; the apply path no
/// longer raises it — it skips such a UTXO (logged) so one
/// unresolvable row never aborts a whole flush, and the balance
/// re-warms when the address later derives.
#[error("unspent utxo address {address} is not in core_derived_addresses")]
UtxoAddressNotDerived { address: String },

/// A live `addresses_derived` entry arrived without its address in the
/// wallet's `account_address_pools` manifest. The emitter must attach a
/// full pool snapshot in-band with every derivation, so a derived
/// address absent from the manifest means the emitter contract is
/// broken — a logic regression, not a benign SPV gap. Failing loud at
/// the storage trust boundary surfaces it instead of persisting a row
/// the manifest can't vouch for.
#[error(
"emitter contract violated: derived address {address} is absent from the \
account_address_pools manifest (pool snapshot not emitted in-band)"
)]
DerivedIndexInvariantViolated { 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 @@ -373,6 +388,7 @@ impl WalletStorageError {
| Self::AssetLockEntryMismatch { .. }
| Self::BlobTooLarge { .. }
| Self::UtxoAddressNotDerived { .. }
| Self::DerivedIndexInvariantViolated { .. }
| Self::IntegerOverflow { .. } => false,
}
}
Expand Down Expand Up @@ -450,6 +466,7 @@ impl WalletStorageError {
Self::AssetLockEntryMismatch { .. } => "asset_lock_entry_mismatch",
Self::BlobTooLarge { .. } => "blob_too_large",
Self::UtxoAddressNotDerived { .. } => "utxo_address_not_derived",
Self::DerivedIndexInvariantViolated { .. } => "derived_index_invariant_violated",
Self::IntegerOverflow { .. } => "integer_overflow",
}
}
Expand Down
Loading
Loading