Repository navigation
Conversation
V019 and the per-round history repair decoded stored records strictly, so one undecodable record_blob aborted the migration (blocking every open, Recovery included) and made each later write of that txid fail. History repair is best-effort accounting: unreadable stored data is now logged and skipped. Each record is repaired inside its own savepoint so a skipped one leaves no partial writes, the corrupt row is left untouched, and a later write of the same transaction replaces it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V019 and the per-round history repair rewrote stored transaction records in place and marked owned inputs spent without saying which transaction spent them, so a wrong repair could not be undone after the one-off pre-migration backup. Before its first rewrite, a record's original blob is now copied verbatim into the append-only core_transaction_record_originals table (V019 is unreleased and edited in place), and a repair's spent mark records its spender in spent_in_txid. Automatic release after a reorg is left as a TODO pending verification of upstream reorg handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
V019 called the live per-round history repair, so any later edit to that code (or to the queried tables) would silently change what V019 does for users upgrading from V018 or earlier. Move V019's repair into the migrations-local legacy_v019 module, following the legacy_v008 precedent, with its own record reader. Only the low-level blob codec stays shared; TransactionRecord's encoding is owned upstream. A pinning test fixes V019's result on a V018-shaped database: the repaired record bytes, the preserved original, the input index and the spent marks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on load Load replayed only block-confirmed records, so a mempool or InstantSend spend never reached the account's spent set. A redelivered funding transaction (rescan, reorg re-connect, or the funding confirming after a restart) then re-credited the output that spend reserves, and the round wrote it back to SQLite as unspent. Replay unconfirmed records after the confirmed ones, parents before children, so the checker skips re-crediting reserved outputs. The persisted-unspent filter still keeps their own outputs from being credited unless persistence holds them. The regression tests now redeliver funding after reload, for confirmed and still-unconfirmed funding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idge The wallet checker is async only by trait shape and never awaits, yet load drove it through dash_async::block_on, which spawns a thread and runtime on current-thread runtimes, and surfaced bridge failures through a new public WalletStorageError::CoreHistoryReplay variant (a breaking change, as the enum is exhaustive). Poll the replay once instead. If a future upstream checker ever suspends, load logs an error and keeps the pre-replay projection rather than failing the wallet; a unit test pins first-poll completion so such a change fails in CI. This removes the variant and the dash-async dependency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Swift accounting reconcile and the SQLite history repair disagreed on direction: Swift reported an asset lock as internal even when an output left the wallet, and a spend with no remaining outputs as internal. Swift now uses the Rust repair's rule (internal only when nothing leaves the wallet and something stays in it, or an asset lock burns into Platform). The Rust rule moves into a pure helper, and both sides test the same case table. The upstream rust-dashcore recompute stays out of scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The asset-lock fixtures used an empty-script output as the burn, which the aligned direction rule rightly treats as leaving the wallet. Use a real OP_RETURN so the fixtures match what an asset lock carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
netAmount(for:) returned the stored scalar through its single-wallet fast path before checking for unresolved inputs, and the accounting reconcile counted an address-matched output of any local wallet, so a transfer to another local wallet whose TXO was not linked yet showed the sender only part of what it paid. Unresolved inputs now make every amount provisional, and an unlinked address-matched output counts only when it belongs to a spending wallet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard that keeps a funded asset lock's Core debit and fee from being overwritten by a context-only zero update required an already-linked owned input. With every prevout still pending, the synthetic update erased the debit and fee, and reconcile could not restore them. A stored negative amount is itself the proof the wallet funded the lock, so the guard now keys on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
loadWalletList's history accounting pass returned a load failure on any fetch, reconcile or save error, so a problem in display-only amounts left every wallet unrestored. It also fetched a PersistentCoreAddress per unlinked output. A failed pass now rolls back its own edits, logs a distinct event and lets the restore continue; addresses are read once into a lookup. A persisted completion marker is deferred (TODO) because it needs a SwiftData shape change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Transaction views treat an unresolved per-wallet amount as unavailable everywhere, so the fee no longer shows beside "Amount unavailable". - A failed accounting reconcile no longer fails the persistence round and is reported as persistence_transaction_accounting_failed, not save_failed. - Name the Swift transaction-kind and direction values instead of 1/3/6. - Rename the verdict test that never covered Uncredited and add one that does; V019's confirmed-spend marking is pinned by the V019 fixture test. - Drop the hand-written Unreleased section from the generated CHANGELOG. - Mark the divergent record-coalescing helpers with a TODO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… restores it Rework of 63a7f8e, which skipped undecodable records everywhere. Normal operation is strict again: the per-round repair and preserve_known_details treat a corrupt stored record as an error. V019 (which has no recovery mode) drops an undecodable record only when a Core resync re-delivers it: the row has a block height, so a filter rescan finds it again. It then deletes the row and its input-index rows and lowers that wallet's synced_height to just below its birth height; load hands that checkpoint to SPV, which rescans from birth and re-records the transaction. Spent marks and outputs it already produced are kept until the rescan confirms them. An unconfirmed corrupt record cannot be restored that way, so the migration still fails and rolls back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: OpenAI Codex <noreply@openai.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
✅ Final review complete — no blockers (commit ce03450) |
…wallet-restore # Conflicts: # CHANGELOG.md
…llet-restore Merges b30a09e. Twelve of this branch's thirteen commits were an early snapshot of #5208, which has since landed in the base as a squash and kept evolving there. Only 10b15c9 is this branch's own work. Resolutions that are not obvious from the diff: - Twelve conflicted files take the base version verbatim (CHANGELOG.md, legacy_v019.rs, rehydrate.rs, core_history.rs, core_state.rs, sqlite_spent_rehydration.rs, changeset.rs and the five Swift files). Every hunk on our side came from the stale #5208 snapshot. The add/add on legacy_v019.rs was two generations of the same file, not a second migration: this branch defines no schema change, so the base's frozen V019 is untouched and no later migration is needed. - persister.rs keeps the base's load path (replay, sweeps, reload, spent claims) and re-applies this branch's additions on top: the WALLET_RESTORE attestation, the restore_pools call ahead of the replay, and typed errors at the three reconstruction sites instead of string-wrapped backend errors. - The whole-load read transaction from 10b15c9 is dropped. Git merged it cleanly next to the base's per-wallet BEGIN IMMEDIATE, which nests two transactions on one connection and fails every wallet load. The base's per-wallet transaction already gives one wallet one snapshot, so load() is the base's version again. - should_read_one_snapshot_despite_a_concurrent_commit becomes should_reject_a_concurrent_writer_while_a_wallet_load_transaction_is_open. Under the base's write lock the second connection is refused rather than isolated, so the test now asserts the busy rejection from inside the load's own read, keeps the synced_height assertion, and checks that the same write lands once the load is over. The Swift files are the base's, unchanged, and were not compiled here. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`restore_pools` reads `reserved_at` for every pool, so the two `TODO(#4188)` notes saying it is deliberately not consumed are no longer true. They now say where the reservation state is restored instead. The DPNS name-state docs in platform-wallet justified their session-scoped map by saying the SQLite backend does not attest `WALLET_RESTORE` and that `ClientStartState::wallets` is unimplemented. Both are false now. The map is still not rehydrated, because `WALLET_RESTORE` covers the Core wallet snapshot only, and the docs say that. Comments only. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two behaviours of the SQLite wallet restore had no test of their own. A failure while rebuilding a wallet now reaches the caller as the storage error that caused it. A wrong-curve key in the platform-node pool is returned typed under Strict, and under Recovery the dropped wallet is attributed to `blob_decode` instead of an anonymous `backend`. `restore_pools` overlays a persisted row on a pool entry that already exists, and a repaired wallet is rebuilt twice in one load. The entry's transaction count, totals and balance come through unchanged across both rounds. The existing reopen test compares that accounting before and after a restart, but key-wallet never updates those fields, so it only ever compared zeroes. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Static verification at the exact head confirms three pool-restoration correctness gaps, plus targeted coverage and startup-cost improvements. These client-side issues do not violate consensus invariants and are classified as suggestions under the supplied severity policy. No builds or tests were run; the supplied CI snapshot shows successful completed checks but no dedicated Rust wallet-storage test result.
🟡 6 suggestion(s) | 💬 1 nitpick(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); 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: rust-quality); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The intricate restore_pools implementation in packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs changes key handling by reconstructing typed key pools, derivation indices, used-key positions and funding-address reservations. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-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 verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-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-storage/src/sqlite/persister.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/persister.rs:1881-1882: Apply the UTXO-derived reuse guard after restoring persisted pools
The overlay runs after `apply_persisted_core_state` has already applied the used-address set to the reconstructed pools. A valid Standard external-pool snapshot with indices 0–39 Reserved and index 40 Available, followed by a recordless UTXO on index 40, exposes the ordering problem: initial derivation covers 0–29, and discovery stops at index 30 because no used address anchors a deeper horizon. The overlay then inserts index 40 as Available because there is no existing entry whose Used state it can preserve. Recordless UTXOs are supported by the writer, and history replay returns immediately when there are no records, so the next allocation can return the funded address. Restore pools before applying the reuse guard, or reapply the known-used address set after the overlay. Add a reopen regression with reservations beyond the initial gap and a recordless UTXO on a saved available address.
In `packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:605-615: Validate derivation for entries outside the already-generated pool
Address/key consistency is checked only when the reconstructed pool already contains the persisted index. Special funding-key pools initially derive just their gap window and are not extended by the funds-account projection, so deeper persisted entries bypass this check. A corrupted row carrying another child's valid P2PKH script and correctly sized ECDSA key is accepted and assigned the recorded index's derivation path. The pinned key-wallet allocation methods return cached Available entries without deriving them again, while signing derives from the recorded path; the wallet can therefore return an address its corresponding signing key does not control. Pass the registered account key material into restoration and independently derive and compare every publicly derivable persisted index before insertion, including indices absent from the skeleton. Preserve explicit handling for hardened entries that cannot be regenerated from public material, and test valid-width beyond-window mismatches under Strict and Recovery.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:503-505: Reject persisted rows whose pool discriminator cannot be restored
Selecting rows separately for each reconstructed pool means rows with an incompatible discriminator are never validated. For example, changing a reserved `identity_invitation` row from Absent (2) to External (0) passes the database CHECK constraint, but this account exposes only an Absent pool. The overlay skips the row, and the earlier used-address reader also skips it because `used = 0`. Strict load succeeds with the skeleton entry Available, silently losing the reservation and allowing the same funding key to be allocated again. Validate that saved rows for registered accounts map to an actual managed pool and return a typed error for incompatible discriminators. Add a regression proving that an impossible pool discriminator cannot discard a reservation during a successful Strict load.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:660-663: Compute the highest used index once per restored pool
The pinned dependency stores `used_indices` in a HashSet, so `iter().max()` scans the accumulated set on every restored row, including unused rows. A pool with N used entries therefore incurs quadratic work during restoration while the connection mutex and per-wallet IMMEDIATE transaction remain held. No used index is removed here, so maintain the maximum incrementally or compute it once after the row loop.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:605-625: Exercise the new pool-conflict guards under Strict and Recovery
The new account/key-type mismatch, derived address-or-key conflict and duplicate-address-across-indices errors have no direct regression coverage. The malformed-key test exercises a width check, and the provider wrong-curve test fails earlier in `restore_provider_platform_node_pool`, so neither verifies these overlay guards or their policy handling. Add fixtures that reach each guard: assert Strict preserves the typed storage error, and Recovery isolates the affected wallet with the correct degradation kind while retaining a healthy sibling wallet.
- [NITPICK] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:500: Reuse the identical restore statement across pools
Every account/pool iteration prepares the same SELECT text with different bound parameters. Using `prepare_cached`, as the repeated-shape writer and ownership query already do, avoids repeated SQL preparation during wallet loading. This is a small startup optimization, separate from the per-row quadratic scan.
In `packages/rs-platform-wallet-storage/tests/sqlite_wallet_restore.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/tests/sqlite_wallet_restore.rs:160-179: Add DashPay contact-pool restore coverage
The reopen test creates no DashPay accounts, so the new restore query's user/friend identity bindings are exercised only with zero sentinels. Existing contact tests cover writer separation and registration migration, not restoration of used and reserved pool entries for multiple contacts. Two contacts can share the same account label, numeric index and key class, making the identity pair essential to selecting the correct saved pool. Add a reopen test with two contacts at the same numeric index and distinct non-zero identity pairs, then assert their used states, reservation timestamps and addresses restore independently without cross-contact overwrite.
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.
- Validate restored HD entries through the key-wallet owner — The confirmed beyond-window validation defect is merged into 'Validate derivation for entries outside the already-generated pool'. Requiring an upstream restoration API and dependency-pin update is not established as necessary; the essential requirement is independent validation using the account key material.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
A restored pool entry is an address the wallet hands out next. Rows inside the freshly derived window were compared with the derived entry, but a row past that window had nothing to be compared with and was accepted as stored, under the derivation path of its recorded index. A damaged or rewritten row could make the wallet offer an address it cannot sign for. `restore_pools` now derives each pool from the account's public key through its highest stored index before overlaying, so every row meets an entry the wallet derived itself and the existing conflict check applies to all of them. A row left without a derived entry is refused. Deriving costs one key per index, so a stored index past `MAX_REHYDRATION_DERIVATION_INDEX` is refused before anything is derived. Hardened pools are unchanged: their children cannot be derived from public key material, so the pre-derived platform-node keys stay trusted as stored. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
Pool snapshots are not rewritten when an address receives funds, so a stored row can say "available" for an address `core_utxos` holds a coin on. The used-address projection covers that, but it ran before the pools were restored and can only mark addresses a pool already holds. An address past the freshly derived window, with no used address near enough to anchor the discovery walk, was then inserted as available by `restore_pools` and became the next address handed out. Pools are now restored before `apply_persisted_core_state`, so its mark-used and gap-refill pass sees every restored entry. `restore_provider_platform_node_pool` moves with it. It rewrites its entries' state from the `used` column alone, so it has to run before `restore_pools` overlays their reservations; left behind it would erase them. It touches only the platform-node key account, which holds no funds and is not read by the projection it now precedes. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…lacks `restore_pools` reads rows pool by pool, so a row whose `pool_type` names a pool its account does not have was read by nobody. An unused row is also skipped by the used-address reader, so a Strict load succeeded and the reservation the row carried was gone: the same funding key could be handed out again. The stored pool types of each account are now checked against the pools the account really has, and a row outside them fails the wallet with a typed error. No writer produces such a row; it takes a damaged store. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
`restore_pools` recomputed `highest_used` by scanning the whole `used_indices` set after every row, used or not, which is quadratic in a pool's used entries while the connection mutex and the wallet's load transaction are held. It also compiled the same three statements again for every account and pool. The highest used index is now raised as used rows are inserted, and the statements are prepared once per call. The restored pools are unchanged; the reopen test now compares `highest_used` with the pool that was stored. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
The reopen test had no DashPay account, so the identity pair that tells two contacts' stored pools apart was only ever bound to zeroes. Two contacts at the same index, with different states at the same pool indices, now come back from a reopen each under its own pair. The overlay's conflict guards had no test that reached them. Each one now has a fixture that does: a key of the wrong curve for its account, an address and a key that differ from the derived entry, and a platform-node address stored at two indices. Strict returns the guard's typed error; Recovery drops that wallet alone and keeps its healthy sibling. The duplicate-index guard can no longer fire for pools the wallet derives itself, where every row is now compared with its derived entry first. It is what still protects the hardened platform-node pool, whose rows cannot be re-derived. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
The two statements the previous commits added to `restore_pools` called `prepare`, which `tc_p1_003_prepare_cached_in_writers` rejects for any statement in the schema modules that is not an allow-listed one-shot read. These are repeated-shape statements, bound once per account and pool, so they belong in the statement cache rather than on the allow-list. All three restore statements now use `prepare_cached`. The row query only passed the check before because its text starts like the allow-listed typed-pool reader's. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
All seven prior findings are fixed at the reviewed head. Three non-blocking findings remain: unmatched pool coordinates can silently discard reservations, the restoration ceiling rejects valid snapshots the writer accepts, and beyond-window BLS restoration lacks regression coverage. This was a static review without builds or test execution; the supplied CI snapshot does not independently confirm the reported wallet-storage test and clippy results.
🟡 3 suggestion(s)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 4: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The intricate restoration logic in core_pool.rs::restore_pools and derive_through changes key handling by validating persisted public keys against derived entries and restoring reservation and used-key positions that determine which funding addresses can be handed out again. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-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; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-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-storage/src/sqlite/schema/core_pool.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:564-568: Reject pool rows with impossible account discriminators
The pool-type preflight and both restoration queries filter by the reconstructed account's complete coordinates before examining saved rows. A reserved IdentityInvitation row with a nonzero key_class satisfies the SQLite schema but cannot match any IdentityInvitation account: accounts::account_key_class always returns zero for that variant. With no transaction history or UTXO on the address, the used-address reader also excludes the row because used is false. Strict loading consequently succeeds while leaving the reconstructed address Available, silently losing its reservation and permitting allocation again. Validate saved pool coordinates against the reconstructed account set before applying these filters, returning a typed error for impossible or unmatched coordinates. Add Strict and Recovery coverage that verifies rejection and isolation of the affected wallet.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:525-529: Preserve restoration compatibility for valid indices above the discovery ceiling
The documented 10,000-index bound protects restoration from unbounded sequential derivation, but it also introduces a store/load incompatibility. Neither the core_address_pool schema nor apply_pools restricts valid child indices to this ceiling, so a correctly derived Standard pool snapshot containing index 10,001 can still be stored successfully. On reopen, this new check fails Strict loading and excludes the entire wallet under Recovery. The base loader accepted such databases and deferred unresolved deep addresses to synchronization rather than rejecting the wallet. Preserve bounded validation without treating supported historical indices as corruption, or explicitly reconcile the write contract and compatibility policy with the new restriction. Add a reopen regression using correctly derived entries just above the ceiling; the existing i32::MAX corruption test does not cover this case.
In `packages/rs-platform-wallet-storage/tests/sqlite_wallet_restore.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/tests/sqlite_wallet_restore.rs:132-135: Exercise BLS pool restoration beyond the initial derivation window
This fixture obtains key material only through account_of_type, so ProviderOperatorKeys takes the fallback branch instead of generating beyond-window entries. It changes only indices 0 and 1 in the initial operator pool, allowing derive_through to return before exercising the new KeySource::BLSPublic expansion path. The provider-history test also uses operator index 0, and the post-reopen allocation assertion here is skipped when source is None. Obtain the operator source through bls_account_of_type and KeySource::BLSPublic in both source-selection sites, then persist and reopen an operator reservation beyond its initial window. This lets the existing state, address, path, key, and non-reuse assertions detect regressions in BLS pool expansion.
958e930 refused any pool whose highest stored index lay past 10,000. That was a regression: neither the schema nor the writer limits an index, so a pool the wallet had really grown that far — one funding key per invitation or top-up adds up — was stored row for row and then failed every Strict load, and was dropped whole under Recovery. Depth is not what marks a damaged index; sparseness is. The derivation that checks a pool's rows is now budgeted by the pool's own row count plus an allowance of 10,000 indices no row covers. A deep pool brings a row for every index it needs derived and restores in full. An index with almost nothing stored below it still cannot force unbounded work: the pool is derived as far as the budget reaches, the row above it is left out and counted under the new `PoolRowBeyondDerivationBudget` site, and the wallet loads without it under either policy, the way the discovery walk defers an address it cannot reach. A row inside the budget that has no derived entry is still refused. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…s back Pool rows are read account by account, so a reserved row whose coordinates match no account the wallet rebuilds is read by nobody. A reserved invitation row with a damaged `key_class` is one such row: the load succeeded, the address came back available, and nothing said so. Such a row cannot be told apart from a healthy one that belongs to an account this wallet no longer rebuilds, and stores holding those exist: `v4.2-dev` wrote pool rows under the `standard` label, contact accounts once lived only in memory, and the used-address projection already routes rows of accounts outside the wallet instead of failing on them. Refusing every unmatched row would stop those wallets from starting. The load now compares the reservations a wallet has stored with the ones its pools took back and reports the difference under the new `UnrestoredPoolReservation` site, under either policy, without failing the wallet. A wallet that rebuilds no account restores no pool and has nothing a reservation could be handed out from, so `restore_pools` returns before asking the store. That also keeps `load()` within the per-wallet statement count `tc_p4_012_load_query_count_bounded` holds it to. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…indow The reopen fixture took key material from `account_of_type` alone, which has no entry for the BLS operator account. That account therefore went down the "no source" branch, touched only indices 0 and 1, and never made `restore_pools` derive a pool from a BLS public key. Both places that pick a key source now fall back to the operator account's BLS key, so the operator pool gets forty used indices and a reservation past its window like every other derivable pool, and the existing state, address, path, key and non-reuse assertions cover it. Dropping the BLS branch of `public_key_source` fails the reopen. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…llet-restore Brings in the base stack up to 055727f: the rust-dashcore pins, the sweep-claimant and unsafe-mempool-record repairs. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…ed dashcore The merged base pins rust-dashcore at 844da0e8, where ProRegTx carries a `MasternodeNetInfo` service address and ProRegTx / ProUpRegTx gained the version-3 `payouts` and `shared_collateral` fields, so the fixture of `should_restore_provider_key_transaction_history_and_positions` no longer compiled. The fixture builds version-2 payloads. Upstream decodes and constructs those with a `MasternodeNetInfo::Legacy` socket address and both new fields `None`, which is what the fixture now spells out. The transactions and every assertion are otherwise unchanged. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Verified the complete diff at 921dbb8: nine prior findings are fixed, while reservation loss for type-impossible account coordinates remains valid. Five in-scope suggestions remain, including a newly confirmed address-reuse regression; none violates the consensus-critical invariants that the supplied severity policy classifies as blocking. Scoped wallet CI passed at this exact head; this verification was static, with no local builds or tests run.
🟡 5 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: platform-versioning); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The intricate restore_pools and derive_towards changes in packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs directly alter key derivation, address validation, and used-key and reservation restoration that prevent funding-key reuse. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-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; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-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-storage/src/sqlite/persister.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/persister.rs:1874-1875: Run the initial gap refill even when restored entries are already Used
The restore-before-projection ordering fixes saved beyond-window addresses, but it also changes the existing mark/refill loop's starting state. Consider a Standard external pool saved through index 29 with index 20 Used, plus a recordless UTXO at index 45 that has no saved pool row. After this overlay, discovery excludes index 20 because it is already present and stops at index 30 without resolving index 45. In rehydrate.rs:1298-1306, mark_used(20) returns false because the entry is already Used, and mark_used(45) returns false because it is absent, so the loop exits without refilling. The exact pinned key-wallet implementation confirms both return conditions. The base loader instead marked index 20, refilled through index 50, then marked index 45 on the next iteration. The new path can eventually derive and reserve funded index 45 as Available, and recordless history cannot repair it. Preserve the new ordering, but execute an initial bounded refill using the restored highest_used even when no state flips, then continue marking newly derived entries to a fixed point. Add a reopen regression combining saved Used entries with a recordless UTXO outside the saved pool; the existing direct wedge-zone test starts without the overlay.
In `packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:859-867: Count unrestored pool rows once across replay repair
These counts are added immediately to the shared LoadCtx, but load_one_wallet calls load_wallet_snapshot twice when startup history replay produces repair sweeps (persister.rs:1693-1698). Both reconstructions run restore_pools with the same context, and apply_replay_sweeps does not change core_address_pool. One unmatched reservation is consequently counted twice under UnrestoredPoolReservation; an unreachable pool row is likewise counted twice under PoolRowBeyondDerivationBudget. This contradicts LoadDegradation's documented per-row/per-entry semantics under both policies. Accumulate these diagnostics once per load or deduplicate them by persisted row coordinates across repair passes. Add coverage combining an unmatched reservation with history that triggers startup repair.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:775-777: Retain row coordinates when reporting typed pool failures
The new conflict errors preserve their type but contain only a static reason. restore_pools propagates them directly, and the Strict wallet-isolation boundary returns the original error without logging its wallet or row coordinates. A failed load therefore identifies neither the affected wallet nor the account, pool and index that need repair, although all are available at this guard. Retain the typed source and classification while adding structured coordinates to a diagnostic before propagation or to a contextual typed error. Apply the same treatment to the account/key-type and duplicate-address guards.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:847-851: Reject pool rows with impossible account discriminators
(existing thread: https://github.com/dashpay/platform/pull/5210#discussion_r4248454211)
The compatibility policy correctly preserves representable legacy or orphaned accounts, but it also accepts coordinates that those cases cannot produce. accounts::account_key_class always returns zero for IdentityInvitation, apply_pools uses that mapping, and V008's legacy pool conversion delegates to the canonical writer. An invitation reservation with key_class = 1 is therefore distinguishable from a legacy standard label or an absent contact account. The new should_report_a_reservation_that_no_restored_pool_takes_back regression confirms that both policies load the wallet with this previously reserved address Available; reporting the loss does not prevent allocation again. Add account-type coordinate validation before account-filtered restoration queries hide malformed rows. Reject only type-impossible sentinel combinations with a typed error, preserving report-only handling for representable unmatched coordinates. Strict should reject the malformed wallet and Recovery should isolate it.
In `packages/rs-platform-wallet-storage/src/sqlite/load_ctx.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/load_ctx.rs:77-81: Document the Rust source break from extending LoadSite
LoadSite is publicly re-exported, and the v5.0.0-beta.2 tag contains the previous exhaustive enum without #[non_exhaustive]. Adding these variants breaks downstream exhaustive matches even though persisted encodings and FFI layouts are unchanged. The PR's blanket Breaking Changes: None statement and the current Unreleased changelog omit this Rust API change. Document the two new cases and the required downstream match updates in the compatibility discussion and release notes. This belongs to the normal crate/workspace release boundary, not PlatformVersion or PERSISTENCE_CAPABILITIES_VERSION; appending the variants or adding #[non_exhaustive] now would still break existing exhaustive matches.
Out-of-scope follow-up suggestions (2)
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.
- Isolate exported invitation private keys from their persisted parent xpub — The existing invitation flow exports a non-hardened funding child's private key, while account registration persists its parent extended public key and chain code. An invitation recipient who also obtains a readable database or backup can recover the invitation account private key using the BIP32 non-hardened derivation relation, then derive sibling voucher keys. The export, derivation and parent-xpub persistence mechanisms predate this diff; the SQLite restoration changes do not introduce or worsen them.
- Follow-up: Track a separate security follow-up for hardened or independently generated voucher keys, including compatibility handling for existing invitations and a regression demonstrating sibling-key isolation.
- Centralize pool-entry integrity below persistence adapters — Out of scope: the mobile adapter's unchecked insertion path predates this diff, which changes only SQLite restoration. Moving restoration into an upstream key-wallet API and updating both adapters is a broader ownership redesign, not a prerequisite for the guarded SQLite path to work.
- Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.
0c6e418 restores the stored pools before the used-address projection. The projection's mark/refill loop refills a pool's gap window only after `mark_used` reports a flip, and an entry restored from a `used` row is already used, so it reports none. A pool whose stored rows end short of its window came back without the rest of it: saved through index 29 with index 20 used, it stayed at 29 where it used to reach 50, and a coin at index 45 with no pool row and no transaction record was never derived. The coin still counts towards the balance, which comes from the stored coins. What is lost is the window: its addresses are not derived, so the wallet does not watch them, and a funded one among them is later handed out as a fresh address. A pool that enters the loop with a used entry now has its window refilled on the first round regardless of flips; later rounds need a flip, as before. A pool with no used entry takes the same path as it did. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…tup repair When history replay finds conflicts to settle, `load_one_wallet` applies the sweeps and builds the wallet a second time. Both builds counted into the same `LoadCtx`, so everything the wallet tolerated was reported twice: one reservation no pool takes back came out as two, and so did a site that predates this branch — one unresolved coin address counted as two `unresolved_utxo_address`. The first build is thrown away, so its counts are too. `LoadCtx` hands out a checkpoint of its per-site counts and goes back to it before the rebuild; the counts then describe the wallet the load returns. Only the counts go back: the log still shows both passes. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
A typed load error says what is wrong, not where. Under Strict nothing else said it either: `tolerate_at` returns without logging, and the wallet isolation boundary hands back the original error. A refused pool row named neither its wallet nor its account, pool or index. This branch had also taken the wallet id out of three errors. The watch-only rebuild, the core-state projection and the platform-node pool reader used to fail with a string that carried it; they now return their typed cause, which does not. - `restore_pools` logs the wallet, account type, pool type and index of a row it refuses, then returns the same typed error. The address and the key are not logged. - The isolation boundary logs the wallet and the error kind when Strict fails the load, for every cause that ends a wallet. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
`LoadSite` is public and was exhaustive through v5.0.0-beta.4. This branch adds `PoolRowBeyondDerivationBudget` and `UnrestoredPoolReservation`, which already stops an exhaustive match outside the crate from compiling. Marking the enum `#[non_exhaustive]` in the same change makes this the last time a new site does that. Inside the crate nothing changes: `as_str` and `explanation` still match every variant by name, so a new site still fails to compile until it has a tag and its own wording. BREAKING CHANGE: code matching `platform_wallet_storage::LoadSite` exhaustively needs a wildcard arm. Persisted data, FFI layouts and capability bits are unchanged. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Static review of the exact head confirms that Core pool restoration now validates derived entries, restores provider pools, preserves usage state, and runs before UTXO reconciliation. Two in-scope issues remain: omitted reserved rows can be reused for one-time funding pools, and legacy standard-labelled pool rows are not selected during restoration.
🔴 1 blocking | 🟡 1 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: platform-versioning); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 6: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The substantial new restore_pools and derive_towards logic in packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs directly changes key derivation validation and restoration of used and reserved funding addresses, meeting both the intricacy and key-handling criteria. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-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; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-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-storage/src/sqlite/schema/core_pool.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:899-922: Do not successfully restore wallets when one-time funding pool entries are omitted
`restore_pools` treats every reservation not taken back by a reconstructed pool as a non-fatal degradation. Rows beyond the derivation budget are skipped at lines 799–803, while rows whose account coordinates match no reconstructed account are never queried; both cases are included in `stored_reservations` but not `restored_reservations`, leaving the corresponding address absent or Available. For an `IdentityInvitation` or other exported one-time funding pool, the previously exported bearer key can therefore be selected again after reopen. Because `WALLET_RESTORE` is advertised and the PR's purpose is to prevent funding-key reuse, Strict must fail closed for these pools or Recovery must quarantine the affected indices instead of returning a successfully restorable wallet.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:637-658: Restore persisted pools stored under the legacy standard account label
The restoration queries bind only `accounts::account_type_db_label(&account_type)` (for example, `standard_bip44` or `standard_bip32`). V008 deliberately admits the historical `standard` label as compatible with either Standard variant, but a row stored under that label is never selected by these queries. Its reservation is later reported as `UnrestoredPoolReservation` and the reconstructed address can become available again. Resolve the legacy alias using unambiguous account-manifest information, or withhold `WALLET_RESTORE` for wallets containing rows that cannot be safely assigned; otherwise the advertised restore capability is not compatible with valid historical SQLite stores.
…standard` `v4.2` registered both Standard variants under the one label `standard`, and V008 admits that label instead of rewriting it. The label lives in `account_registrations`. `core_address_pool` arrived together with the split labels, its only writer takes the label from the typed account, and the legacy conversion writes through that writer, so no pool row is filed under `standard`. Pin what follows from that for a store of that age: with its Standard account registered under `standard` and its pool rows under `standard_bip44`, the account is rebuilt, takes its pools back, and the load reports nothing. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
…not restored An asset lock is funded from a key used once. An invitation exports that key, and it never shows up as a coin, so the `used` flag or reservation on its pool row is the only record that it is gone. `restore_pools` loaded a wallet without such a row and said little or nothing: with the row's `key_class` damaged the load succeeded with no signal at all, because the unrestored-reservation count ignores `used` rows; with its index moved past the derivation budget it was counted and the wallet still loaded. Either way the next asset lock was funded from the key already handed out. For the six one-time funding accounts a used or reserved row that no restored pool takes back now fails the wallet with a typed error and its coordinates in the log: a row whose account coordinates match no restored account, a row above the derivation budget, and a row whose script cannot be read. Strict fails the load; Recovery drops that wallet, names it, and loads the rest of the file. Other accounts keep the reported, non-fatal path. Handing one of their addresses out again costs privacy, not funds, and the wallet marks the address used as soon as a coin on it is seen. Not covered: a row deleted outright leaves nothing in the store to notice, and a wallet that rebuilds no account is not checked. Two tests of the non-fatal path used an invitation reservation as their unmatched row; they now use a provider owner key reservation, and the invitation case is asserted as a refusal. The set of accounts is the one the asset-lock writer in rs-platform-wallet snapshots. That list is not exported, so a test reads it from the source and fails when the two differ. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
8acdf9b left `ManagedAccountTrait` imported in `next_funding_index`, which calls nothing through it. The tests built, but clippy with `-D warnings` did not. <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSi8cX229kW2fmLxKW6ir4
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
Reviewed the complete diff at ce03450 and found no remaining blocking defect; two non-blocking suggestions remain around coordinate-blob validation and shared funding-role classification. All 16 prior findings were independently revalidated: 14 are fixed, one is withdrawn, and one is intentionally deferred outside the corrected one-time funding-key case. Validation was static only; the supplied CI snapshot still has Rust workspace tests and Kotlin SDK checks queued, with the dedicated Rust wallet test job skipped.
🟡 2 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: platform-versioning); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 5: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); reviewer 7: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 8: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 9: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 10: gpt-6.1-sol (agent: phase2-reviewer, role: platform-versioning); reviewer 11: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 12: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The substantial restoration logic in packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs directly changes key handling by validating derived keys and restoring used positions and reservations that prevent one-time funding key reuse. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-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; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— platform-versioning (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-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-storage/src/sqlite/schema/core_pool.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:973-979: Check identity-blob widths before copying unmatched funding rows
The new wallet-wide audit reads both identity columns directly into Vec<u8>, bypassing the pre-allocation checks used by load_used_addresses_with_ctx and owning_account_for_script. A reserved, unused funding row with malformed identity coordinates is excluded by the used-address reader and cannot match the per-account restoration queries, so this audit is its first validation boundary. V009 imposes no width constraint on these columns: the audit can therefore copy multi-megabyte blobs before rejecting coordinates that must contain exactly 32 bytes. The connection's 32 MiB ceiling bounds the exposure, so this is hardening rather than an unbounded or unauthenticated remote vulnerability. Select length(user_identity_id) and length(friend_identity_id), validate both with blob::check_fixed_width(..., 32, ...), and only then retrieve the vectors. Add reserved-row coverage for malformed and oversized identities, checking the typed Strict failure and Recovery isolation with a healthy sibling wallet.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_pool.rs:556-572: Share the one-time funding-role definition with the asset-lock writer
ONE_TIME_FUNDING_LABELS independently reproduces the funding-account classification in wallet/asset_lock/build.rs::persist_asset_lock_account_pools. The sets currently agree, but their connection is enforced by a test that reads and parses the private writer's source syntax. This makes maintenance of the fail-closed restoration rule depend on another crate's implementation text rather than a typed wallet-domain contract. Export a shared AccountType predicate or equivalent classification API from platform-wallet's asset_lock module and consume it in both paths. Keep database labels, coordinate validation and Strict/Recovery handling in SQLite; translating the shared roles through account_type_db_label removes the parallel semantic lists without moving backend policy into the wallet layer.
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 one-time key issuance independently of individual pool rows — The fixture should_hand_out_again_a_one_time_funding_key_whose_row_was_deleted asserts the documented residual: deleting an issued invitation key's row leaves the loader unable to distinguish that key from an unused one. This is a concrete funding-key integrity concern, but detecting missing issuance history requires additional durable metadata and a recovery policy beyond this PR's restoration of present rows.
- Follow-up: Track a separate issuance-integrity task covering missing pool rows and explicitly distinguishing local issuance-watermark protection from stale-backup rollback protection.
TL;DR
Reopening a SQLite-backed wallet restores its Core address reservations and used-key positions, so previously handed-out funding keys are not offered again.
User story
As a wallet user, I can restart the app with my saved Core balances, pending spends and address reservations intact, and receive an error when required saved data cannot be restored.
Scenario
Use funding addresses or reserve a new address, close the wallet, and reopen the same database. The wallet restores the used and reserved addresses before allocating another key. While a wallet is loading, a write from another connection is refused, so that wallet's state comes from one snapshot.
Detailed discussion
Dependencies and overlap
Stacked on #5207 (
fix/sqlite-asset-lock-reconciliation), which is stacked on #5150. The history replay, pending-spend reservations, accounting repair and migrations of #5150 are reused unchanged; this PR is limited to SQLite address-pool restoration, the capability declaration, typed load errors and focused tests.What was done?
pool_row_beyond_derivation_budget; a stored reservation that no restored pool takes back is counted asunrestored_pool_reservation. Both are reported under Strict and Recovery and do not fail the wallet, except for one-time funding key accounts (next item). Hardened pools (platform-node keys) cannot be re-derived from public material and stay trusted as stored.unresolved_utxo_address, also stop doubling during startup repair.unrestored_pool_reservation, and a row beyond the derivation budget aspool_row_beyond_derivation_budget; neither is restored, so that address can be handed out again until a coin on it is seen.backend.BEGIN IMMEDIATEtransaction introduced by fix(platform-wallet)!: restore Core spending state and repair persisted accounting #5150: a writer on another connection is refused while a wallet loads, so its records, spend claims, coins, pools and sync checkpoints come from one snapshot. The guarantee is per wallet, not per load: platform address state, the wallet id list, shielded viewing keys and the unimplemented-row count are read outside those transactions.WALLET_RESTOREcapability after completing its Core-snapshot restore path. Capability meanings and bit values remain unchanged.PROVIDER_TRANSACTIONSis not newly declared.WALLET_RESTOREis the existing persisted Core wallet snapshot contract. Separate token caches, DashPay profile overlays, deferred contact queues and invitation UI records are not made complete by this flag; their readback remains separate. DET's #1036 pin remains on the narrower #5207. Declaring it lets SQLite hosts pass preflights that require it, such as funding a shielded pool from an asset lock.Out of scope
orphaned_utxo_ownerandunresolved_utxo_addresson every load: the used-address projection from fix(platform-wallet)!: restore Core spending state and repair persisted accounting #5150 routes addresses of key-only accounts to the first funds account. The keys themselves are restored correctly; only the report is noisy.validate_account_typestill admitsstandardfor pool rows although no writer produces it; it belongs to the reader introduced with feat(platform-wallet-storage): embeddable SQLite persistence backend with seedless rehydration #3968.length()gate the other readers use. A mis-sized row is still refused; the cost is one transient copy bounded by the connection's 32 MiB value limit.ONE_TIME_FUNDING_LABELSrepeats the account filter of the asset-lock writer inplatform-wallet; a test fails when the two differ. A typed predicate exported fromplatform-walletwould replace both.How Has This Been Tested?
platform-wallet-storagesuite passes with all features and the locked dependency graph: 1,106 tests, 0 failed. Clippy with--all-targets --all-features --locked -- --no-deps -D warningsis clean forplatform-wallet-storageandplatform-wallet; formatting check passes.sqlite_wallet_restore.rscovers reopen and real manager hydration, special funding-key non-reuse, reservation timestamps, exact pending-spend and per-address accounting, malformed-key rejection before partial manager load, a typed platform-node pool failure under Strict and Recovery, a writer refused as busy while a wallet's load transaction is open, and ProReg/ProUpReg key-account history and positions. It also covers a funded address beyond the derived window, a foreign script stored beyond that window, a row filed under a missing pool, two DashPay contact pools sharing an index, and each pool-conflict guard under Strict and Recovery. It also covers the pools of a standard account registered under the legacy label, and a used one-time funding key row that no restored pool takes back. Unit tests pin that restoring pools twice leaves per-address accounting unchanged and that a row the derivation budget cannot reach is skipped and counted, with nothing derived past the budget.platform-wallettests (its only change is two doc comments), the full workspace, live-network or user-funds testing. No Swift file differs from the base.Breaking Changes
Rust source only:
platform_wallet_storage::LoadSitegainsPoolRowBeyondDerivationBudgetandUnrestoredPoolReservationand becomes#[non_exhaustive]; code matching it exhaustively needs a wildcard arm. No persisted encoding, FFI layout or capability bit changes.Checklist
🤖 Co-authored by Claudius the Magnificent AI Agent