Skip to content

feat(platform-wallet)!: support DashPay shielded tips with dedicated accounts - #5288

Open
PastaPastaPasta wants to merge 4 commits into
v5.1-devfrom
feat/dashpay-shielded-tips
Open

PastaPastaPasta wants to merge 4 commits into
v5.1-devfrom
feat/dashpay-shielded-tips

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Supersedes #4616, recreated from a dashpay/platform branch so the full CI (Rust, wallet, Kotlin, Swift, e2e) runs; fork PRs skip those jobs. The commits are unchanged (head 5b6e8d6583). Review history is on #4616. Two review threads on #4616 are still open for a maintainer decision (restart-safe tip submission in the example apps).

Issue being fixed or feature implemented

Let a DashPay user publish a reusable shielded tip address and receive payments by username. Wallet-generated addresses use a dedicated ZIP-32 account per identity, separating tip viewing keys from ordinary wallet activity. Users can also publish an externally generated Orchard address.

This follows the transparent profile-address work in #4380 and the 43-byte format in dashpay/dips#188.

Split (2026-09-16): the contract field shipped separately in #4768 and the DPNS resolver fix in #4769; both are in v5.1-dev. This PR now carries only the wallet, persistence, native bindings, and Swift/Kotlin example flows, and targets v5.1-dev (milestone v5.1.0). Nothing that remains here is consensus code.

What was done?

  • The contract field (profile.shieldedAddress on DashPay v2 at position 7, 43 raw bytes including the full diversifier; consensus enforces the byte length, clients validate the Orchard decoding) landed in feat(dpp)!: dashpay profile shielded address field #4768 together with the protocol 13 to 14 upgrade tests and the fee/root fixtures. Since the 2026-10-05 merge of v5.1-dev those files are no longer in this diff.
  • Carry all three profile payment fields through Rust wallet models, document parsing/construction, persistence, C FFI, Swift, and Kotlin. Explicit keep/set/remove updates preserve addresses during unrelated profile edits. Publication checks the connected chain's contract capability.
  • Reserve the upper half of ZIP-32 account indices for tips, mapping each wallet identity index to 0x40000000 + index. Preparation verifies the seed, binds viewing keys, and flushes persistence before returning an address. Identity discovery reconstructs the account without relying on the current profile; retired tip accounts keep scanning. Ordinary balance/spending choices exclude tip accounts, with explicit access to tip funds.
  • Add publish/replace/remove and pay-by-username flows in both example apps. Resolve DPNS and a proof-verified profile, display the identity/address, warn about previously seen recipient changes, and recheck the confirmed recipient before sending. No transparent fallback or contact request is involved.
  • Migrate existing SQLite, SwiftData, and Room profile data without losing existing identity/contact state. Expose the address metadata in the Swift storage explorer and preserve coverage checks for inherited schema models. The DPNS resolver fix for identifiers decoded as bytes moved to fix(rs-sdk): resolve DPNS names whose identity record is not an Identifier value #4769.
  • Document account allocation, restoration, address rotation, and external-address ownership. Account separation is wallet policy; it is not a consensus restriction or a guarantee against correlations from later spending. Per-contact encrypted addresses and authenticated sender attribution are outside this change.

How Has This Been Tested?

Local macOS builds and targeted validation:

  • Rust wallet: 876 tests passed, including profile patches, recipient changes, dedicated-account recovery, wrong-seed rejection, retired accounts, and received-note isolation/rotation.
  • SQLite storage: 139 unit tests passed (2 ignored), 7 migration tests and 17 persistence roundtrip tests passed.
  • Drive/ABCI: 18 upgrade/schema tests, 160 document tests, 21 check-tx tests (1 ignored), and 2 deterministic-root tests passed. Historical protocol roots remain unchanged. (Now covered by feat(dpp)!: dashpay profile shielded address field #4768.)
  • Native FFI: 6 profile tests and 55 persistence tests passed. Wallet, FFI, and host JNI Clippy checks passed; the wallet also builds with default nonshielded features plus serde.
  • Swift: 23 migration/persistence/history tests passed; simulator framework and example app built with warnings treated as errors. The framework/app were refreshed after the final shared SDK change.
  • Kotlin: 556 JVM tests passed across SDK and example app; SDK/app builds and instrumented-test Kotlin compilation passed. SDK lint passed. App lint retains one pre-existing StateFlow.value composition finding in SyncStatusScreen.kt:215.
  • Follow-up review validation: all 11 viewing-key binding tests and the profile replay test pass; Rust Clippy passes; Swift app rebuild and 4 migration tests pass; Kotlin SDK/app rebuilds, SDK lint, and 2 additional retry-policy tests pass.
  • Changed Rust files pass rustfmt; git diff --check passes. Storage explorer coverage passes for all 36 models, with a negative fixture verifying inherited-model omissions are detected.

GitHub skips Rust and mobile runner jobs for this fork under the existing workflow trust policy; the results above are local validation.

Not exercised: a live funded network tip transaction, Android NDK/native .so build, or Android device/instrumented execution. Full network end-to-end validation remains a release validation step.

Breaking Changes

Client-side only: there is no consensus or protocol change here (the protocol-14 DashPay v2 contract bytes and their fee/root baselines shipped in #4768). The client changes are breaking:

  • C ABI: the #[repr(C)] restore structs IdentityEntryFFI and ContactProfileRowFFI gained the three payment addresses and their presence flags (IdentityEntryFFI grows from 224 to 312 bytes), and the native profile/update structs widened too. Every C ABI consumer, the Swift xcframework, and the Android JNI library must be rebuilt together with the host code; mixing old and new binaries misreads these structs. The existing FFI profile setter stays as a wrapper with keep-address semantics.
  • Rust API: the public profile and update models (DashPayProfile, the profile patch types) gained fields.
  • Persistence: forward migrations in SQLite (V019), Room (14 to 15), and SwiftData (live V4). Downgrading a migrated wallet database to an older SDK is not supported.

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

This pull request was created by Codex.

Fix-up (2026-09-16, maintainer push)

Merged v4.2-dev (ffb6e53f20) into the branch and addressed the open review threads; head = merge 47e9e1bc95 + a637a0e2bd.

  • Storage migration renumbered V008 to V019: v4.2-dev already ships V008 to V018. The entry_format / profile_format stamps now DEFAULT 1 and the migration marks the rows it finds 0, so only pre-V019 rows are decoded as the legacy shape; both identity readers on the hard-delete schema dispatch on the stamp. tests/sqlite_profile_address_encoding.rs walks a V007 database through the whole chain. SCHEMA.md documents both columns.
  • (Superseded by the 2026-10-05 update below.) Swift schema V4 frozen through the generator (scripts/freeze_schema_models.py, FREEZES row at 787cac09e7) instead of a hand-written copy; DashSchemaV5 adds PersistentDashpayPaymentAddresses; dash-v5.store written by this build for the fixture-based hash test from feat(swift-sdk): generate the frozen SwiftData schema models, and guard them with real stores #4644.
  • Drive PV14 pins recomputed for DashPay v2 on top of the contract version item (feat(platform)!: prove data contract versions without the contracts via a PV14 version item #4749): contract create/update fees, document deletion/replacement fees, and the genesis root hash. These pins shipped with feat(dpp)!: dashpay profile shielded address field #4768.
  • Review fixes: catch_query_panic around the non-spending tip exports plus an entry-point-level panic regression test; app-owned ShieldedTipSubmissions on iOS (parity with Android); Kotlin tip account derived from the live identity index rather than Room's placeholder 0.

Verified locally: storage crate suite, wallet tip/profile/bind tests (87), FFI panic tests, Drive check-tx/document/root-hash tests, Swift package persistence tests (18) and the example app build with warnings as errors, Kotlin SDK/app compile and tip unit tests.

Update (2026-10-05, maintainer push)

Merged v5.1-dev (1ebcedb028): the #4768/#4769 copies dropped out (143 to 99 files). Persistence numbering moved to follow the base:

  • Room: v5.1-dev ships versions 12 to 14, so the profile address columns are now MIGRATION_14_15 (database version 15, 15.json); migration tests walk 14 to 15 and 10 to 15.
  • SwiftData: v5.1-dev replaced the development V4/V5 chain with App Store release snapshots (feat(swift-sdk)!: freeze schemas only after App Store publication #4818, fix(swift-sdk)!: restore historical V2 migration to live V3 #4910) and published schema 3.0.0. The hand-registered V4 freeze and V5 from the earlier fix-up are gone: DashSchemaV3 is bound to its generated release snapshot and a new live DashSchemaV4 adds PersistentDashpayPaymentAddresses (V2 to V3 to V4, and V1 to V3 to V4). SCHEMA_RELEASES.md lists the routes.
  • Storage: SQLite stays at V019.

Also addressed in the remaining review threads: the tip resolver releases the wallet registry lock before its network lookups; Kotlin ties the tip account to the identity it was resolved for; Swift identity deletion paths remove payment-address rows; literal-bytes coverage for the pre-V019 profile record.

🤖 Generated with Claude Code

PR Hygiene · 17b627f

  • Bots — coderabbitai skipped after the window · thepastaclaw ✓
  • Self-review — post /self-reviewed
  • Build green
  • Approvals
    • kotlin-sdk (packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/di/AppContainer.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayJson.kt, packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayProfileScreen.kt and 29 more) — HashEngineering
    • rs-platform-wallet-ffi (packages/rs-platform-wallet-ffi/src/dashpay_profile.rs, packages/rs-platform-wallet-ffi/src/identity_persistence.rs, packages/rs-platform-wallet-ffi/src/persistence.rs and 3 more) — HashEngineering or ZocoLini or llbartekll or romchornyi
    • wallet-storage (packages/rs-platform-wallet-storage/SCHEMA.md, packages/rs-platform-wallet-storage/migrations/V019__profile_address_encoding.rs, packages/rs-platform-wallet-storage/src/sqlite/mod.rs and 10 more) — lklimek
    • rs-platform-wallet (packages/rs-platform-wallet/docs/SHIELDED_TIPS.md, packages/rs-platform-wallet/src/changeset/changeset.rs, packages/rs-platform-wallet/src/error.rs and 18 more) — HashEngineering or ZocoLini or llbartekll or romchornyi
    • files with no dedicated owner (packages/rs-unified-sdk-jni/src/dashpay.rs, packages/rs-unified-sdk-jni/src/funding.rs, packages/rs-unified-sdk-jni/src/persistence.rs and 2 more) — QuantumExplorer or shumkov
    • swift-sdk (packages/swift-sdk/SCHEMA_RELEASES.md, packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashLegacyStoreSQLite.swift, packages/swift-sdk/Sources/SwiftDashSDK/Persistence/DashModelContainer.swift and 35 more) — llbartekll or romchornyi

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

@PastaPastaPasta PastaPastaPasta added this to the v5.1.0 milestone Oct 5, 2026
@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: Repository: dashpay/platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7a5bae81-3fee-4dce-a0fb-d672e6f59f35

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
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Oct 5, 2026
@thepastaclaw

thepastaclaw commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 17b627f) · triage: critical

PastaPastaPasta added a commit that referenced this pull request Oct 5, 2026
Live V4 adds PersistentDashpayPaymentAddresses, which sorts ahead of
PersistentIdentity and renumbers it from 13 to 14. Core Data embeds the
ordinal in generated column names, so the token-balance table's
Z13TOKENBALANCES foreign key becomes Z14TOKENBALANCES with its values
intact. validatePreservation normalized Z_<n> join-table names but not
the Z<n><NAME> column form, so the V1 -> V3 -> V4 and historical
V2 -> V3 -> V4 migrations were reported as removing a column
(DashModelMigrationTests, CI on #5288).

validatePreservation now keys columns by entity name for the
cross-store comparison; a dropped or type-converted column is still
rejected. layout()/evidence() keep their existing keys so recovery
journals written by earlier builds still verify.

Test: a renumbered copy of the dash-v1 fixture (entity inserted at 8,
join-table and generated columns renamed) passes; the same copy with
the column dropped or converted to VARCHAR is rejected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@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.

Final validation — Phase 1 + Phase 2

Verified the supplied findings against head 02460d6 and confirmed four issues involving identity-placement persistence, restart-safe submissions, identity-specific spending-account selection, and concurrent account binding. These are client-side defects, classified as suggestions under the supplied policy reserving blocking severity for consensus invariants. Validation was static: the supplied CI snapshot reports successful Rust workspace and Kotlin checks, skipped dedicated wallet tests, and Swift build/tests still in progress.

🟡 4 suggestion(s)

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The large, cross-language diff directly changes key derivation and dedicated account handling in packages/rs-platform-wallet/src/wallet/shielded/tips.rs, funds submission in packages/rs-platform-wallet-ffi/src/shielded_send.rs, and persisted-data migrations in SQLite, SwiftData, and Room.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 15% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs:206-207: Preserve corrected identity indices through changeset merging
  The new adoption path changes identity_index, but IdentityChangeSet::merge never copies that field into an existing entry. With SQLite FlushMode::Manual, a snapshot buffered at placeholder index 0 can therefore absorb the subsequent discovery snapshot at verified index 5 while retaining index 0. Both Buffer::normalize_identities and the final buffered merge use these semantics, so flush/reload restores the incorrect placement. The same stale slot also remains occupied during buffered admission checks, potentially rejecting another identity that discovery places into the freed slot.

  Make identity_index follow the latest snapshot alongside wallet_id. Add a SQLite regression that buffers an old-slot snapshot, adopts the identity into its verified slot, persists the resulting snapshot, then flushes and reloads; also verify that another identity can occupy the freed slot.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  The submission guard and wallet map are memory-only, and each new process initializes the guard to ready. Kotlin's ShieldedTipSubmissions has the same lifecycle. Restarting after a broadcast with an unresolved outcome therefore removes the warning and permits the user to retry the same intended payment as a new transfer. A relay withholding confirmation is sufficient to create this ambiguity; process termination during broadcast also exposes it.

  Rust records pending transfer activity and can redrive the original transition, but transfer reserves individual input notes rather than a payment intent. Additional spendable notes can fund the retry, allowing both transfers to execute. Persist an unresolved intent before entering the native send, restore its guard before enabling submission, and reconcile it with the original activity or transition. Add process-recreation coverage with an ambiguous original transfer and additional spendable notes; the existing reopening tests retain the same in-memory owner.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayProfileView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayProfileView.swift:39-44: Verify the identity index before selecting its tip spending account
  Rust's optional index getter returns recorded metadata, not a verified identity-to-account association. The mobile restore path constructs ManagedIdentity::new(identity, spec.identity_index), so a persisted placeholder zero becomes Some(0). The existing should_refuse_tip_account_for_a_placeholder_index_until_it_is_verified regression demonstrates an identity belonging at index 5 restored at index 0 while index 0's tip account is bound.

  Preparation rejects this association by checking the seed, but this balance/spending selector and Kotlin's corresponding selectors bypass that check. They can display index 0's funds as belonging to the restored identity and pass that account to the send API, which validates account keys but receives no source identity to verify. Resolve identity-specific tip accounts through a verified wallet operation, or disable these selectors until the association is verified. Extend the placeholder regression to cover balance lookup and spending-account selection, not only address preparation.

In `packages/rs-platform-wallet/src/wallet/shielded/tips.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/tips.rs:142-147: Make tip preparation an atomic augmentation of the bound account set
  The account snapshot and replacement bind are separate operations. Preparation can snapshot ordinary account 0, another task can finish binding accounts 0 and 1, and preparation can then bind its stale snapshot plus the tip account. bind_shielded automatically retains discovered and persisted tip accounts, but not omitted ordinary accounts. Consequently, register_locked removes account 1 from registration and purges its live subwallet state, contradicting preparation's ordinary-account preservation guarantee.

  The shield guard and coordinator install transaction serialize individual installations, not the preceding read-modify-write sequence. Serialize configuration changes across the snapshot and bind, or provide an augmentation operation that merges with the current registration inside the installation transaction. Add a deterministic interleaving regression proving that an ordinary account added during preparation remains bound.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Seed verification runs while holding the wallet-manager read guard — The guard covers bounded synchronous derivation and verification with no network call or await inside the guarded block. Writers are briefly excluded, but the finding supplies no demonstrated hot-path or material contention problem; moving this work outside the lock is an optional optimization rather than a confirmed review defect.
    • Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.

Comment on lines +28 to +32
@Published private(set) var status: Status = .ready
@Published private(set) var message: String?

var busy: Bool { status == .sending }
var submitted: Bool { status != .ready }

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.

🟡 Suggestion: Restore outstanding tip submissions before enabling another send

The submission guard and wallet map are memory-only, and each new process initializes the guard to ready. Kotlin's ShieldedTipSubmissions has the same lifecycle. Restarting after a broadcast with an unresolved outcome therefore removes the warning and permits the user to retry the same intended payment as a new transfer. A relay withholding confirmation is sufficient to create this ambiguity; process termination during broadcast also exposes it.

Rust records pending transfer activity and can redrive the original transition, but transfer reserves individual input notes rather than a payment intent. Additional spendable notes can fund the retry, allowing both transfers to execute. Persist an unresolved intent before entering the native send, restore its guard before enabling submission, and reconcile it with the original activity or transition. Add process-recreation coverage with an ambiguous original transfer and additional spendable notes; the existing reopening tests retain the same in-memory owner.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)

Comment thread packages/rs-platform-wallet/src/wallet/shielded/tips.rs Outdated
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@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.

Re-review — Final validation — Phase 1 + Phase 2

Static verification confirms the original concurrent-bind race and buffered identity-index merge defect are fixed, and the reported occupied-placeholder spending case is now rejected before recipient resolution. Six client-side suggestions remain concerning restart recovery, stale source-account selection, identity replay, account preservation, and test isolation; no consensus-blocking defect was confirmed. No builds or tests were run, and the supplied exact-head CI snapshot still had most validation queued or pending.

🟡 6 suggestion(s)

1 finding(s) not shown inline (the lines are not part of this PR's diff)

🟡 Suggestion: Serialize shielded_add_account with the new shielded config lock
packages/rs-platform-wallet/src/wallet/platform_wallet.rs:1154-1160

Tip preparation now holds shielded_config_lock across its account snapshot and replacement bind, but shielded_add_account writes the same shielded_keys map without participating in that lock. Preparation can snapshot [0], an addition can persist and insert ordinary account 1, and preparation can then install its stale snapshot plus the tip account, overwriting the completed insertion while leaving its viewing-key row persisted. This breaks the new preparation helper's ordinary-account preservation guarantee even though the concurrent-bind case is fixed. Acquire the same configuration lock for the complete add operation, preserving the existing rule that host callbacks run outside the key-slot guard. The add API intentionally does not register accounts with the coordinator itself, so this finding concerns loss of its in-memory insertion, not a claim that it creates a coordinator registration. Extend the interleaving coverage to race preparation with this writer.

source: muse-spark-1.3-contributor (phase1-reviewer: rust-quality)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, intricate diff directly changes funds movement and dedicated ZIP-32 key/account handling in packages/rs-platform-wallet/src/wallet/shielded/tips.rs and packages/rs-platform-wallet-ffi/src/shielded_send.rs, as well as persisted-data migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 15% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt:336-345: Invalidate and reverify the source account when an identity is re-slotted
  The lookup keys include the wallet wrapper, manager, and identity ID, but not the identity's placement. This PR allows discovery to move that same identity from placeholder index 0 to verified index 5 without replacing those objects. If discovery completes while this composition is alive, the cached result remains `0x40000000` and is still offered as that identity's dedicated source account. A blocking discovery can continue natively after its search screen is dismissed, so returning to DashPay before it finishes can expose this interleaving. The new Rust check does not reject the stale source: `verify_tip_account_owner` succeeds when slot 0 has become empty, and also succeeds if the rightful identity 0 later occupies it. A confirmed send can thus spend the retained account-0 funds rather than the selected identity's account-5 funds. Invalidate the lookup when verified placement changes and recheck the selected source identity/account association at submission; checking only the account's current occupant is insufficient. Add a regression resolving at placeholder 0, adopting into 5, and attempting a send with the old selection. The profile-screen lookup also needs placement-change invalidation.

In `packages/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/state/manager/lifecycle.rs:195-207: Carry identity relocation through changeset replay
  Adoption now changes bucket placement and identity coordinates, but `IdentityManager::apply_identity_entry` still updates an existing identity in place and returns without applying `wallet_id`, `identity_index`, or `location_index`. Replaying valid snapshots A@0, then adopted A@5, then newly admitted B@0 through the public `PlatformWallet::apply` or `PlatformWalletInfo::apply_changeset` API leaves A at slot 0 until B's fresh insertion overwrites it. A's state is lost and its reverse-index entry still points to slot 0, so `identity(A)` returns B. Promotion from the observed bucket likewise fails to reproduce the adoption. Normal startup reconstruction uses a different path, which limits the current impact, but the public replay API no longer reproduces the mutations introduced here. Apply relocation while preserving managed state and reverse-index consistency, reject unrelated occupied targets, and handle merged batches independently of identity-ID ordering when one entry frees another's slot. Add round-trip coverage for observed-identity promotion and A@0 → A@5 followed by B@0.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/identity/SearchWalletsForIdentitiesScreen.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/identity/SearchWalletsForIdentitiesScreen.kt:147-151: Preserve ordinary accounts during discovery-triggered rebinding
  This new refresh calls the replacement bind API with its default account list `[0]`. For a wallet already bound to ordinary accounts `[0, 1]`, Rust adds discovered and persisted tip accounts but does not retain omitted ordinary account 1. `NetworkShieldedCoordinator::register_locked` consequently removes account 1's registration and purges its live notes and watermark, so discovering identities unexpectedly stops an existing ordinary account from scanning. Swift preserves a snapshot of ordinary accounts, but its snapshot and replacement call are also outside the native configuration lock. Provide a refresh operation that reads the current configuration and adds discovered tip accounts atomically under `shielded_config_lock`, and use it from discovery rather than treating `[0]` as a complete replacement configuration. Keep full bind available for intentional replacement. Cover discovery refresh starting from `[0, 1]` and verify that both ordinary registrations survive.

In `packages/rs-platform-wallet/src/wallet/platform_wallet.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/platform_wallet.rs:1154-1160: Serialize shielded_add_account with the new shielded config lock
  Tip preparation now holds `shielded_config_lock` across its account snapshot and replacement bind, but `shielded_add_account` writes the same `shielded_keys` map without participating in that lock. Preparation can snapshot `[0]`, an addition can persist and insert ordinary account 1, and preparation can then install its stale snapshot plus the tip account, overwriting the completed insertion while leaving its viewing-key row persisted. This breaks the new preparation helper's ordinary-account preservation guarantee even though the concurrent-bind case is fixed. Acquire the same configuration lock for the complete add operation, preserving the existing rule that host callbacks run outside the key-slot guard. The add API intentionally does not register accounts with the coordinator itself, so this finding concerns loss of its in-memory insertion, not a claim that it creates a coordinator registration. Extend the interleaving coverage to race preparation with this writer.

In `packages/rs-platform-wallet/src/wallet/shielded/tips.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/tips.rs:258-263: Isolate the preparation pause hook by wallet instance
  The global pause hook is consumed using only the deterministic wallet ID. The concurrency regression, placeholder-index regression, and discovery/restoration regression all create independent testnet wallet instances from `MESSAGE_SIGNING_TEST_MNEMONIC`, so they share that ID. Under parallel execution, another test's preparation can remove the concurrency test's hook and signal `reached` while holding its own configuration lock. The intended preparation then runs without pausing, allowing the competing bind to complete inside the timeout and failing the regression despite correct production locking. Attach the hook to shared state belonging to the actual wallet instance, use an instance-specific token, or give the concurrency fixture a distinct deterministic mnemonic. The current wallet-ID key does not provide the parallel-test isolation claimed by the comment.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  The shared owner preserves the submission guard across sheet dismissal, but a new process reconstructs an empty wallet map and a `.ready` submission. Kotlin has the same lifecycle. After termination during an accepted-but-unconfirmed send, reopening therefore removes the uncertainty warning and permits the user to confirm the same payment again. Native pending activity and redrive records do not prevent this: note reservations exclude particular inputs, while `reserve_unspent_notes` can select other funded notes for a fresh transfer. The redrive transaction itself is armed only after an ambiguous broadcast result returns, so process loss during the call also cannot be treated as a recovered safe retry. Persist a network/wallet-scoped unresolved intent before entering native spending, restore its warning before enabling confirmation, and reconcile native activity or require explicit acknowledgment that another submission is a separate payment. Add process-recreation tests using newly constructed owners and sufficient independent notes to fund both attempts. The PR's pending maintainer decision is not an implemented recovery boundary.

Comment on lines +336 to +345
val tipAccountKey = listOf(managed, tipManager, identityHex)
val taggedTipAccount by produceState<Pair<List<Any?>, Result<Int>>?>(
initialValue = null, managed, tipManager, identityHex,
) {
value = tipAccountKey to runCatching {
val index = requireNotNull(managed) { "Wallet is not loaded" }
.identityIndex(identity.identityId)
?: error("Tip account requires a recoverable identity index")
requireNotNull(tipManager).shieldedTipAccountIndex(index)
}

@thepastaclaw thepastaclaw Oct 5, 2026 •

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.

✅ No longer applies at f2fff572; see the replies below.

🟡 Suggestion: Invalidate and reverify the source account when an identity is re-slotted

The lookup keys include the wallet wrapper, manager, and identity ID, but not the identity's placement. This PR allows discovery to move that same identity from placeholder index 0 to verified index 5 without replacing those objects. If discovery completes while this composition is alive, the cached result remains 0x40000000 and is still offered as that identity's dedicated source account. A blocking discovery can continue natively after its search screen is dismissed, so returning to DashPay before it finishes can expose this interleaving. The new Rust check does not reject the stale source: verify_tip_account_owner succeeds when slot 0 has become empty, and also succeeds if the rightful identity 0 later occupies it. A confirmed send can thus spend the retained account-0 funds rather than the selected identity's account-5 funds. Invalidate the lookup when verified placement changes and recheck the selected source identity/account association at submission; checking only the account's current occupant is insufficient. Add a regression resolving at placeholder 0, adopting into 5, and attempting a send with the old selection. The profile-screen lookup also needs placement-change invalidation.

source: gpt-6.1-sol (phase2-reviewer: general)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Partly valid; fixed in 257fa0e. The lookup keys didn't include placement, so a re-slot while the screen was open left the old account cached. Both the DashPay tab and the profile lookup now also key on Room's identityIndex, which the persistence callback rewrites on a re-slot, so the lookup reruns against the live index. The tip sheet reads the account when you confirm, so it gets the corrected one.

We're not adding a separate check at submission:

  • A placeholder slot-0 spend while slot 0 is occupied is already refused by verify_tip_account_owner.
  • Once the re-slot empties slot 0, 0x40000000 is this wallet's own unowned (retired) tip account. Spending from it is allowed by design, and it doesn't touch another identity's funds.

🤖 Posted autonomously by Claude on behalf of pasta.

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.

Still applies (re-reviewed at 6c88c318): You're right that the sheet receives the refreshed account on recomposition; the remaining window is after confirmation copies sendAccount and before the suspending history.confirm finishes. A re-slot during that write leaves the queued native call unchanged, and permitting explicit retired-account spending does not verify this captured identity-specific source selection.

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.

Resolved (re-reviewed at 44515451): Your placement key and tagged result remove the long-lived stale-account cache in the tab, and the profile lookup also reruns when the persisted index changes. The remaining interval after confirmation captures the account is tracked only under the separate submission-site finding 496638144e05.

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.

No longer applies (re-reviewed at f2fff572): Your identityIndex effect key and tagged tab result fix the original indefinitely cached lookup after Room observes relocation. The narrower submission-time association gap is tracked under 496638144e05 rather than duplicated on this lookup.

Comment on lines +195 to +207
self.wallet_identities
.entry(wallet_id)
.or_default()
.insert(identity_index, managed);
self.location_index_insert(*identity_id, target);
}
if let Some(managed) = self
.wallet_identities
.get_mut(&wallet_id)
.and_then(|bucket| bucket.get_mut(&identity_index))
{
managed.wallet_id = Some(wallet_id);
managed.identity_index = Some(identity_index);

@thepastaclaw thepastaclaw Oct 5, 2026 •

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.

✅ Resolved at 74a473f8; see the replies below.

🟡 Suggestion: Carry identity relocation through changeset replay

Adoption now changes bucket placement and identity coordinates, but IdentityManager::apply_identity_entry still updates an existing identity in place and returns without applying wallet_id, identity_index, or location_index. Replaying valid snapshots A@0, then adopted A@5, then newly admitted B@0 through the public PlatformWallet::apply or PlatformWalletInfo::apply_changeset API leaves A at slot 0 until B's fresh insertion overwrites it. A's state is lost and its reverse-index entry still points to slot 0, so identity(A) returns B. Promotion from the observed bucket likewise fails to reproduce the adoption. Normal startup reconstruction uses a different path, which limits the current impact, but the public replay API no longer reproduces the mutations introduced here. Apply relocation while preserving managed state and reverse-index consistency, reject unrelated occupied targets, and handle merged batches independently of identity-ID ordering when one entry frees another's slot. Add round-trip coverage for observed-identity promotion and A@0 → A@5 followed by B@0.

source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: rust-quality)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 93d4d3b. Production restore doesn't go through this API, but replay should still reproduce the re-slot. Changes:

  • apply_identity_entry moves an existing identity to the (wallet_id, identity_index) its entry carries, via adopt_into_wallet, which keeps the reverse index in lockstep. If another identity still holds that slot, it logs and updates in place.
  • A fresh entry whose slot is occupied is skipped instead of overwriting the occupant.
  • apply_changeset applies batches order-independently through apply_identity_entries. Pure removals run first. Known identities run next, each deferred until a blocker moves out (a cycle falls back to an in-place update). New identities are inserted last. Removal still wins for an id that is both snapshotted and removed.

New tests in wallet/apply.rs:

  • A@0 → A@5 → B@0, applied one at a time and as a merged batch that visits B first
  • an observed identity promoted into the wallet
  • an occupied-slot insert being refused
  • a same-batch removal freeing a slot

🤖 Posted autonomously by Claude on behalf of pasta.

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.

Still applies (re-reviewed at 6c88c318): Your known-before-fresh ordering fixes the listed sequential relocation and promotion cases, but removal winning for A does not prevent A's retained snapshot from first blocking B: merging A@​5, remove A, and B@​5 still skips B before deleting A. The cycle fallback also retains old placement for a conflict-free final swap accepted by storage, so merged replay still does not consistently reproduce the mutation result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Both cases are addressed in 4788181 and 74a473f; details in #5288 (comment).

  • Tombstone first: tombstoned identities are removed before placement and their snapshots are dropped, so merged A@5 / remove A / B@5 places B at 5.
  • Swap: a conflict-free final swap (A@1/B@0 from A@0/B@1) is staged out of the wallet and installed. Replay now matches what SQLite's check_index_conflicts accepts.

🤖 Posted autonomously by Claude on behalf of pasta.

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.

Resolved (re-reviewed at 74a473f8): Your latest changes close both remaining merged-replay cases: removed snapshots are excluded before placement, and conflict-free relocation cycles are staged and adopted into their final slots. Combined with adoption of existing managed values and refusal to overwrite occupied slots, replay now preserves the reported relocation and promotion scenarios.

Comment on lines +147 to +151
// Rust adds tip accounts itself, so the default [0] request
// suffices; a bind failure stays non-fatal.
if (container.shieldedService.isAvailable) {
try {
mgr.bindShielded(wallet.walletId)

@thepastaclaw thepastaclaw Oct 5, 2026 •

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.

✅ Withdrawn at 6c88c318; see the replies below.

🟡 Suggestion: Preserve ordinary accounts during discovery-triggered rebinding

This new refresh calls the replacement bind API with its default account list [0]. For a wallet already bound to ordinary accounts [0, 1], Rust adds discovered and persisted tip accounts but does not retain omitted ordinary account 1. NetworkShieldedCoordinator::register_locked consequently removes account 1's registration and purges its live notes and watermark, so discovering identities unexpectedly stops an existing ordinary account from scanning. Swift preserves a snapshot of ordinary accounts, but its snapshot and replacement call are also outside the native configuration lock. Provide a refresh operation that reads the current configuration and adds discovered tip accounts atomically under shielded_config_lock, and use it from discovery rather than treating [0] as a complete replacement configuration. Keep full bind available for intentional replacement. Cover discovery refresh starting from [0, 1] and verify that both ordinary registrations survive.

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, platform-versioning, rust-quality, security-auditor)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Doesn't apply to the Kotlin app: it only ever binds ordinary account 0. These bind paths all request the default [0], and nothing in the Kotlin app or SDK adds other ordinary accounts:

  • AppContainer
  • ShieldedService.bind / bindEngine / clearLocalState
  • PlatformWalletManager.bindShielded
  • this discovery rebind

So the rebind can't drop an ordinary account, and Rust keeps the discovered and persisted tip accounts. Mirroring Swift's snapshot would need a new JNI binding for shielded_account_indices that nothing in Kotlin needs yet, so that's out of scope here.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

Withdrawn (re-reviewed at 6c88c318): You're right that the Kotlin example's reachable startup, service, and discovery bindings use ordinary account 0; I found no caller establishing ordinary account 1 for this flow. The configurable SDK API does not substantiate the application-specific premise, so I withdraw it.

Comment on lines +258 to +263
pub(crate) async fn after_snapshot(wallet: [u8; 32]) {
let hook = AFTER_SNAPSHOT
.lock()
.unwrap()
.as_mut()
.and_then(|hooks| hooks.remove(&wallet));

@thepastaclaw thepastaclaw Oct 5, 2026 •

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.

✅ Resolved at 6c88c318; see the replies below.

🟡 Suggestion: Isolate the preparation pause hook by wallet instance

The global pause hook is consumed using only the deterministic wallet ID. The concurrency regression, placeholder-index regression, and discovery/restoration regression all create independent testnet wallet instances from MESSAGE_SIGNING_TEST_MNEMONIC, so they share that ID. Under parallel execution, another test's preparation can remove the concurrency test's hook and signal reached while holding its own configuration lock. The intended preparation then runs without pausing, allowing the competing bind to complete inside the timeout and failing the regression despite correct production locking. Attach the hook to shared state belonging to the actual wallet instance, use an instance-specific token, or give the concurrency fixture a distinct deterministic mnemonic. The current wallet-ID key does not provide the parallel-test isolation claimed by the comment.

source: gpt-6.1-sol (phase2-reviewer: rust-quality)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 93d4d3b. The pause hook is now keyed by wallet instance (Arc::as_ptr of its shielded_config_lock, which clones share), not by the deterministic wallet id. Tests built from the same mnemonic no longer share a hook. The concurrency regressions now share one tip_race_fixture / race_tip_preparation helper.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

Resolved (re-reviewed at 6c88c318): Your hook now uses the shielded_config_lock Arc allocation for both registration and consumption, so clones share it while independent same-mnemonic wallets do not. The shared race helper arms the actual wallet instance, addressing the parallel-test interference.

@PastaPastaPasta

Copy link
Copy Markdown
Member Author

Re: the review's non-inline finding "Serialize shielded_add_account with the new shielded config lock" (platform_wallet.rs:1154-1160).

Confirmed and fixed in 93d4d3b. shielded_add_account now takes shielded_config_lock after its attachment check, before its key-slot read. Host callbacks still run outside the key-slot guard, and the lock order (config lock, then shield_guard / shielded_keys) is unchanged.

The new regression should_keep_an_added_account_during_tip_preparation pauses preparation after its snapshot, races shielded_add_account(1) against it, and checks that account 1 is still bound afterwards. The test fails without the lock.


🤖 Posted autonomously by Claude on behalf of pasta.

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@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.

Re-review — Final validation — Phase 1 + Phase 2

At head 6c88c31, the configuration-locking, pause-hook isolation, and identity-index merge fixes are present, but three client-side suggestions remain around merged replay, queued source-account selection, and restart-safe submission state; no consensus blocker was confirmed. All nine prior findings were independently reconciled, with the acknowledged balance-provenance follow-up kept deferred. Validation was static only: the supplied CI snapshot shows Rust workspace and Kotlin checks passing, the dedicated wallet job skipped, and PR Hygiene pending; the Swift failure log confirms “database is locked” in testCaptureHistoricalV2Fixture.

🟡 3 suggestion(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, intricate diff changes funds movement and key handling in packages/rs-platform-wallet-ffi/src/shielded_send.rs and packages/rs-platform-wallet/src/wallet/shielded/tips.rs, as well as persisted-state migrations across SQLite, SwiftData, and Room.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 15% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/rs-platform-wallet/src/wallet/apply.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/apply.rs:162-170: Carry identity relocation through changeset replay
  The new ordering handles sequential relocation, but it still evaluates occupancy against transient state rather than the merged batch's final survivors. Starting with A@0, merge the changesets from moving A to 5, removing A, and inserting B@5. IdentityChangeSet::merge retains both A's snapshot and tombstone, so this partition postpones A's removal. Replay moves A to 5, skips fresh B because A occupies 5, then removes A and returns success with B missing. B's subsequent key/contact entries cannot route, and applying the batch again can produce a different result.

  The relocation loop in identity/state/manager/apply.rs has another instance of the same final-state problem: A@0/B@1 ending at A@1/B@0 remains blocked and falls back to in-place updates. The public adoption API can produce those final snapshots through free slot 2, and SQLite's check_index_conflicts explicitly accepts a two-way swap, so replay can disagree with persistence even for a conflict-free final batch.

  Remove all tombstoned identities before placement and exclude their snapshots. Stage surviving relocations so valid cycles can be installed while preserving managed state and reverse-index consistency. Add regressions using actual merged mutation changesets, checking replay against the final persisted placement and checking repeated application.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt:95-99: Invalidate and reverify the source account when an identity is re-slotted
  The placement keys now refresh the account offered by the open sheet, but confirmation copies it into sendAccount before history.confirm suspends on Dispatchers.IO. Discovery can move the selected identity A from placeholder 0 to verified 5 during that suspension. Subsequent recomposition cannot change the account captured by the application-owned submission, so the native call still receives 0x40000000.

  Rust's verify_tip_account_owner accepts that account if slot 0 is empty, or verifies its rightful replacement B if B now occupies slot 0. Neither case establishes that the submitted account belongs to selected identity A. These are still wallet-owned funds, and explicitly spending a retired account is legitimate; the defect is silently using that pool from an identity-specific dedicated-account choice.

  Carry the expected source identity through the identity-scoped submission and verify its seed-derived association with the requested account before selecting notes. Preserve intentional retired-account spending through an explicitly account-based path. Add a regression that pauses recipient-history persistence, re-slots A, and verifies that the captured old source is rejected.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  The application-owned guard survives navigation, but its status and registry are memory-only. After process termination, forWallet creates a submission in ready without consulting persisted outgoing activity or recovery state. Kotlin's registry has the same lifetime boundary, and recipient confirmation history records the destination rather than the payment outcome.

  If the process exits after broadcast but before the result reaches the host, relaunch permits a fresh attempt while the first payment may already have executed or remain eligible for redrive. Native reservations protect the first payment's nullifiers, not the logical payment: operations::transfer selects other available notes without an unresolved-intent check. With another sufficient note set, both transfers can execute and pay the recipient twice. This is duplicate payment, not a consensus double-spend.

  Persist an unresolved intent before entering the native send, restore a conservative reconciliation state before enabling another attempt, and clear it only after a definitive outcome. Add process-recreation coverage with an unresolved first payment and enough other notes to fund another transfer; the current tests reuse the same submission owner.

Comment on lines 162 to 170
let (freeing, last): (Vec<_>, Vec<_>) =
removed.iter().partition(|id| !identities.contains_key(id));
for removed_id in freeing {
self.identity_manager.remove_for_apply(removed_id);
}
self.identity_manager
.apply_identity_entries(identities.into_values());
for removed_id in last {
self.identity_manager.remove_for_apply(removed_id);

@thepastaclaw thepastaclaw Oct 6, 2026 •

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.

✅ Resolved at 74a473f8; see the replies below.

🟡 Suggestion: Carry identity relocation through changeset replay

The new ordering handles sequential relocation, but it still evaluates occupancy against transient state rather than the merged batch's final survivors. Starting with A@0, merge the changesets from moving A to 5, removing A, and inserting B@5. IdentityChangeSet::merge retains both A's snapshot and tombstone, so this partition postpones A's removal. Replay moves A to 5, skips fresh B because A occupies 5, then removes A and returns success with B missing. B's subsequent key/contact entries cannot route, and applying the batch again can produce a different result.

The relocation loop in identity/state/manager/apply.rs has another instance of the same final-state problem: A@0/B@1 ending at A@1/B@0 remains blocked and falls back to in-place updates. The public adoption API can produce those final snapshots through free slot 2, and SQLite's check_index_conflicts explicitly accepts a two-way swap, so replay can disagree with persistence even for a conflict-free final batch.

Remove all tombstoned identities before placement and exclude their snapshots. Stage surviving relocations so valid cycles can be installed while preserving managed state and reverse-index consistency. Add regressions using actual merged mutation changesets, checking replay against the final persisted placement and checking repeated application.

source: gpt-6.1-sol (phase2-reviewer: rust-quality)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Confirmed and fixed in 4788181, with a follow-up guard in 74a473f.

  • Tombstones: apply_changeset now runs every removal first and drops the snapshots of removed identities. Removal still wins over a snapshot in the same batch, and a removed identity's retained snapshot can no longer hold the slot. In your A@5 / remove A / B@5 case, B lands at 5.
  • Swaps: when no remaining move can make progress, apply_identity_entries finds blocked moves that form a closed cycle. It parks them out of the wallet, then adopts them into their target slots, keeping managed state and the reverse index in step. A@0/B@1 merged into A@1/B@0 now replays as the swap.
  • What still stays put: a move blocked by an identity the batch doesn't move. So does a cycle whose slot another entry also claims, which only an inconsistent batch can produce.

Regressions build the batch with IdentityChangeSet::merge from per-mutation changesets and apply it twice to check repeated application:

  • should_place_an_entry_in_the_slot_of_a_removed_identity
  • should_replay_a_merged_slot_swap
  • should_keep_a_move_blocked_by_a_stationary_identity_in_place
  • should_not_stage_a_swap_whose_slot_is_claimed_twice

🤖 Posted autonomously by Claude on behalf of pasta.

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.

Resolved (re-reviewed at 74a473f8): Your removal-first filtering now drops tombstoned snapshots before they can occupy a survivor's target, and closed-cycle staging installs the final swap while moving the existing managed values and reverse-index entries together. I also inspected the repeated-application regressions and the duplicate-claim guard in 74a473f; the two reported counterexamples are addressed.

Comment on lines +95 to +99
val sendAccount = if (spendTips) tipAccount else 0
recipient = null
submission.submit {
history.confirm(manager.network, walletId, sendUsername, selected)
manager.sendShieldedTip(walletId, sendUsername, selected, sendAmount, account = sendAccount)

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.

🟡 Suggestion: Invalidate and reverify the source account when an identity is re-slotted

The placement keys now refresh the account offered by the open sheet, but confirmation copies it into sendAccount before history.confirm suspends on Dispatchers.IO. Discovery can move the selected identity A from placeholder 0 to verified 5 during that suspension. Subsequent recomposition cannot change the account captured by the application-owned submission, so the native call still receives 0x40000000.

Rust's verify_tip_account_owner accepts that account if slot 0 is empty, or verifies its rightful replacement B if B now occupies slot 0. Neither case establishes that the submitted account belongs to selected identity A. These are still wallet-owned funds, and explicitly spending a retired account is legitimate; the defect is silently using that pool from an identity-specific dedicated-account choice.

Carry the expected source identity through the identity-scoped submission and verify its seed-derived association with the requested account before selecting notes. Preserve intentional retired-account spending through an explicitly account-based path. Add a regression that pauses recipient-history persistence, re-slots A, and verifies that the captured old source is rejected.

source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general, ffi-engineer)

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@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.

Re-review — Final validation — Phase 1 + Phase 2

The client-only diff preserves proof-verified recipient resolution and addresses the reported identity-replay, account-configuration locking, and changeset-merging defects. Three non-blocking concerns remain: two source-account relocation windows and loss of unresolved-payment guards after process restart; unverified balance attribution remains an explicitly separated follow-up. Validation was static only: git diff --check passed, and the supplied exact-head CI snapshot still had Kotlin build/tests and PR Hygiene pending.

🟡 3 suggestion(s)

3 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, intricate diff changes key derivation and viewing-key binding in packages/rs-platform-wallet/src/wallet/shielded/tips.rs, funds submission in packages/rs-platform-wallet-ffi/src/shielded_send.rs, and persisted profile encoding and migrations across SQLite, SwiftData, and Room.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 15% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt:95-99: Invalidate and reverify the source account when an identity is re-slotted
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4190547321)
  The source account is copied into sendAccount before history.confirm suspends on Dispatchers.IO. Discovery can move the selected identity from placeholder slot 0 to verified slot 5 during that suspension. A subsequent Room update cannot change the captured argument, and the JNI/C send signatures carry the recipient identity but no expected source identity. Rust's verify_tip_account_owner accepts the emptied slot-0 account, so with sufficient retained notes the payment can spend account 0x40000000 instead of the selected identity's account-5 pool. This is a source-selection error, not an unauthorized-spending bypass: both pools remain wallet-owned. Carry the selected source identity and confirmed account through the identity-specific submission path, reject a changed association before spending, and require renewed review. Preserve explicitly selected retired-account spending separately. Add an interleaving regression that relocates the source while confirmation-history persistence is suspended.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/DashPayTabScreen.kt:337-345: Invalidate and reverify the source account when an identity is re-slotted
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4187974162)
  The placement key fixes eventual cache invalidation, but there is also a window before Room delivers the changed placement. Native discovery updates the identity manager first; until the corresponding row reaches composition, tipAccountKey still matches the old result and the sheet can confirm that account as the selected identity's dedicated source. Reading the current prop at confirmation does not close this window. The native ownership check validates the account's current occupant, not the selected source identity, and deliberately permits an empty retired slot. Retaining that policy is reasonable, but this identity-selected flow should not silently substitute the retired pool. Keep the Room-key fix and validate the expected source identity/account association natively at submission. Cover relocation before the corresponding Room emission, separately from relocation after the sheet captures its argument.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  Every reconstructed submission starts ready, and both apps retain their per-wallet guards only in memory. Restarting after a broadcast with an unresolved outcome therefore enables another confirmation without restoring the previous sending/uncertain state. Native recovery does not make this payment-intent-safe: PendingRedrive is armed after an ambiguous result returns, and even a surviving record reserves only the original transaction's notes. reserve_unspent_notes can select other available notes for a fresh transfer, allowing both payments to execute if the user repeats the apparently unfinished tip. Persist an unresolved submission marker by network and wallet before invoking native send, restore it before enabling payment, and reconcile it with shielded activity/recovery state. Add reconstruction tests with a new submissions owner over the same durable state; the current reopening tests exercise only the existing owner. The description identifies a pending maintainer decision, but the current implementation still has this limitation.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Track verified identity placement for tip-balance attribution — The restore path constructs ManagedIdentity::new(identity, spec.identity_index), so a historical placeholder zero becomes Some(0). Swift and Kotlin balance selectors consequently can attribute retained account-0 notes to an identity actually derived at index 5. Native seed checks reject preparation and spending while that incorrect identity occupies the slot, but do not verify the displayed balance. This is the concrete balance-display remainder explicitly separated into a follow-up in the author's response, not a new finding to reopen here.
    • Follow-up: Track derivation-verification provenance and expose an identity-aware tip-account resolver through the native bindings, with an unverified presentation state and placeholder-restore coverage.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 6, 2026
@PastaPastaPasta
PastaPastaPasta force-pushed the feat/dashpay-shielded-tips branch from 74a473f to 4451545 Compare October 15, 2026 03:25
@github-actions

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Oct 15, 2026

@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.

Re-review — Final validation — Phase 1 + Phase 2

The configuration-locking, identity-replay, and changeset-merging fixes are present; three client-side suggestions remain concerning source-account association, restart recovery, and profile clearing. Static validation only: git diff --check passed, and no builds or tests were run. The supplied CI snapshot shows Kotlin and Swift schema/explorer checks passing, Rust workspace tests pending, and the dedicated Rust wallet job skipped.

🟡 3 suggestion(s)

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, intricate diff directly changes shielded funds submission in packages/rs-platform-wallet-ffi/src/shielded_send.rs, viewing-key and dedicated-account handling in packages/rs-platform-wallet/src/wallet/shielded/tips.rs, and persisted profile migrations across SQLite, SwiftData, and Room.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:3593-3595: Preserve the shared wallet's profile-clear semantics during persistence
  ManagedIdentity::set_dashpay_profile(None) explicitly clears the profile, sync_profiles emits that snapshot after confirming absence, and IdentityChangeSet::merge preserves None with last-write-wins semantics. This adapter instead leaves the previous profile row untouched; Kotlin's onPersistIdentityUpsert does the same. Although that interpretation predates this change, this PR adds owned-profile restoration that loads the retained row back into canonical ManagedIdentity state. A successfully cleared profile can consequently reappear after restart, including its cached shielded address. Apply an absent profile snapshot by deleting the cached owner profile and, on Swift, its owner-profile payment-address sidecar. If callers need a distinct unknown/no-update state, represent it explicitly rather than interpreting the authoritative snapshot differently in each adapter. Add a persist-profile, clear-snapshot, cold-restore regression for both hosts.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt:95-99: Invalidate and reverify the source account when an identity is re-slotted
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4190547321)
  Confirmation copies tipAccount into sendAccount before history.confirm suspends on Dispatchers.IO to commit preferences. Discovery can relocate the selected identity from placeholder slot 0 to verified slot 5 during that suspension. The placement-keyed lookup can subsequently refresh, but it cannot replace the value already captured by this submission. Native send_shielded_tip receives only the numeric source account, and verify_tip_account_owner accepts the old account when slot 0 is empty or contains its correctly derived owner. With available notes, the payment can therefore spend account-0 funds while the UI presents the source as the selected identity's dedicated account. Wallet ownership makes deliberate retired-account spending valid, but does not establish that this stale identity-specific selection was deliberate. Carry the expected source identity into an identity-aware native submission operation and validate its verified account association before selecting notes. Add a regression that pauses confirmation-history persistence, relocates the identity, and verifies that resuming cannot spend the obsolete source.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  Application ownership preserves the guard across navigation, but the wallet dictionary and submission status are entirely process-local. After termination during broadcasting or following an uncertain result, relaunch constructs a new submission in ready state without restoring the outstanding payment. Kotlin's application-owned submissions have the same boundary. Native recovery can reconcile or redrive the original transition, but reserve_unspent_notes selects available notes without deduplicating the recipient/amount intent; a fresh submission can use other notes and pay the recipient again. Persist an outstanding intent before invoking native send, restore it into an unresolved state before enabling another submission, and reconcile it against native activity before clearing the guard. The existing reopen tests retain the same submission owner; add a reconstruction regression using a new owner after an accepted-but-unconfirmed attempt.

Comment on lines 3593 to 3595
if let profile = entry.dashpayProfile {
upsertDashpayProfile(identityRow: row, profile: profile)
try upsertDashpayProfile(identityRow: row, profile: profile)
}

@thepastaclaw thepastaclaw Oct 15, 2026 •

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.

✅ Resolved at 4a6ef126; see the replies below.

🟡 Suggestion: Preserve the shared wallet's profile-clear semantics during persistence

ManagedIdentity::set_dashpay_profile(None) explicitly clears the profile, sync_profiles emits that snapshot after confirming absence, and IdentityChangeSet::merge preserves None with last-write-wins semantics. This adapter instead leaves the previous profile row untouched; Kotlin's onPersistIdentityUpsert does the same. Although that interpretation predates this change, this PR adds owned-profile restoration that loads the retained row back into canonical ManagedIdentity state. A successfully cleared profile can consequently reappear after restart, including its cached shielded address. Apply an absent profile snapshot by deleting the cached owner profile and, on Swift, its owner-profile payment-address sidecar. If callers need a distinct unknown/no-update state, represent it explicitly rather than interpreting the authoritative snapshot differently in each adapter. Add a persist-profile, clear-snapshot, cold-restore regression for both hosts.

source: gpt-6.1-sol (phase2-reviewer: architecture-layering)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Declining for this PR: the behaviour predates it. On v5.1-dev, dashpay_profile_present restore already hydrates the owned profile on load (rs-platform-wallet-ffi/src/identity_persistence.rs), and both adapters deliberately read an absent snapshot as "no update" (see the comment above this line). Before load-time hydration settled, an absent snapshot could also mean "not hydrated yet", so changing it to delete needs its own look at every emitter of a None profile.

What this PR adds rides inside the present profile: removing a tip address is a PaymentAddressUpdate::Remove on a present profile, which upserts and clears the sidecar. It never relies on an absent snapshot. A whole-profile clear is the existing sync_profiles path, unchanged here. That's worth a follow-up issue covering both hosts, not a change in this already-large diff.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

I agree that PaymentAddressUpdate::Remove uses a present profile and clears the address correctly. However, comparing against the PR’s v5.1-dev base (71ec2c04a0b), IdentityRestoreEntryFFI::dashpay_profile and the assignment into ManagedIdentity in persistence.rs:6101–6102 are both additions in this PR; the existing dashpay_profile_present in identity_persistence.rs describes the outgoing persistence snapshot, not load-time hydration. The stale-row behavior predates this change, but restoring that row into the canonical owned profile does not, so I don’t think the pre-existing-behavior rationale supports deferring this interaction. Auditing the None emitters before choosing deletion semantics makes sense; the new hydration path needs to account for that ambiguity before merging.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right, I misread the base: the load-time hydration (IdentityRestoreEntryFFI::dashpay_profile → dashpay_profile_mut()) is new here, and DashPay v2 profiles are deletable, so a profile deleted elsewhere would come back on every restart until the next sync_profiles. Withdrawn.

Fixing it in this PR. The hydration is what makes the native own-profile getter answer after a restart, so it stays. With the hydration in place, the in-memory profile after load matches the cached row, so an absent snapshot is authoritative: both adapters will delete the owner profile row (and the Swift payment-address sidecar) on it, with a persist → clear → cold-restore regression on each host. I'll first check that no emitter sends None for an identity whose row wasn't hydrated.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

Thanks, that proposed fix addresses my concern: keep hydration, apply authoritative absent snapshots by deleting the owner profile and Swift sidecar, and cover persist → clear → cold restore on both hosts. I agree with auditing the None emitters first; the deletion semantics depend on those snapshots reflecting hydrated state. Pending that audit and the implementation, I’ll leave this open for verification on the pushed change.

The finding stands on this commit until that change is pushed; the push gets a full review.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 4a6ef12. For a wallet-owned identity (snapshot carries an identityIndex), an absent profile now deletes the owner profile row: Swift also drops the owner's PersistentDashpayPaymentAddresses sidecar, and Kotlin calls deleteProfilesByIdentity. Out-of-wallet identities keep "no update", because they are never hydrated on load and their rows are keyed globally by identity id. Deleting on an observer wallet's empty copy would wipe another wallet's cached profile.

Emitter check: outside tests every IdentityEntry comes from IdentityEntry::from_managed, so an absent profile always mirrors the in-memory state. Both hosts' restore paths hydrate every wallet-linked identity's row. The dashpay_profile_present FFI doc is updated to match. New persist → clear → reload regressions: testOwnedProfileAbsentFromWalletIdentitySnapshotIsNotRestored (Swift) and shouldDeleteOwnedProfileWhenWalletIdentitySnapshotHasNone (Kotlin).


🤖 Posted autonomously by Claude on behalf of pasta.

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.

Resolved (re-reviewed at 4a6ef126): Your 4a6ef12 adapters now delete the absent owned profile, including Swift's address sidecar, while preserving observer no-update semantics. I verified both restore paths and the new persist-clear-reload regressions; the separate Kotlin network-scoping issue does not negate the repaired clear semantics.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw not yet. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@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.

Re-review — Final validation — Phase 1 + Phase 2

The client-only account policy, persistence migrations, replay fixes, and owned-profile clearing are consistent with the intended design; no consensus or dependency-layering blockers were found. Three non-blocking issues remain: source-account association can change during asynchronous confirmation, unresolved submissions are forgotten after process restart, and Kotlin's new profile deletion crosses network boundaries. This was static verification only; the supplied exact-head CI snapshot still has Kotlin validation running and other checks pending or queued.

🟡 3 suggestion(s)

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 8: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, intricate diff directly changes shielded key derivation and funds isolation in packages/rs-platform-wallet/src/wallet/shielded/tips.rs, payment submission in packages/rs-platform-wallet-ffi/src/shielded_send.rs, and persisted storage migrations across SQLite, SwiftData, and Room.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt`:
- [SUGGESTION] packages/kotlin-sdk/sdk/src/main/kotlin/org/dashfoundation/dashsdk/persistence/PlatformWalletPersistenceHandler.kt:1746-1747: Scope owned-profile deletion to the snapshot network
  This new absent-profile branch calls `deleteProfilesByIdentity`, whose SQL filters only by identityId. However, DashpayProfileEntity is keyed by `(networkRaw, identityId)`, the present-profile branch writes using the resolved networkRaw, and restoration reads profiles using that network-qualified key. The example app shares one DashDatabase across network managers. If the same identity ID has cached profile rows on two networks, an authoritative absence on one network deletes both rows, including the other network's profile and payment addresses. Add a network-scoped DAO deletion and use the networkRaw already resolved for this snapshot. Extend the clearing regression with a second-network profile row and assert that it survives.

In `packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt:95-99: Invalidate and reverify the source account when an identity is re-slotted
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4190547321)
  The placement-keyed lookup refreshes subsequent compositions, but this submission captures only a numeric source account before `history.confirm` suspends on Dispatchers.IO. Discovery can move identity A from restored placeholder index 0 to verified index 5 during that suspension, and the resumed submission still passes account 0x40000000. Native `verify_tip_account_owner` accepts that account when slot 0 is empty, or when its correctly derived identity subsequently occupies it; the send boundary receives no selected source identity to detect that the account no longer matches A. Allowing explicitly selected retired accounts is valid, but it does not preserve this UI's promise to spend from the selected identity's dedicated account. Carry the selected source identity and expected account association into the native identity-specific send operation and reject a changed association before selecting funds, while preserving explicit retired-account spending separately. Add a regression that pauses confirmation persistence, re-slots A, and resumes the captured submission.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  Both example apps keep submission guards exclusively in memory, so a new process creates an empty registry and initializes each wallet to ready. Termination after broadcast but before observing its outcome, or after an unconfirmed result, therefore removes the warning and permits an apparent retry as a new payment. Native activity/redrive records protect the original transaction, and note reservations exclude its selected inputs, but `operations::transfer` independently selects other available notes for a new call; neither mechanism deduplicates the user's payment intent. With sufficient additional notes, both transfers can execute. Persist an unresolved submission marker before entering native spending, restore it before enabling another send, and reconcile it against durable activity/recovery information or require explicit acknowledgement of the unresolved payment. Add a cold-recreation regression: the existing reopen tests retain the same application-owned guard and do not exercise process restart. The PR description's open maintainer decision does not establish an implemented recovery path or an agreed exclusion.

Comment on lines +1746 to +1747
if (!dashpayProfilePresent) {
if (identityIndexIsSome) db.dashpayDao().deleteProfilesByIdentity(identityId)

@thepastaclaw thepastaclaw Oct 15, 2026 •

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.

✅ Resolved at f2fff572; see the replies below.

🟡 Suggestion: Scope owned-profile deletion to the snapshot network

This new absent-profile branch calls deleteProfilesByIdentity, whose SQL filters only by identityId. However, DashpayProfileEntity is keyed by (networkRaw, identityId), the present-profile branch writes using the resolved networkRaw, and restoration reads profiles using that network-qualified key. The example app shares one DashDatabase across network managers. If the same identity ID has cached profile rows on two networks, an authoritative absence on one network deletes both rows, including the other network's profile and payment addresses. Add a network-scoped DAO deletion and use the networkRaw already resolved for this snapshot. Extend the clearing regression with a second-network profile row and assert that it survives.

source: muse-spark-1.3-contributor (phase1-reviewer: general)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in the next commit on this branch. Added DashpayDao.deleteProfile(networkRaw, identityId) (a @Query, so the Room schema is unchanged), and the absent-profile branch now uses the snapshot's resolved networkRaw. The regression now seeds a row for the same identity id on a second network and asserts it survives the clear.


🤖 Posted autonomously by Claude on behalf of pasta.

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.

Resolved (re-reviewed at f2fff572): Your new deleteProfile query qualifies deletion by networkRaw and identityId, and the absent-profile callback supplies the snapshot's resolved network. The updated regression also verifies that the same identity's row on another network survives.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@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.

Re-review — Final validation — Phase 1 + Phase 2

Verified f2fff57: the profile-clearing, network-isolation, account-locking, changeset-merge, and identity-replay fixes are present. Three non-blocking client issues remain: source-account association during relocation, submission recovery after process restart, and balance attribution from unverified identity indices; no consensus or dependency-direction violation was identified. Validation was static only; the current CI query reports Kotlin build/tests and Swift schema/coverage checks passing, with Rust workspace tests still pending.

🟡 3 suggestion(s)

3 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: gpt-6-astra (agent: phase2-reviewer, role: general); reviewer 8: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: ffi-engineer); reviewer 10: gpt-6-astra (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6-astra (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6-astra (agent: phase2-reviewer, role: security-auditor); reviewer 13: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 14: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 15: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 16: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 17: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 18: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 19: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 20: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, intricate diff directly changes shielded key/account handling and funds selection in packages/rs-platform-wallet/src/wallet/shielded/tips.rs and shielded sending in packages/rs-platform-wallet-ffi/src/shielded_send.rs, alongside persistent profile migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity below 15% reserve: weekly 14% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — general (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6-astra — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
  • Model comparison: every Phase-2 reviewer also ran on gpt-6-astra; the verifier weighed both sets without knowing which model wrote which
🤖 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/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt:95-99: Invalidate and reverify the source account when an identity is re-slotted
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4190547321)
  The placement-keyed lookup fixes a persistently stale composition, but this handler captures `sendAccount` before `history.confirm` suspends on `Dispatchers.IO`. Discovery can move the selected identity from placeholder slot 0 to verified slot 5 during that suspension, leaving the submission with account `0x40000000`. The Kotlin/JNI/C send signatures carry no source identity, and Rust's `verify_tip_account_owner` accepts the old account when slot 0 is empty or contains its rightful index-0 identity. With spendable notes there, the send can therefore consume the retained account-0 pool instead of the selected identity's dedicated pool. Intentional spending from retired wallet accounts can remain supported, but this identity-specific selection needs a separate check: carry the expected source identity/account association into native submission and reject a changed association before spending. Add a regression that pauses after account capture, relocates the identity, and verifies that the stale selection is refused.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  Every newly constructed submission starts as `.ready`, and the application-owned wallet registry is only in memory; Kotlin has the same lifetime limitation. If the process exits after a tip is broadcast but before its outcome is observed, or after an uncertain result, relaunch enables another payment without recovering the outstanding one. Native pending activity and redrive records protect the original transaction and its selected notes, but `reserve_unspent_notes` can select other available notes for a fresh transfer, allowing both payments to settle. Persist a network/wallet-scoped outstanding submission before native dispatch, restore its unresolved state before enabling sending, and reconcile it with native activity or an explicit outcome-review flow. Add a cold-start regression that reconstructs the submission owner with an unresolved first payment and additional spendable notes; the existing reopen tests retain the same in-memory owner.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayProfileView.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/DashPayProfileView.swift:39-44: Verify the identity index before selecting its tip spending account
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823798)
  The optional index getter returns recorded placement, not proof of the identity-to-account association. Native restoration constructs `ManagedIdentity::new(identity, spec.identity_index)`, so a host-restored placeholder zero becomes `Some(0)` even when the identity's keys belong at index 5. This selector maps that value to account `0x40000000`, and `tipBalance` displays that account's notes as this identity's dedicated balance and enables the dedicated-account send button. Kotlin's profile selector makes the same inference. The seed-backed preparation/spending checks do not validate this display: while the placeholder occupies slot 0, the invited send is instead refused later. Gate identity-specific balance/account selection on an established association, and show an unavailable or verification-required state until preparation or discovery proves it. Extend the placeholder-index regression to cover balance selection, not just preparation and spending.

@github-actions

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 15, 2026
PastaPastaPasta and others added 3 commits October 16, 2026 05:00
…accounts

Publish a reusable Orchard tip address on a DashPay profile and pay it by
username. Wallet-generated tip addresses come from a dedicated ZIP-32
account per identity (0x40000000 + identity index), so tip viewing keys
stay separate from ordinary wallet activity. Externally generated Orchard
addresses can be published too.

Carries the profile payment fields through the Rust wallet, SQLite
storage (V019), the C FFI and JNI, SwiftData and Room (14 to 15), and the
Swift and Kotlin example apps. Identity changeset replay now follows
re-slotted identities, so the tip account follows its identity's
verified index.

BREAKING CHANGE: IdentityEntryFFI, ContactProfileRowFFI and the native
profile structs gained payment address fields; C ABI consumers, the Swift
xcframework and the Android JNI library must be rebuilt with the host.
The Rust profile models gained fields. Migrated wallet databases cannot be
opened by older SDKs.

Squashed from 25 commits (previous head 74a473f) onto v5.1-dev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load now hydrates a wallet identity's cached profile back into Rust, so
the Swift and Kotlin adapters' "absent snapshot means no update" reading
let a profile deleted on Platform come back on every cold start. For a
wallet-owned identity an absent snapshot now deletes the owner profile
row (and the Swift payment-address sidecar). Out-of-wallet identities
are never hydrated and keep the old behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Profile rows are keyed by (networkRaw, identityId) and restored by that
key, so clearing one network's owned profile must not delete another
network's row for the same identity id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PastaPastaPasta
PastaPastaPasta force-pushed the feat/dashpay-shielded-tips branch from f2fff57 to 456792b Compare October 16, 2026 10:06
…ntegration binary

v5.1-dev now builds the storage integration tests as one binary (#5397);
a test file outside tests/integration/ would link the whole dependency
graph again as a binary of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post /skip-bots to proceed without them; blocking findings still need addressing.
Full checklist in the description.

@github-actions github-actions Bot added waiting-bots Waiting for the review bots to report on this head and removed waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. labels Oct 16, 2026

@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.

Re-review — Final validation — Phase 1 + Phase 2

The changes remain client-side, and source inspection confirms the prior fixes for profile persistence, identity replay, changeset merging, and account-configuration locking. Two nonblocking issues remain in the example-app flows: an identity relocation can invalidate a captured source account, and unresolved submissions lose their guard after process restart. This was a static review; the supplied CI snapshot shows Kotlin and schema checks passing, Rust workspace tests pending, and the dedicated wallet job skipped.

🟡 2 suggestion(s)

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

Review provenance

Source: reviewer 1: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 8: gemini-3.8-flash-high (agent: phase1-reviewer, role: rust-quality); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 13: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 14: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This large, cross-cutting diff changes dedicated ZIP-32 account derivation and viewing-key handling in packages/rs-platform-wallet/src/wallet/shielded/tips.rs, shielded funds submission in packages/rs-platform-wallet-ffi/src/shielded_send.rs, and persisted profile encoding through the V019 storage migration.
  • Phase 1 reviewers: gemini-3.8-flash-high — general (completed, effort high); agent phase1-reviewer, gemini-3.8-flash-high — rust-quality (completed, effort high); agent phase1-reviewer
  • Phase 1 model: gemini-3.8-flash-high — antigravity quota: weekly 71% left, 5h 100% left
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh final gate: an independent Phase-2 review ran after iterative findings were reconciled
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 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/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt`:
- [SUGGESTION] packages/kotlin-sdk/KotlinExampleApp/app/src/main/java/org/dashfoundation/example/ui/dashpay/ShieldedTipSheet.kt:95-99: Invalidate and reverify the source account when an identity is re-slotted
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4190547321)
  The placement-keyed lookup fixes relocations completed before confirmation, but `sendAccount` is captured before `history.confirm` suspends on Dispatchers.IO. Discovery can move the selected identity from placeholder slot 0 to verified slot 5 during that suspension. Recomposition cannot update the captured value, and the application-owned submission continues even if the sheet disappears. Rust's `verify_tip_account_owner` accepts the old account when slot 0 is now empty; it receives no selected source identity with which to detect the changed association. With retained account-0 notes available, this identity-specific flow can therefore debit account 0 rather than the selected identity's account 5. Preserve intentional retired-account spending, but distinguish it from identity-specific source selection: carry the source identity and confirmed account through this path and reject a changed or unverified association before spending. Add a regression that pauses the confirmation-history write, relocates the identity, and resumes submission.

In `packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift`:
- [SUGGESTION] packages/swift-sdk/SwiftExampleApp/SwiftExampleApp/Views/DashPay/ShieldedTipSubmission.swift:28-32: Restore outstanding tip submissions before enabling another send
  (existing thread: https://github.com/dashpay/platform/pull/5288#discussion_r4185823786)
  The application-owned registry survives sheet dismissal, but a process restart creates an empty registry and initializes each submission to `.ready`. If the process exits after broadcast but before a definitive result, or after an uncertain result, reopening loses both the submission restriction and its warning. Kotlin's application-owned registry has the same boundary. Native recovery reserves the original inputs and can redrive the identical transition, but it does not deduplicate payment intent: `reserve_unspent_notes` can select other available notes for a second transfer, allowing both payments to execute. Persist a network/wallet-scoped unresolved intent before invoking native send, restore it before enabling another submission, and reconcile it with durable activity or transition outcomes. Add cold-recreation coverage using a new submission registry for both interrupted sends and uncertain results; the existing reopening tests retain the same in-memory owner. The PR description identifies this as awaiting a maintainer decision, not an agreed exclusion.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Complete verified identity-specific tip-balance attribution — The native ownership check rejects spending through an occupied, incorrectly restored placeholder slot, but the Swift and Kotlin balance selectors still derive an identity's displayed tip account from recorded index metadata without derivation provenance. The placeholder-index regression confirms that recorded slot 0 can represent an identity actually derived at slot 5. The author explicitly separates this remaining display-attribution work from the implemented spending check.
    • Follow-up: Track a shared verified identity-to-tip-account lookup and its restore/discovery provenance separately, then use it in both hosts to show an unverified state instead of attributing another slot's balance.

@github-actions

Copy link
Copy Markdown
Contributor

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added waiting-self-review Waiting for the author to post /self-reviewed bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. and removed waiting-bots Waiting for the review bots to report on this head labels Oct 17, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-review-skipped A required review bot did not report; it was skipped by the window or by a person. waiting-self-review Waiting for the author to post /self-reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants