Skip to content

feat(platform-wallet): persist typed BLS/EdDSA provider keys as core address rows#4127

Merged
QuantumExplorer merged 3 commits into
v4.1-devfrom
claude/gifted-maxwell-d1160d
Jul 14, 2026
Merged

feat(platform-wallet): persist typed BLS/EdDSA provider keys as core address rows#4127
QuantumExplorer merged 3 commits into
v4.1-devfrom
claude/gifted-maxwell-d1160d

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 14, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

CoreAddressEntryFFI carried a fixed 33-byte ECDSA-only public_key slot, so non-secp256k1 provider keys were dropped on the SwiftData persist round-trip:

  • Provider Operator Keys (BLS, 48 bytes) — address rows persisted with an empty publicKey, kept alive in-memory only by a merge workaround in restore_address_pool.
  • Provider Platform Node Keys (Ed25519, 32 bytes) — persisted zero address rows; the keys traveled through a dedicated account-level derivedPlatformNodeKeys batch (ProviderPlatformNodeKeyFFI on AccountSpecFFI) with its own restore path (restore_platform_node_pool).

What was done?

Rust / FFI

  • Widened CoreAddressEntryFFI to a typed key: public_key: [u8; 48] + public_key_len (0 = none / 33 ECDSA / 48 BLS / 32 EdDSA) + key_type_tag (new KeyTypeTagFFI enum). build_core_address_entry_ffi marshals all three PublicKeyType variants with per-curve length validation; address_info_from_ffi rebuilds the typed key, forgiving on invalid tag/len pairs.
  • register_wallet now populates the managed platform-node pool from the pre-derived batch (new populate_platform_node_pool in rs-platform-wallet) before the address-pool snapshot, so EdDSA keys persist as ordinary typed address rows and the in-memory pool matches what a restore reconstructs.
  • Retired the batch plumbing: ProviderKeyAccountEntry.derived_platform_node_keys, ProviderPlatformNodeKeyFFI, the AccountSpecFFI ptr/len pair, and restore_platform_node_pool are gone (AccountAddressPoolFFI layout guard updated 136 → 120).
  • Removed the typed-key merge workaround in restore_address_pool — the row itself is now authoritative for its typed key.
  • Added a thin FFI bridge platform_wallet_platform_node_id_from_ed25519_pubkey (wraps dashcore::PlatformNodeId::from_ed25519_public_key) so the host can render node ids from persisted pubkeys.

Swift

  • PersistentCoreAddress gains keyType; the persistence handler marshals public_key_len/key_type_tag both ways. PersistentAccount.derivedPlatformNodeKeys and its marshaling removed (pre-release, no migration shims — stale dev stores are deleted + re-imported).
  • Storage explorer labels the key by curve: ECDSA (66 hex) / BLS (96 hex) / Ed25519 (64 hex).
  • The Node Keys screen reads the account's typed Ed25519 address rows and computes node ids via the new bridge — still no keychain prompt.

How Has This Been Tested?

  • cargo test -p platform-wallet -p platform-wallet-ffi --all-features — all green, including new tests:
    • typed_public_key_survives_ffi_round_trip_into_fresh_pool — BLS-48 / EdDSA-32 / ECDSA-33 through build_core_address_entry_ffi → address_info_from_ffi → restore_address_pool into a fresh pool, byte-for-byte, no merge path.
    • populate_platform_node_pool_lands_typed_eddsa_rows — registration-side pool population pins typed rows + node-id-derived addresses + watermark.
  • cargo check --all-targets on platform-wallet, platform-wallet-ffi, rs-unified-sdk-ffi; cargo fmt --all --check clean.
  • ./build_ios.sh --target sim — full Rust + SwiftDashSDK + SwiftExampleApp build succeeded.
  • On-simulator (iPhone 16 Pro, fresh wallet registration): SwiftData shows Provider Operator Keys rows with keyType=1, 48-byte keys and Provider Platform Node Keys rows with keyType=2, 32-byte keys (20 rows, previously zero); storage explorer renders "BLS Public Key" (96 hex) and "Ed25519 Public Key" (64 hex); typed rows survive terminate → relaunch (restore path); Node Keys screen lists all keys from persistence with node ids matching SHA256(pubkey)[..20] exactly.

Breaking Changes

None on-chain / consensus. The FFI ABI and SwiftData schema changed together in-repo (pre-release; existing dev stores fail container creation and need delete + re-import, per project policy).

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

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Persisted core addresses now retain public-key type information for ECDSA, BLS, and Ed25519 keys.
    • Added Swift SDK support for deriving platform node IDs from Ed25519 public keys.
    • Platform-node keys are restored directly from persisted address records.
  • Bug Fixes
    • Improved persistence and restoration of typed public keys, including addresses beyond the current generation gap.
    • Invalid key metadata no longer prevents the associated address from being restored.
  • UI
    • Address details now identify the public-key curve type alongside the key data.

…address rows

Widen CoreAddressEntryFFI from an ECDSA-only 33-byte slot to a typed
48-byte key (key_type_tag ECDSA/BLS/EdDSA + public_key_len), so BLS
operator and Ed25519 platform-node public keys survive the SwiftData
persist/restore round-trip in the row itself. Populate the managed
platform-node pool at registration so its keys ride the normal
address-pool pipeline, and retire the account-level
derived-platform-node-keys batch plumbing plus the typed-key merge
workaround in restore_address_pool. The storage explorer labels keys
by curve, and the Node Keys screen reads the typed address rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99b2aa56-bdc2-45d7-9d0f-c06ca19394b6

📥 Commits

Reviewing files that changed from the base of the PR and between f93f5bf and e6fd9b5.

📒 Files selected for processing (1)
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
📝 Walkthrough

Walkthrough

The PR replaces fixed ECDSA-only address keys with typed ECDSA, BLS, and EdDSA persistence across Rust FFI and SwiftData. Platform-node keys are populated into address pools before snapshotting, restored through core-address rows, and displayed from persisted typed addresses.

Changes

Typed key persistence

Layer / File(s) Summary
FFI contracts and layout
packages/rs-platform-wallet-ffi/src/core_address_types.rs, packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs, packages/rs-platform-wallet-ffi/src/wallet_registration_persistence.rs
FFI address rows now use a 48-byte key slot with explicit length and type fields; obsolete platform-node batch fields are removed and layout assertions are updated.
Platform-node pool registration
packages/rs-platform-wallet/src/changeset/changeset.rs, packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs, packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs
EdDSA platform-node keys are inserted as typed rows before wallet address pools are snapshotted.
FFI persistence and restoration
packages/rs-platform-wallet-ffi/src/persistence.rs
Typed ECDSA, BLS, and EdDSA keys are encoded, decoded, tested through round trips, and restored through common core-address pools.
Swift persistence bridge
packages/swift-sdk/Sources/SwiftDashSDK/Persistence/*, packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/*
SwiftData stores key types, marshals widened FFI rows, removes derived-key batch handling, and exposes platform-node ID derivation through Rust FFI.
Example app presentation
packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/{Core/Views,Views}/*
Platform-node details are derived from persisted EdDSA core addresses, and public-key labels identify key types by byte length.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlatformWalletManager
  participant ManagedWalletInfo
  participant FFIPersister
  participant SwiftData
  participant AccountDetailView
  PlatformWalletManager->>ManagedWalletInfo: populate typed EdDSA platform-node rows
  ManagedWalletInfo->>FFIPersister: snapshot typed core-address pools
  FFIPersister->>SwiftData: persist public key bytes and key type
  SwiftData->>AccountDetailView: load typed core-address records
  AccountDetailView->>AccountDetailView: derive platform-node display data
Loading

Possibly related PRs

Suggested labels: Client Only

Suggested reviewers: shumkov, zocolini

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: persisting typed BLS and EdDSA provider keys as core address rows.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gifted-maxwell-d1160d

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.

@github-actions github-actions Bot added this to the v4.1.0 milestone Jul 14, 2026
@thepastaclaw

thepastaclaw commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

⛔ Blockers found — Sonnet deferred (commit e6fd9b5)
Canonical validated blockers: 1

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift (2)

4682-4689: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid hardcoding the struct layout size.

Consider using MemoryLayout.size(ofValue: e.public_key) instead of the hardcoded 48. If the FFI struct definition for public_key changes in the future, this will automatically adapt and prevent potential out-of-bounds errors or unintentional truncation.

♻️ Proposed refactor
-                if row.publicKey.count <= 48 {
+                if row.publicKey.count <= MemoryLayout.size(ofValue: e.public_key) {
                     copyBytes(row.publicKey, into: &e.public_key)
                     e.public_key_len = UInt8(row.publicKey.count)
                     e.key_type_tag = row.keyType
                 } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`
around lines 4682 - 4689, Update the public-key length check in the
PlatformWallet persistence mapping to compare row.publicKey.count against
MemoryLayout.size(ofValue: e.public_key) instead of the hardcoded 48, while
preserving the existing copy and fallback behavior.

6167-6179: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Simplify initialization and prevent out-of-bounds traps.

You can safely initialize the Data buffer using the collection's prefix method. This guarantees bounds safety up to src.count and eliminates the need for manual memory copying and explicit subscripting, which would trap if keyLen ever exceeded the tuple's bounds due to an FFI mismatch or memory corruption.

🛡️ Proposed refactor
-                let publicKey: Data
-                if keyLen > 0 {
-                    var pk = Data(count: keyLen)
-                    withUnsafeBytes(of: entry.public_key) { src in
-                        pk.withUnsafeMutableBytes { dst in
-                            dst.copyMemory(from: UnsafeRawBufferPointer(rebasing: src[0..<keyLen]))
-                        }
-                    }
-                    publicKey = pk
-                } else {
-                    publicKey = Data()
-                }
+                let publicKey = keyLen > 0 ? withUnsafeBytes(of: entry.public_key) { Data($0.prefix(keyLen)) } : Data()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`
around lines 6167 - 6179, Update the publicKey initialization in the entry
parsing block to construct Data directly from the public_key byte collection’s
prefix(keyLen), preserving an empty Data result for nonpositive lengths. Remove
the manual buffer allocation, unsafe byte closures, and direct src[0..<keyLen]
slicing so lengths exceeding the source bounds are safely truncated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 4682-4689: Update the public-key length check in the
PlatformWallet persistence mapping to compare row.publicKey.count against
MemoryLayout.size(ofValue: e.public_key) instead of the hardcoded 48, while
preserving the existing copy and fallback behavior.
- Around line 6167-6179: Update the publicKey initialization in the entry
parsing block to construct Data directly from the public_key byte collection’s
prefix(keyLen), preserving an empty Data result for nonpositive lengths. Remove
the manual buffer allocation, unsafe byte closures, and direct src[0..<keyLen]
slicing so lengths exceeding the source bounds are safely truncated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d72dd934-e998-4f29-b62c-3dd4fac1e640

📥 Commits

Reviewing files that changed from the base of the PR and between 58543d3 and f93f5bf.

📒 Files selected for processing (14)
  • packages/rs-platform-wallet-ffi/src/core_address_types.rs
  • packages/rs-platform-wallet-ffi/src/persistence.rs
  • packages/rs-platform-wallet-ffi/src/provider_key_at_index.rs
  • packages/rs-platform-wallet-ffi/src/wallet_registration_persistence.rs
  • packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs
  • packages/rs-platform-wallet/src/changeset/changeset.rs
  • packages/rs-platform-wallet/src/manager/wallet_lifecycle.rs
  • packages/rs-platform-wallet/src/wallet/provider_key_at_index.rs
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentAccount.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentCoreAddress.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/ManagedPlatformWallet.swift
  • packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Core/Views/AccountDetailView.swift
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift
💤 Files with no reviewable changes (2)
  • packages/swift-sdk/Sources/SwiftDashSDK/Persistence/Models/PersistentAccount.swift
  • packages/rs-platform-wallet-ffi/src/wallet_restore_types.rs

…implify key copy

Address CodeRabbit review: derive the 48-byte slot bound from the FFI
field itself and build the persisted key Data via a bounds-safe prefix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer merged commit 4b67f37 into v4.1-dev Jul 14, 2026
17 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/gifted-maxwell-d1160d branch July 14, 2026 16:32

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preliminary review — Codex only

The typed provider-key persistence and round-trip tests pass, but the Storage Explorer exposes a curve-incorrect private-key action for the newly persisted BLS and Ed25519 rows. The two prior migration findings remain technically valid but are intentionally deferred under the documented pre-release policy of recreating stale development stores.

Validated blockers were found in the Codex precheck. Sonnet is deferred until a fresh Codex revalidation clears the blocker gate.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (failed), gpt-5.6-sol — general (completed)
  • Verifier: gpt-5.6-sol — verifier
  • Sonnet: not run (deferred by blocker gate)

🔴 1 blocking

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift`:
- [BLOCKING] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/StorageRecordDetailViews.swift:1602: Do not derive secp256k1 secrets for typed provider-key rows
  The Storage Explorer queries every PersistentCoreAddress and routes each row to this detail view, while this PR now persists BLS and Ed25519 provider keys as ordinary rows with keyType 1 and 2. The private-key section is rendered unconditionally and calls coreAddressPrivateKey, whose Rust implementation looks up only the stored derivation path, always performs secp256k1 private derivation, and always returns a WIF. For a provider row this can therefore display a plausible but unrelated secp256k1 secret instead of the BLS or Ed25519 key matching the displayed public key. Hide this generic action for non-ECDSA rows or route them through providerKeyAtIndex, as the account detail screen already does.

bezibalazs added a commit that referenced this pull request Jul 14, 2026
…nd-example-app

Conflicts: Cargo.lock (theirs + re-resolve) and identity_top_up.rs (base
#4093's rework — MIN_TOP_UP_DUFFS floor, out_new_balance sentinel, guard
tests — supersedes the branch's older zero-guard).

Breaking-FFI adaptations for the Android JNI/Kotlin side:
- #4093/#4126: shielded identity-create/transfer/unshield/withdraw gained
  a mnemonic_resolver_handle param — threaded through the JNI exports,
  FundingNative externs, and PlatformWalletManager; the
  transfer/unshield/withdraw trio now runs under teardownGate.op (they
  borrow the manager's resolver, so the round-46 ungated rationale no
  longer holds).
- #4127 typed provider keys: the pool-entry write trampoline forwards
  exactly public_key_len meaningful bytes (blob self-describing by
  length: 33 ECDSA / 48 BLS / 32 EdDSA); the load path rebuilds the
  48-byte slot + key_type_tag from the stored blob length. No Kotlin
  bridge/descriptor change needed.
- AccountSpecFFI lost derived_platform_node_keys(+count) (moved into the
  typed core-address rows) — the round-42 null-init is dropped.
- #4126 Orchard viewing keys: the three new vtable slots are None —
  binds keep resolving the seed via the mnemonic resolver (the
  documented fallback); the seedless-bind persistence port is tracked as
  a follow-up.

Verified: cargo fmt, JNI check (shielded), workspace clippy
--all-features -D warnings, platform-wallet-ffi lib tests (170), gradle
:sdk+:app tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants