TL;DR
platform-wallet's "unowned identity" storage silently drops an identity's public keys when it's added, but the docs on the read side promise the opposite — that a loaded identity is "usable without a second call." A consumer that trusts that doc for an out-of-wallet identity gets a silent, empty key map instead of an error.
User story
As a downstream consumer of platform-wallet/platform-wallet-storage (e.g. dash-evo-tool, storing masternode/evonode identities it doesn't own a wallet for), I want the out-of-wallet identity API to either persist the keys I pass it or clearly document that it won't, so that I don't build a feature on a doc promise the storage layer doesn't keep.
Scenario
Actual behavior: IdentityManager::add_out_of_wallet_identity takes a full dpp::identity::Identity (public keys included) but converts it via ManagedIdentity::new_out_of_wallet(identity), which — per its own doc comment — produces "ManagedIdentity rows with wallet_id == None and an empty public-key map." Unlike the wallet-owned path (add_identity), which explicitly calls managed_identity.keys_snapshot_changeset() and persists an identity_keys changeset alongside the identity, add_out_of_wallet_identity persists only an IdentityChangeSet — no keys changeset at all, ever.
Meanwhile, SqlitePersister::load_unowned_identities's doc says: "Keys are folded in exactly as load() does for wallet-owned identities, so a returned ManagedIdentity is usable without a second call." That's accurate about the fold mechanism, but it creates a false impression for any row that entered through add_out_of_wallet_identity — there was never anything to fold in, so the "usable" ManagedIdentity comes back with a permanently empty key map, silently.
The gap is invisible from the writer's side too: nothing in add_out_of_wallet_identity or its callers is told the keys it was handed were discarded.
Expected behavior: One of:
add_out_of_wallet_identity persists the identity's public keys the same way add_identity does (call keys_snapshot_changeset() and include it in the stored changeset) — the natural fix, since the caller already has the keys in hand; or
- If keeping the out-of-wallet bucket key-less is intentional (e.g. because upstream write paths for this bucket are meant to be read-only/observed identities without signing capability), say so explicitly on
add_out_of_wallet_identity itself, and update load_unowned_identities's doc to not claim the loaded identity is "usable without a second call" when it never held keys in the first place.
Detailed discussion
Found while reviewing dash-evo-tool PR #955, which uses add_out_of_wallet_identity to mirror masternode/evonode identities (identities DET has no owning wallet for, but for which it does hold real keys — e.g. the masternode voting key — in its own vault) into the upstream store so they survive wallet-removal cascades and boot reconciliation can find them. DET consumes only the identity IDs from the mirror today (a diff against its own local state), so this gap is currently inert for that specific caller — but it's a trap for the next consumer who reads a mirrored identity's public_keys and trusts the load-path doc.
Relevant code (as of 4784de03):
rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs — add_identity (persists keys via keys_snapshot_changeset()) vs. add_out_of_wallet_identity (does not).
rs-platform-wallet/src/wallet/identity/state/managed_identity/identity_ops.rs:52 — keys_snapshot_changeset is pub(crate), so an external consumer can't call it to compensate on its own side even if it wanted to.
rs-platform-wallet-storage/src/sqlite/persister.rs:575 — the load_unowned_identities doc comment making the "usable without a second call" claim.
Option 1 above (persist the keys) seems like the more consistent fix given add_identity's own precedent, but I don't have visibility into whether the out-of-wallet bucket was deliberately designed to stay signing-incapable (its own doc separately describes it as "observed read-only," which DET's actual usage — a device that can sign with some of these identities — doesn't quite match either; that's a separate discussion for dash-evo-tool, not filed here). Flagging both options in case the read-only framing is intentional and only the doc needs fixing.
🤖 Co-authored by Claudius the Magnificent AI Agent
TL;DR
platform-wallet's "unowned identity" storage silently drops an identity's public keys when it's added, but the docs on the read side promise the opposite — that a loaded identity is "usable without a second call." A consumer that trusts that doc for an out-of-wallet identity gets a silent, empty key map instead of an error.User story
As a downstream consumer of
platform-wallet/platform-wallet-storage(e.g.dash-evo-tool, storing masternode/evonode identities it doesn't own a wallet for), I want the out-of-wallet identity API to either persist the keys I pass it or clearly document that it won't, so that I don't build a feature on a doc promise the storage layer doesn't keep.Scenario
Actual behavior:
IdentityManager::add_out_of_wallet_identitytakes a fulldpp::identity::Identity(public keys included) but converts it viaManagedIdentity::new_out_of_wallet(identity), which — per its own doc comment — produces "ManagedIdentityrows withwallet_id == Noneand an empty public-key map." Unlike the wallet-owned path (add_identity), which explicitly callsmanaged_identity.keys_snapshot_changeset()and persists anidentity_keyschangeset alongside the identity,add_out_of_wallet_identitypersists only anIdentityChangeSet— no keys changeset at all, ever.Meanwhile,
SqlitePersister::load_unowned_identities's doc says: "Keys are folded in exactly asload()does for wallet-owned identities, so a returnedManagedIdentityis usable without a second call." That's accurate about the fold mechanism, but it creates a false impression for any row that entered throughadd_out_of_wallet_identity— there was never anything to fold in, so the "usable"ManagedIdentitycomes back with a permanently empty key map, silently.The gap is invisible from the writer's side too: nothing in
add_out_of_wallet_identityor its callers is told the keys it was handed were discarded.Expected behavior: One of:
add_out_of_wallet_identitypersists the identity's public keys the same wayadd_identitydoes (callkeys_snapshot_changeset()and include it in the stored changeset) — the natural fix, since the caller already has the keys in hand; oradd_out_of_wallet_identityitself, and updateload_unowned_identities's doc to not claim the loaded identity is "usable without a second call" when it never held keys in the first place.Detailed discussion
Found while reviewing
dash-evo-toolPR #955, which usesadd_out_of_wallet_identityto mirror masternode/evonode identities (identities DET has no owning wallet for, but for which it does hold real keys — e.g. the masternode voting key — in its own vault) into the upstream store so they survive wallet-removal cascades and boot reconciliation can find them. DET consumes only the identity IDs from the mirror today (a diff against its own local state), so this gap is currently inert for that specific caller — but it's a trap for the next consumer who reads a mirrored identity'spublic_keysand trusts the load-path doc.Relevant code (as of
4784de03):rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs—add_identity(persists keys viakeys_snapshot_changeset()) vs.add_out_of_wallet_identity(does not).rs-platform-wallet/src/wallet/identity/state/managed_identity/identity_ops.rs:52—keys_snapshot_changesetispub(crate), so an external consumer can't call it to compensate on its own side even if it wanted to.rs-platform-wallet-storage/src/sqlite/persister.rs:575— theload_unowned_identitiesdoc comment making the "usable without a second call" claim.Option 1 above (persist the keys) seems like the more consistent fix given
add_identity's own precedent, but I don't have visibility into whether the out-of-wallet bucket was deliberately designed to stay signing-incapable (its own doc separately describes it as "observed read-only," which DET's actual usage — a device that can sign with some of these identities — doesn't quite match either; that's a separate discussion fordash-evo-tool, not filed here). Flagging both options in case the read-only framing is intentional and only the doc needs fixing.🤖 Co-authored by Claudius the Magnificent AI Agent