Skip to content

platform-wallet: orphan identities cannot be loaded — persistor writes them, but no consumer-side reader #3745

@Claudius-Maginificent

Description

@Claudius-Maginificent

Statement

With current code on the platform-wallet PR stack (verified on fix/3625-thepastaclaw-hardening), it is not possible to load identities that don't belong to any wallet — even though the persistor schema allows writing them.

Out-of-wallet ("orphan" / observed-only) identities can be stored to the SQLite persister with wallet_id = NULL (V002 schema permits this by design — CODE-002), but they are silently dropped on startup: no consumer-side reader exists to bring them back into the in-memory IdentityManager.

Evidence

1. ClientStartState has no orphan-identities slot

packages/rs-platform-wallet/src/changeset/client_start_state.rs:26-40

pub struct ClientStartState {
    pub platform_addresses: BTreeMap<WalletId, PlatformAddressSyncStartState>,
    pub wallets: BTreeMap<WalletId, ClientWalletStartState>,
    #[cfg(feature = "shielded")]
    pub shielded: ShieldedSyncStartState,
}

No top-level BTreeMap<Identifier, ManagedIdentity> or equivalent orphan collection.

2. load.rs destructure ignores everything else

packages/rs-platform-wallet/src/manager/load.rs:38-48

let ClientStartState {
    platform_addresses,
    wallets,
    #[cfg(feature = "shielded")]
    shielded,
} = self.persister.load()?;

3. IdentityManager has an orphan bucket — but only per-wallet

packages/rs-platform-wallet/src/wallet/identity/state/manager/mod.rs:69-98

pub struct IdentityManager {
    pub out_of_wallet_identities: BTreeMap<Identifier, ManagedIdentity>,
    pub wallet_identities: BTreeMap<WalletId, BTreeMap<RegistrationIndex, ManagedIdentity>>,
    location_index: BTreeMap<Identifier, IdentityLocation>,
}

IdentityManager instances are constructed inside ClientWalletStartState, scoped to a single wallet — there is no client-level IdentityManager that owns orphans across wallets.

4. Persister load() does not load identities at all

packages/rs-platform-wallet-storage/src/sqlite/persister.rs:28, 1001-1028

pub(crate) const LOAD_UNIMPLEMENTED: &[&str] = &["ClientStartState::wallets"];

fn load(&self) -> Result<ClientStartState, PersistenceError> {
    let mut state = ClientStartState::default();
    state.platform_addresses = schema::platform_addrs::load_all(&conn)?;
    // wallets left empty — rehydrated per-wallet later
    Ok(state)
}

5. Per-wallet schema reader explicitly excludes orphans

packages/rs-platform-wallet-storage/src/sqlite/schema/identities.rs:104-142

// V002: wallet_id is nullable on identities; this load path still
// wants only the rows belonging to the wallet the caller asked
// for, so the WHERE clause matches by wallet_id (orphan identities
// — wallet_id NULL — are out of scope for this per-wallet loader).
let mut stmt = conn.prepare(
    "SELECT identity_id, entry_blob, tombstoned FROM identities WHERE wallet_id = ?1",
)?;

No sibling load_orphans() / load_out_of_wallet() reader exists.

Root cause

Two-level loader gap. The current load contract is:

  1. persister.load() → reconstructs ClientStartState (platform addresses only today; wallets deferred).
  2. Per-wallet register_wallet() → calls schema::identities::load_state(conn, wallet_id) filtering WHERE wallet_id = ?1.

There is no third path that loads identities where wallet_id IS NULL. Even though V002 schema permits orphan rows and the in-memory IdentityManager has an out_of_wallet_identities bucket, the bucket is always empty post-load.

What would be needed

  • Add a top-level orphan slot to ClientStartState — e.g. pub out_of_wallet_identities: BTreeMap<Identifier, ManagedIdentity> — OR introduce a client-level IdentityManager separate from the per-wallet ones.
  • Add a persister reader for orphans: SELECT ... FROM identities WHERE wallet_id IS NULL — either as a new method on the schema module or by extending load().
  • Update manager::load::load_from_persistor to destructure and consume the new slot, populating the runtime IdentityManager with orphans.
  • Decide ownership model: should the client-level orphan collection be a single global IdentityManager, or should each per-wallet manager hold a view into the global orphans?
  • Add round-trip tests (orphan stored → restart → orphan present and usable).

Scope notes

Related

  • PR feat(platform-wallet): add platform-wallet-storage crate (sqlite persister) #3625 (sqlite persister) — V002 schema enables orphan storage; this issue is the consumer-side follow-up.
  • CODE-002 — original triage finding that motivated V002 cascade-through-identities.
  • PROJ-001 (this PR) — FFI register_identity now requires wallet_id at the boundary; once this issue is fixed, a sibling FFI entry point for orphan registration may also be wanted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions