Skip to content

feat(platform-wallet): add contacts and identity-key rehydration (item G)#3693

Draft
Claudius-Maginificent wants to merge 6 commits into
feat/platform-wallet-rehydrationfrom
feat/platform-wallet-rehydration-contacts
Draft

feat(platform-wallet): add contacts and identity-key rehydration (item G)#3693
Claudius-Maginificent wants to merge 6 commits into
feat/platform-wallet-rehydrationfrom
feat/platform-wallet-rehydration-contacts

Conversation

@Claudius-Maginificent
Copy link
Copy Markdown
Collaborator

@Claudius-Maginificent Claudius-Maginificent commented May 20, 2026

STACKED PR — review diff against feat/platform-wallet-rehydration (PR-2), not v3.1-dev.

Stack: #3625 (feat/platform-wallet-sqlite-persistor) ← #3672 (feat/platform-wallet-storage-secrets) ← PR-2 (feat/platform-wallet-rehydration) ← this PR (feat/platform-wallet-rehydration-contacts)

Merge order: #3625#3672 → PR-2 → this.


Issue being fixed or feature implemented

After PR-2 landed full signing-wallet rehydration ((seed + persistor) → Wallet), the two PUBLIC-material slots — DashPay contacts and identity public keys — were still deferred: ClientWalletStartState had no fields for them, and LOAD_UNIMPLEMENTED listed both. This is item G from the rehydration plan. A freshly rehydrated wallet would therefore have empty identity public_keys maps and no contact state until the next sync cycle, even though the data was already on disk and readers existed for contacts.

This PR closes that gap: contacts and identity public keys now rehydrate on load() with no additional sync required.

What was done?

Changeset-shape extension (rs-platform-wallet)ClientWalletStartState gains two new public fields:

  • contacts: ContactChangeSet — persisted sent/received/established DashPay contact state. Public material only; removed_* fields are always empty on the rehydration feed (deletes never reach storage as rows).
  • identity_keys: IdentityKeysChangeSet — persisted per-identity public-key entries. No private key material; removed is always empty on the rehydration feed.

Both fields have Default::default() fallback so in-tree consumers compile without modification.

New keyless reader (rs-platform-wallet-storage)schema::identity_keys::load_state reads all public-key rows for a given wallet and returns an IdentityKeysChangeSet. The existing contacts::load_state reader was already sufficient; it is now wired into load().

SqlitePersister::load() wiring — both readers are called per-wallet and their results placed into the new ClientWalletStartState fields. LOAD_UNIMPLEMENTED shrinks from ["contacts", "identity_keys", "core::last_applied_chain_lock"] to ["core::last_applied_chain_lock"].

DRY refactor — IdentityManager::apply_contacts_and_keys — a new shared method on IdentityManager is now the single source of truth for routing contact entries and identity-key entries onto managed identities. It replaces an approximately 88-line inline block that existed in PlatformWalletInfo::apply_changeset (the runtime changeset-replay path) and is called from both that path and the new persister rehydration path in load_from_persistor. Identity keys are applied before contacts so a contact entry never lands before its owner's keys; orphan entries are logged and skipped, never fatal.

FFI consumer adaptation (rs-platform-wallet-ffi)build_wallet_start_state sets the two new slots to Default::default(). The iOS path does not use them: identity public keys are already reconstructed directly into Identity.public_keys by build_wallet_identity_bucket (feeding the slot too would double-apply them), and WalletRestoreEntryFFI carries no contact state back from Swift on load. The empty defaults make apply_contacts_and_keys a no-op for the FFI path, preserving established iOS behaviour.

Test updatestc043 is flipped from a deferral assertion to a positive round-trip assertion: it now verifies that a persisted contact request rehydrates into state.wallets[w].contacts.sent_requests and that LOAD_UNIMPLEMENTED lists only the single remaining deferred area. Three new dedicated round-trip tests cover the full G surface:

  • g_rt1_contacts_rehydrate_into_keyless_payload — sent + received requests rehydrate bit-exact; removed_* slots are empty.
  • g_rt2_identity_keys_rehydrate_into_keyless_payload — two identity-key entries round-trip bit-exact; removed is empty.
  • g_rt3_empty_slots_for_bare_wallet — a wallet with no contact or key rows produces empty (not error) slots.

A secrets_scan allow-list entry is added for the SELECT statement in identity_keys::load_state (the word key appears in the column name public_key_hash; no secret material is involved).

No V001 migration required — the identity_keys and contacts_* columns are already in V001. This PR adds no new SQL columns or tables.

Known limitation (documented, tracked as a follow-up)

The FFI consumer (rs-platform-wallet-ffi) projects both new slots as empty. This is intentional and correct for the current iOS integration:

  • Identity public keys: build_wallet_identity_bucket already populates Identity.public_keys directly from the FFI restore payload; feeding identity_keys through apply_contacts_and_keys on top would double-apply and overwrite keys already placed there.
  • Contacts: WalletRestoreEntryFFI carries no contact-request fields across the C ABI — Swift does not send contacts back on load. Surfacing them would require a new FFI struct field and corresponding Swift wiring.

The core non-FFI manager/load path (SqlitePersister + load_from_persistor) fully populates both slots — no data loss for the native Rust path. The FFI follow-up is tracked as a separate task.

How Has This Been Tested?

Gate commands run before submission:

  • cargo check --workspace and cargo check --workspace --all-features — clean.
  • cargo test -p rs-platform-wallet-storage — includes the three new G round-trip tests (g_rt1, g_rt2, g_rt3), the flipped tc043, and the updated secrets_scan/secrets_guard allow-list check.
  • cargo test -p rs-platform-wallet-storage --features secrets — secrets-feature path clean.
  • Doctests for SqlitePersister::load() — green.
  • PR-2 regression suite (RT-2 / RT-W / RT-S / RT-Z / tc_p4_*): 319 tests pass; no regressions introduced.

Internal multi-agent review (security, QA, and consistency passes) completed. CI has not yet been run on this branch.

User story. Imagine you are building a wallet app on top of PlatformWalletManager. Before this PR, calling load_from_persistor after a restart would give you back balances and identity skeletons, but their public_keys maps would be empty and your contacts list would be blank — you would have to wait for a full sync pass before the wallet was usable for DashPay operations. After this PR, both the identity public keys and the contact records come back automatically on restore. The wallet is immediately usable for DashPay without waiting for a sync.

Breaking Changes

None. The two new fields on ClientWalletStartState have Default implementations and are additive; no existing field is removed or renamed. The FFI consumer is adapted in-tree. The IdentityManager::apply_contacts_and_keys method is pub(crate).

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent


Rebase note (2026-05-20): Cascaded onto rebased #3692 tip (b7508a0d47); zero conflicts; 5 commits preserved.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd5acae6-2275-4947-813c-08329ab7dc86

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/platform-wallet-rehydration-contacts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

lklimek and others added 5 commits May 20, 2026 17:20
schema::identity_keys::load_state reads identity_keys rows back into a
keyless IdentityKeysChangeSet keyed by (identity_id, key_id) via the
existing decode_entry (PUBLIC IdentityPublicKey only — no private key
material). Fail-hard on a corrupt blob; empty wallet → empty changeset.
No V001 column missing (verified). RT: sqlite_identity_keys_reader (3).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…ys (G)

Changeset-shape extension (rs-platform-wallet public API):
- ClientWalletStartState gains keyless ContactChangeSet +
  IdentityKeysChangeSet slots (PUBLIC material; removed_* always empty
  on the rehydration feed).
- New IdentityManager::apply_contacts_and_keys — single source of truth
  for the contact/key routing, shared by the runtime changeset-replay
  path (wallet::apply, now DRY-refactored to call it) and the persister
  rehydration path (manager::load, now routes the new slots onto the
  rebuilt managed identities after IdentityManager::from).

26 wallet::apply tests still green (orphan-skip / established-drops-
pending / tombstone / idempotency preserved by the DRY extraction).

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…d() (G)

- New schema::contacts::load_changeset projects the dormant
  ContactsRecords reader into a keyless ContactChangeSet (PUBLIC,
  removed_* empty).
- SqlitePersister::load() now fills ClientWalletStartState.contacts
  (contacts::load_changeset) + .identity_keys (identity_keys::load_state).
- LOAD_UNIMPLEMENTED shrunk to ["core::last_applied_chain_lock"] (the
  only genuinely-deferred area left).
- Flipped tc043 (contacts now rehydrate — positive assertion) + header.
- E-test ClientWalletStartState constructors extended (PR-2 RT-W/S/Z/
  seed-roundtrip + C wiring all still green).

RT (sqlite_contacts_keys_rehydration, 3): contacts (sent+recv) +
identity-keys round-trip bit-exact through load(); bare wallet → empty
slots. secrets_scan green.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…ape (G)

ClientWalletStartState gained contacts + identity_keys slots; the FFI
keyless projection sets both to Default (empty) — the documented
minimal-correct adaptation: the iOS path already reconstructs identity
PUBLIC keys directly into Identity.public_keys via
build_wallet_identity_bucket (feeding the slot too would double-apply),
and WalletRestoreEntryFFI carries no contacts back from Swift on load
(surfacing them needs a new cross-boundary struct field + Swift wiring,
tracked as a follow-up). Empty slots make apply_contacts_and_keys a
no-op for this path, preserving established iOS behaviour.

Acceptance: cargo check --workspace AND --all-features both exit 0.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
…ELECT (G)

The PR-3 identity_keys::load_state reader uses prepare() for a one-shot
SELECT by design; add it to READ_ONLY_PREPARE_ALLOWED so tc_p1_003
(writers must use prepare_cached) stays green without weakening the
writer-side rule. Same pattern as PR-2 commit ec30c54.

Co-Authored-By: Claudius the Magnificent (1M context) <noreply@anthropic.com>
@lklimek lklimek force-pushed the feat/platform-wallet-rehydration branch from b573fca to b7508a0 Compare May 20, 2026 16:46
@lklimek lklimek force-pushed the feat/platform-wallet-rehydration-contacts branch from d91c35f to 39690eb Compare May 20, 2026 16:46
@lklimek lklimek self-assigned this May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants