Skip to content

fix(platform-wallet)!: restore Core spending state and repair persisted accounting - #5150

Open
lklimek wants to merge 53 commits into
chore/rust-dashcore-1112-v5.1from
fix/pr-5126
Open

lklimek wants to merge 53 commits into
chore/rust-dashcore-1112-v5.1from
fix/pr-5126

Conversation

@lklimek

@lklimek lklimek commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: Repair Core transaction history that omits spent inputs and prevent already-spent outputs from becoming selectable after a SQLite wallet restart.

Stacked on #5307, the v5.1-dev port of #4932. The rust-dashcore dependency migration is reviewed in that base PR; this diff contains the wallet accounting and persistence repairs. Merge the base PR first, then retarget this PR to v5.1-dev.

Issue being fixed or feature implemented

Fixes #5126. As a wallet user, I want history to reflect what each transaction actually spent and received, including when funding is discovered late or an asset lock is restored after restart.

What was done?

  • Replay SQLite history in dependency order during load, settling conflicts using persisted ChainLock and InstantSend finality. Repair losing history and non-consumed asset-lock lifecycle entries atomically; recovery loads roll back both. Validate replay arithmetic and restore persisted spent claims, including recordless and unknown-owner claims.
  • Add reversible, frozen SQLite migration V019 to index raw inputs and repair historical accounting. Preserve original blobs, validate blob/script sizes before loading, and reconcile subsequent updates. Undecodable confirmed history forces a rescan; undecodable unconfirmed history fails migration atomically.
  • Preserve known ownership through partial replays, isolate accounting by wallet, exclude contact watch-only outputs, and retain spendability for ambiguous credit verdicts. Project corrected transaction topology and coalesce repeated account snapshots before calculating totals.
  • Share Rust ownership, direction and net-amount rules between live projection and runtime SQLite repair. Add typed storage errors for transaction-body conflicts, unknown wallet networks and amount overflow.
  • Reconcile SwiftData history at load and persistence-round completion, protecting funded asset-lock amounts from empty updates. Wallet transaction list/detail views use wallet-relative amounts and direction. SwiftData V4 preserves unavailable amounts across restart and wallet deletion through a lightweight migration from frozen V3.

The base PR supplies rust-dashcore#1112 (870af146), including merged #1082. This PR uses its spent-claim restoration API. Core spending does not imply Platform asset-lock consumption. No consensus-versioned behavior changes are introduced in this diff.

Deferred and related work

How Has This Been Tested?

  • The restacking changes commit ancestry and PR base only; the final source tree is identical to 5ee6f20ec4690c17a8fa4c494aea3c50801fb192.
  • On that tree, 72 targeted SQLite restart, spent rehydration, migration, schema pinning, error-classification and persistence-error-mapping tests passed with all features and the locked dependency graph.
  • Earlier branch validation covered wallet/accounting, encryption, DPP signing/BLS and RPC compatibility; migration-specific validation is documented in chore(platform)!: update rust-dashcore (incl secp256k1 0.33) #5307.
  • The reported SQLite query-caching gate failure remains unresolved; passing targeted tests do not imply a green workspace gate.
  • Apple/Swift builds, live-network end-to-end tests and the full workspace runtime suite were not run here.

Breaking Changes

WalletStorageError becomes #[non_exhaustive] and gains TransactionBodyConflict, UnknownWalletNetwork and NetAmountOverflow; downstream exhaustive matches need a wildcard arm. SQLite adds V019 and SwiftData adds V4. Development databases that applied an earlier version of this unreleased V019 must be recreated because refinery rejects divergent migrations.

Core cryptography API changes, Node.js >=20 and upstream development-database compatibility requirements belong to the base PR #5307. The FFI ABI is unchanged.

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
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent

lklimek and others added 5 commits September 28, 2026 15:04
Build on rust-dashcore PR #979 and account-local correction/event fixes at ed4c02e119898f1bb510cf79abc8a125a9bc9bea. Cargo metadata --locked validates the pin; wallet integration checks follow with the storage changes.
Repair fully resolved history from durable TXOs at round commit and wallet
load, preserve funded asset-lock accounting during context-only recovery,
and scope history presentation to the selected wallet.

Swift tests require macOS tooling and were not executable on this host.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Use one ownership predicate for persisted inputs, outputs, and scoped
history amounts, including legacy watch-only TXOs. Preserve accountless
rows with known wallet ownership.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Index historical inputs to repair late funding and existing SQLite history,
preserve known ownership across partial replay, and project corrected bridge
topology without double-counting account snapshots. Keep unknown credit
verdicts from inventing spends or resurrecting coins.

Co-Authored-By: Codex GPT-6 <noreply@openai.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Replace the PR 979-based dependency with the isolated accounting correction
backported onto the original e4208c90 base. The identical upstream patch is
available on current rust-dashcore dev without SPV or address-pool changes.

Validated 24 Core storage and 132 changeset tests with the new pin; scoped
Clippy with CI flags and formatting checks pass.

Co-Authored-By: Codex <noreply@openai.com>
@coderabbitai

coderabbitai Bot commented Sep 28, 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: bb04dc9f-82c9-40ce-a391-16c5dd0968cf

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
  • Autopilot · 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.

@thepastaclaw

thepastaclaw commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

⛔ Final review complete — 4 blocking finding(s) (commit d397edb) · triage: critical

PastaPastaPasta and others added 2 commits September 28, 2026 14:27
#5043 introduced PLATFORM_V15 and made it latest, but the committed
grovedb-structure.json and one structure test still expected protocol 14,
so `structure::tests` has failed on v4.3-dev since it merged:

- should_match_committed_grovedb_structure_json: regenerate the snapshot.
  Only the origin labels and latest_protocol_version change (14 -> 15);
  v15 has the same state structure as v14.
- should_record_a_contract_layer_with_its_documents_on_top: read the
  expected origin from PlatformVersion::latest() instead of a hard-coded
  14, so the next protocol bump does not break it again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@lklimek
lklimek marked this pull request as ready for review September 29, 2026 08:45
@github-actions github-actions Bot added the waiting-bots Waiting for the review bots to report on this head label Sep 29, 2026
…sync

Replay persisted confirmed transactions through the wallet checker on load,
retain only persisted unspent outputs not consumed by confirmed history,
and restore finality before sync checkpoint pruning. Use dash-async for
synchronous persistence loading.

Cover restart, stale projections, funding redelivery, finality, same-block
spends, reserved inputs, and synchronous/current-thread/multi-thread callers.

Co-authored-by: Codex <noreply@openai.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
@lklimek lklimek changed the title fix(sdk): repair persisted Core transaction accounting on Rust and iOS fix(sdk)!: restore Core spending state and repair persisted accounting Sep 29, 2026
lklimek and others added 3 commits September 29, 2026 16:21
…count

The load projection parks an unattributable UTXO in the first funds
account; replaying confirmed history then credits the same outpoint to
its real owner. The post-replay filter only checked a wallet-wide
unspent set, so both copies survived and update_balance double counted.

Drop the load-time fallback copy whenever replay credits the outpoint to
a different funds account, while still refusing to re-credit anything
persistence excluded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dable

Pre-fix builds persisted DashPay contact (watch-only) outputs as unspent
core_utxos rows. V019 re-labels their history as Sent but left the rows
unspent, so load routed them to the first funds account: the balance
included coins the wallet cannot sign and coin selection could pick one.

load_state now skips unspent contact-only rows. The rows are kept in
SQLite (no destructive migration step), so the exclusion is reversible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eset

loadWalletList saved or rolled back the background context
unconditionally after reconciling transaction accounting. Inside an open
begin/endChangeset round that committed half of the round or silently
discarded its staged writes while the round still reported success.

Guard the pass with !inChangeset like every other load-time writer; the
round reconciles its own dirty rows and the next load runs the full pass.

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

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Preliminary review — Phase 1 blocker gate

The SQLite repair logic addresses the stated restart and historical-accounting failures, but the confirmed-spend replay is implemented only in the SQLite persister while the FFI/SwiftData restore path does not rebuild the corresponding spend guards. Several additional correctness and scalability issues remain in the new history-repair and Swift reconciliation code.

Validated blockers were found by the Phase-1 review and confirmed by a fresh verifier. Phase 2 is deferred until a fresh same-head revalidation clears the blocker gate.

🔴 1 blocking | 🟡 7 suggestion(s)

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

🟡 Suggestion: History replay holds the SQLite connection lock for the full confirmed history
packages/rs-platform-wallet-storage/src/sqlite/persister.rs

load_one_wallet invokes restore_confirmed_transactions while load holds the connection mutex. The replay performs an async block_on round trip and runs check_core_transaction once per confirmed record, even though the replay itself does not access SQLite. Large histories therefore block concurrent store and flush operations for the duration of the replay. Snapshot the required records and release the connection lock before replaying, or document and bound this startup write-stall behavior.

source: muse-spark-1.3-contributor (phase1-reviewer: general, architecture-layering, rust-quality)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: architecture-layering); reviewer 3: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); final verifier: gpt-6-astra (agent: astra-gate-verifier, role: verifier)

  • Triage: critical by gpt-6-astra (effort low) — The diff combines intricate persisted-accounting repair and a new SQLite migration with changes to restore_confirmed_transactions in packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs that directly determine spendable UTXOs through transaction replay, spent-output exclusion, account deduplication, and finality restoration.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — architecture-layering (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 13% left, 5h 100% left), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh verifier: gpt-6-astra — verifier; agent astra-gate-verifier
  • Phase 2 reviewers: not run (deferred by blocker gate)
🤖 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/rehydrate.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs:423-505: Confirmed-history replay is implemented only for SQLite
  The restart invariant is that every persister must hand WalletManager a spend projection containing confirmed spent outpoints. This change enforces that invariant only in SQLite by replaying persisted records through `check_core_transaction` inside `restore_confirmed_transactions`. The FFI restore path still reconstructs spendability from persisted TXO rows, and the Swift reconciliation pass only rewrites `netAmount` and `direction`; it does not rebuild Rust's `spent_outpoints` or remove confirmed inputs from the in-memory UTXO set. Consequently, a wallet restored through the FFI/SwiftData path can still rehydrate a confirmed-spent output as selectable, while SQLite behaves differently. Move the confirmed replay to the shared async `load_from_persistor` boundary, or provide equivalent replay semantics for every persister before the manager consumes the start state.

In `packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:6811-6863: SwiftData independently re-derives Rust-owned accounting
  The SwiftData repair path recomputes ownership, amounts, and direction from decoded prevouts, while SQLite's `repair_record` derives the same values from persisted UTXO state and the Rust wallet projection. These implementations already have materially different rules: Swift requires every input prevout to resolve, passes `allOutputsOwned = false` to its helper, and re-encodes contact exclusion using a raw account-type tag, whereas the Rust repair can recover ownership from the UTXO table and distinguishes internal transactions from external outputs. Future checker or contact-ownership changes can therefore make the two persistence backends repair the same transaction differently. Centralize this accounting projection in Rust and have Swift persist the returned verdict rather than reimplementing it.
- [SUGGESTION] packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift:6915-6934: Load-time repair scans the entire store and fails all wallets on one bad row
  `loadWalletList` fetches every `PersistentTransaction` and every `PersistentTxo`, then filters transactions in memory for the wallets being loaded. This makes startup work proportional to the entire SwiftData store even when loading one wallet. The same `do` block also treats any reconciliation or fetch error as a failure for the complete wallet list, so one unreadable transaction or TXO hides otherwise healthy wallets. Scope both fetches to the loaded wallet IDs, or batch per wallet and isolate per-wallet reconciliation failures.

In `packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs:32-36: Same-txid redelivery with different witness bytes aborts persistence
  `preserve_known_details` rejects a persisted transaction whenever `previous.transaction != incoming.transaction`. A transaction's txid excludes witness data, so legitimate observations of the same txid can differ only in witness bytes. That causes the entire changeset to fail even though the row is already keyed by txid and the ownership details below could be merged. Accept same-txid redeliveries whose non-witness transaction identity is unchanged, or compare an appropriate txid-level identity rather than full transaction equality. If a mismatch must remain fatal, include the txid in the typed error so the failure is diagnosable.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs:150-157: Contact filtering hardcodes a database label
  `contact_only_script` embeds `account_type != 'dashpay_external'` directly in SQL, while the canonical mapping is `accounts::account_type_db_label`. If the persisted label changes, the writer and this repair query can silently diverge and contact-only outputs can be treated as wallet-owned during repair. Use a shared constant or construct the query with the canonical label and add a test tying the SQL value to `account_type_db_label`.
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs:292-313: Migration performs redundant decoding and wallet lookups per record
  During migration, each row is decoded by `get_tx_record`, then `repair_record` calls `get_tx_record` again for the same txid. The loop also calls `network(tx, &wallet_id)` once per transaction, which repeats the wallets-table lookup for every record. Because this runs inside the migration transaction, large histories hold the migration lock while doing twice the decoding and redundant queries. Cache the network per wallet and pass the already-decoded record into the repair function.

In `packages/rs-platform-wallet/src/changeset/changeset.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/changeset/changeset.rs:628-643: Account coalescing mixes finality from one observation with body data from another
  When a later record has lower context rank, the code keeps the later record's amounts, inputs, and outputs but replaces only its context with the earlier record's more-confirmed context. That produces a synthetic record combining the stale observation's body with finality it did not carry. If corrected-then-stale ordering is reachable, the higher-ranked observation should win wholesale; if it is impossible, this branch is dead and should be removed. Add a regression test for corrected-then-stale ordering and replace the record wholesale based on context rank.

In `packages/rs-platform-wallet-storage/src/sqlite/persister.rs`:
- [SUGGESTION] packages/rs-platform-wallet-storage/src/sqlite/persister.rs: History replay holds the SQLite connection lock for the full confirmed history
  `load_one_wallet` invokes `restore_confirmed_transactions` while `load` holds the connection mutex. The replay performs an async `block_on` round trip and runs `check_core_transaction` once per confirmed record, even though the replay itself does not access SQLite. Large histories therefore block concurrent store and flush operations for the duration of the replay. Snapshot the required records and release the connection lock before replaying, or document and bound this startup write-stall behavior.

Comment thread packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs Outdated
Comment thread packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs Outdated
Comment thread packages/rs-platform-wallet/src/changeset/changeset.rs Outdated
Comment thread packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs Outdated
lklimek and others added 3 commits September 30, 2026 07:22
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PV15

#5057 folded DRIVE_ABCI_QUERY_VERSIONS_V3 into V2 (identical values) and
deleted v3.rs; PV14 was updated but PV15, introduced on v4.3-dev, still
imported V3, so the v4.2-dev -> v4.3-dev merge stopped compiling.
Pure rename; the selected query versions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v4.2-dev -> v4.3-dev merge (704307e) left an unused import and
stray blank lines in dpns.rs, failing clippy -D warnings and rustfmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lklimek lklimek changed the title fix(sdk)!: restore Core spending state and repair persisted accounting fix(platform-wallet)!: restore Core spending state and repair persisted accounting Sep 30, 2026
The structure generator describes PlatformVersion::latest(), which is 15
once PLATFORM_V15 compiles. The committed JSON and a hard-coded
"@14" assertion were stale. Regenerated the JSON (version labels only)
and derive the expected fixture origin from PlatformVersion::latest().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restore recordless spent guards with claimant provenance, remove swept
asset-lock lifecycle rows atomically, reject unsafe replay operands, and
return wallet-scoped reconciliation failures. Pin the minimal upstream
restoration API and cover restart, finality, conflicts, and rollback.

Validation: 133 targeted tests, package rustfmt, and CI-matching Clippy pass.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>

@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

One in-scope blocker remains: load-time conflict repair can erase a durable spend claim with a NULL claimant and make the coin selectable. The cumulative-sum overflow report is refuted by the per-iteration range check, while full-history replay cost remains explicitly deferred. Verification was static; the supplied CI snapshot contains no Rust-wallet or Swift-migration test results, and the documented upstream-pin merge gate remains outstanding.

🔴 1 blocking

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: 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, intricate changes in sqlite/rehydrate.rs, sqlite/schema/core_state.rs, and sqlite/migrations/legacy_v019.rs alter persisted spend claims, spendable-output restoration, and storage migration behavior, directly affecting coin selection and durable wallet state.
  • 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 — platform-versioning (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — rust-quality (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-storage/src/sqlite/schema/core_state.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/core_state.rs:529-533: Preserve unknown durable claims during load-time conflict repair
  The `.optional()?.flatten()` treats an existing `spent = 1` row with a NULL `spent_in_txid` exactly like an absent row, so it bypasses the durable-claim veto. NULL provenance is supported: V019 can delete an unreadable confirmed spender while retaining its spent outputs, and the transaction-delete trigger clears `spent_in_txid` without clearing `spent`.

  Persist a mempool loser L spending A and B, a settled winner W spending only A, and an independent unknown durable claim on B whose spender body is absent. Conflict planning runs before durable claims are restored into memory and reports B released. This branch skips B; the generic sweep then marks its materialized row unspent or deletes its placeholder. The second snapshot rebuild consequently loses the claim and can return an already-spent coin as selectable. Restoring spent outpoints afterward cannot recover a row that has been released or deleted.

  Distinguish an absent row from a present row with an unknown claimant. Preserve the latter unless removal is established, including its nullable provenance through the post-sweep restoration. Add a restart regression that persists the conflicting records and NULL claimant before loading, for both materialized and placeholder rows. The existing unknown-claim matrix creates its competing spend only after load, so it does not exercise this SQL repair path.
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 confirmed-history restoration at the shared persistence boundary — The reviewed shared loader still replays only host-supplied unconfirmed outgoing transactions; complete recorded-history replay remains SQLite-specific. The confirmed-spend restart gap in the FFI/SwiftData path predates this PR, and the author explicitly separates the ABI-changing restoration work into #5220.
    • Follow-up: Retain the confirmed-spend FFI restart regression and shared recorded-history restoration work in the already-tracked #5220.

Comment thread packages/rs-platform-wallet-storage/src/sqlite/schema/core_state.rs Outdated
Base automatically changed from fix/structure-tests-latest-protocol to v5.1-dev October 5, 2026 13:36
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 1 thread unresolved — resolve it. 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
Keep existing spent rows with NULL claimants distinct from absent rows.
Preserve their nullable claimant and height through conflict repair,
reload, and funding redelivery while allowing known loser claims to release.

Validation: 76 targeted restart/sweep tests, rustfmt, and CI-matching Clippy.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 2 threads unresolved — resolve them. 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 a2b6f50, 19 prior findings are addressed and six explicitly scoped follow-ups remain deferred. Two in-scope blockers remain: ordinary mempool persistence omits claimant provenance needed for conflict release, and malformed peer observations can be persisted successfully but subsequently prevent wallet restoration. Verification was static only; the supplied CI snapshot still had Rust workspace tests and the Swift frozen-schema check running, with dedicated Rust wallet tests skipped.

🔴 2 blocking

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); 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-astra (agent: phase2-reviewer, role: general); reviewer 9: gpt-6-astra (agent: phase2-reviewer, role: architecture-layering); 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); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The large, intricate changes in sqlite/rehydrate.rs, sqlite/schema/core_state.rs, and sqlite/migrations/legacy_v019.rs directly alter persisted spend claims, spendable-output restoration, conflict resolution, and storage migration behavior.
  • 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 — 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 — 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
  • 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/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs:222-225: Record claimant provenance for newly persisted mempool spends
  The new protection for unknown durable claims also captures ordinary mempool reservations created by the production writer. TransactionDetected projects owned inputs into spent_utxos, but core_state::apply only sets spent = 1, and this condition skips assigning spent_in_txid for Mempool records. If a persisted loser spends A+B and a separately persisted InstantSend winner spends only A, replay reports B as released. apply_replay_sweeps nevertheless withholds B because its claimant is NULL, deletes the loser, and restores B's NULL spend claim; subsequent loads keep B unavailable even though no surviving transaction spends it. The release regression masks this mismatch by manually assigning the loser's txid at sqlite_spent_rehydration.rs:970–983, which the normal mempool persistence path does not do. Atomically record known claimant provenance when creating a new live mempool reservation, without overwriting unrelated or genuinely unknown historical claims. Add a store/reopen/conflict regression without the fixture's manual provenance UPDATE. This is release after a proven conflict, not the deferred expiry of unresolved mempool observations.

In `packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs:634-642: Reject malformed mempool amounts before they can poison wallet startup
  With mempool monitoring enabled, a malicious connected peer can send a decodable transaction containing a foreign input, a small output to a watched receive address, and a foreign P2PKH output worth i64::MAX duffs. In the exact pinned dependency, MempoolManager::handle_tx forwards the transaction to process_mempool_transaction without monetary-range validation. Account matching sums only owned outputs, and record_transaction omits ordinary foreign output details when there are no owned inputs, so this observation produces a small positive receipt without overflowing live accounting. core_bridge then persists the complete transaction body and only the small owned UTXO; core_history::repair_record also calculates accounting from owned details, allowing persistence to succeed. On reopening, this new validation sums all raw outputs and returns IntegerOverflow. The error propagates through load_wallet_snapshot: default Strict loading fails the store, while Recovery omits the affected wallet. The offending record remains stored, so later startups fail without further attacker interaction. Keep the replay arithmetic guard, reject unsafe unauthenticated observations before persistence, and safely isolate or quarantine already-persisted malformed mempool records. Cover the ingress-to-persistence-to-reopen path with a small owned receipt and an overflowing foreign-output total. This attack needs no victim-owned input and is distinct from the deferred reservation-expiry issue.
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.

  • Bound startup history replay without weakening spend guards — Outside the focused repair scope and already reconciled under 62e8727af31c. Full-history replay still holds the connection mutex, but bounding it is an explicitly deferred performance change rather than an additional correctness requirement.
    • Follow-up: Consider creating a separate issue or author/maintainer-requested PR for this.

Comment on lines +222 to +225
// Stale mempool rows cannot overrule a later sweep's release.
if !matches!(record.context, TransactionContext::Mempool) {
// Record the spender so the mark stays attributable and
// reversible; an existing claim by another spender stands.

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.

🔴 Blocking: Record claimant provenance for newly persisted mempool spends

The new protection for unknown durable claims also captures ordinary mempool reservations created by the production writer. TransactionDetected projects owned inputs into spent_utxos, but core_state::apply only sets spent = 1, and this condition skips assigning spent_in_txid for Mempool records. If a persisted loser spends A+B and a separately persisted InstantSend winner spends only A, replay reports B as released. apply_replay_sweeps nevertheless withholds B because its claimant is NULL, deletes the loser, and restores B's NULL spend claim; subsequent loads keep B unavailable even though no surviving transaction spends it. The release regression masks this mismatch by manually assigning the loser's txid at sqlite_spent_rehydration.rs:970–983, which the normal mempool persistence path does not do. Atomically record known claimant provenance when creating a new live mempool reservation, without overwriting unrelated or genuinely unknown historical claims. Add a store/reopen/conflict regression without the fixture's manual provenance UPDATE. This is release after a proven conflict, not the deferred expiry of unresolved mempool observations.

source: gpt-6-astra (phase2-reviewer: general)

Comment on lines +634 to +642
total(
"replay.outputs",
record
.transaction
.output
.iter()
.chain(credit_outputs)
.map(|o| o.value),
)?;

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.

🔴 Blocking: Reject malformed mempool amounts before they can poison wallet startup

With mempool monitoring enabled, a malicious connected peer can send a decodable transaction containing a foreign input, a small output to a watched receive address, and a foreign P2PKH output worth i64::MAX duffs. In the exact pinned dependency, MempoolManager::handle_tx forwards the transaction to process_mempool_transaction without monetary-range validation. Account matching sums only owned outputs, and record_transaction omits ordinary foreign output details when there are no owned inputs, so this observation produces a small positive receipt without overflowing live accounting. core_bridge then persists the complete transaction body and only the small owned UTXO; core_history::repair_record also calculates accounting from owned details, allowing persistence to succeed. On reopening, this new validation sums all raw outputs and returns IntegerOverflow. The error propagates through load_wallet_snapshot: default Strict loading fails the store, while Recovery omits the affected wallet. The offending record remains stored, so later startups fail without further attacker interaction. Keep the replay arithmetic guard, reject unsafe unauthenticated observations before persistence, and safely isolate or quarantine already-persisted malformed mempool records. Cover the ingress-to-persistence-to-reopen path with a small owned receipt and an overflowing foreign-output total. This attack needs no victim-owned input and is distinct from the deferred reservation-expiry issue.

source: gpt-6-astra (phase2-reviewer: security-auditor)

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Your move: thepastaclaw requested changes on this head; dismiss the review or push a fix; thepastaclaw left review threads unresolved; resolve them.
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
Drive real wallet events through the SQLite adapter, reopen the database,
and verify funding replay cannot make spent coins available. Cover pending,
mined and finalized spends plus recordless conflict winners.

Removing only restore_spent_outpoints reproduces both recordless-winner
failures while ordinary spends and uninterrupted controls remain correct.

Co-Authored-By: Codex <noreply@openai.com>

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
Merge current v5.1-dev and PR #4932, then pin all eight rust-dashcore dependencies to PR #1112 (870af146). Preserve DPP crypto implementations, canonical node IDs, and legacy Core RPC behavior. Use the upstream blst no_std API fix at 71a00877 for WASM compatibility.

Validated wallet/storage, signing/BLS, encryption and RPC regressions, scoped strict Clippy, and WASM builds. The published tree matches the locally tested tree b69a241.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 4 threads unresolved — resolve them. 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 6, 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

Verified the exact head and independently reconciled all 27 prior findings. Two carried blockers remain at transaction admission and mempool spend attribution; the new restoration API also loses protection for later-added funding accounts, and two new query sites fail the existing SQLite CI gate. This was a static review: the Rust job reports 17,171 passing tests, one failure, and 1,318 tests not run; the platform and browser suites have since passed, while the Swift build was skipped.

🔴 4 blocking

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); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The large, intricate changes in packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs, src/sqlite/schema/core_state.rs, and src/sqlite/migrations/legacy_v019.rs directly alter persisted spend claims, spendable-output restoration, conflict resolution, and storage migration behavior.
  • 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-storage/src/sqlite/persister.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/persister.rs:1905-1908: Keep restored spend claims effective for later-added funding accounts
  At the pinned revision, ManagedWalletInfo::restore_spent_outpoints copies claims only into the funding accounts currently present; it retains no wallet-level claim set. Its documentation explicitly requires reapplication after adding funding accounts. Platform calls it only here during loading, while register_contact_account later constructs and inserts a fresh ManagedCoreFundsAccount without restoring those claims. A recordless or unknown-owner claim restored before its receiving account is registered is therefore absent from that new account. When the funding transaction is redelivered, the new account can credit the consumed output: update_utxos consults that account's spent set, and the recordless case need not retain spender history or an observed-spend entry. This breaks the PR's promised protection for unknown-owner and recordless claims. Retain durable claims at wallet scope and apply their exclusion to every funding account, including later insertions, or provide an equivalent guaranteed registration hook. Add a restart regression that restores a recordless claim, registers its previously unknown receiving account, and redelivers the funding transaction.

In `packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs:209-211: Restore the SQLite query-caching invariant for the new queries
  The exact-head Rust job fails tc_p1_003_prepare_cached_in_writers on this new tx.prepare call and conn.prepare in core_state::load_spent_claims at line 973. I checked the test and failed-job log: these are its two reported offenders, and neither query matches the explicit read-only allowlist. This is the workspace job's sole reported failure and cancels execution of 1,318 remaining tests. Use prepare_cached at both sites, or explicitly register an eligible read-only query in the existing allowlist, so the workspace gate can complete.

In `packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs:634-642: Reject malformed mempool amounts before they can poison wallet startup
  (existing thread: https://github.com/dashpay/platform/pull/5150#discussion_r4185552401)
  The replay bounds are checked only after the transaction body has become durable. At the exact rust-dashcore pin, MempoolManager::handle_tx forwards peer transactions to wallet processing without monetary validation, and the account checker sums only matching wallet outputs. A transaction with foreign inputs, a small output to a wallet address, and a separate foreign output worth 1_u64 << 63 therefore has small wallet-owned accounting. The foreign output is omitted from an incoming record's accounting details but remains in its raw transaction body. core_state::apply can store that body, and core_history::repair_record checks the owned net rather than every raw output. On reopening, this loop rejects the foreign amount with IntegerOverflow: Strict load fails, and Recovery omits the affected wallet. Reject malformed observations before state mutation and record emission, and enforce replay-safe amount bounds before committing incoming history. Retain the defensive load checks and add an event-driven ingestion → persistence → reopen regression; the existing malformed-blob fixtures do not cover admission.

In `packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs:222-225: Record claimant provenance for newly persisted mempool spends
  (existing thread: https://github.com/dashpay/platform/pull/5150#discussion_r4185552392)
  The production TransactionDetected bridge emits spent_utxos for pending transactions, but core_state::apply writes only spent = 1, and this guard skips assigning spent_in_txid for Mempool records. A freshly persisted pending spend consequently reloads with NULL claimant provenance even though its spending record identifies the claimant. The new restoration API makes that omission consequential: restore_spent_outpoints installs a None claim, and upstream release_spent_marks deliberately retains it when the actual transaction is removed. For a pending loser spending A+B and a final winner spending only A, B remains reserved after restart even after the loser is definitively removed. Startup reconciliation similarly vetoes B's release because its persisted claimant is NULL. The existing release controls manually UPDATE spent_in_txid to the loser, bypassing the missing production write; the clean event-driven tests cover preservation rather than this extra-input release. Attribute spends when the current changeset establishes them, separately from historical accounting repair, while preserving genuinely unknown claims and preventing stale mempool history from recreating released reservations. Add a clean event-driven two-input loser → restart → one-input final winner → funding-redelivery regression.
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.

  • Bound unauthenticated pending-spend reservations — The pinned peer-processing path still reserves tracked inputs without verifying spending signatures, and replay preserves these reservations without provenance-aware expiry or revalidation. The PR explicitly separates this lifecycle work into #5225. It remains a concrete security follow-up, distinct from the in-scope failure to release a definitively defeated claimant.
    • Follow-up: Complete the tracked #5225 work across peer processing, wallet state, and persistence, clearing expired or rejected reservations consistently without releasing settled claims.

Comment on lines +1905 to +1908
// Restore durable claims after replay/finality, including claims with no spend body.
let spent =
schema::core_state::load_spent_claims(conn, &wallet_id).map_err(PersistenceError::from)?;
wallet_info.restore_spent_outpoints(&spent);

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.

🔴 Blocking: Keep restored spend claims effective for later-added funding accounts

At the pinned revision, ManagedWalletInfo::restore_spent_outpoints copies claims only into the funding accounts currently present; it retains no wallet-level claim set. Its documentation explicitly requires reapplication after adding funding accounts. Platform calls it only here during loading, while register_contact_account later constructs and inserts a fresh ManagedCoreFundsAccount without restoring those claims. A recordless or unknown-owner claim restored before its receiving account is registered is therefore absent from that new account. When the funding transaction is redelivered, the new account can credit the consumed output: update_utxos consults that account's spent set, and the recordless case need not retain spender history or an observed-spend entry. This breaks the PR's promised protection for unknown-owner and recordless claims. Retain durable claims at wallet scope and apply their exclusion to every funding account, including later insertions, or provide an equivalent guaranteed registration hook. Add a restart regression that restores a recordless claim, registers its previously unknown receiving account, and redelivers the funding transaction.

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

Comment on lines +209 to +211
let mut stmt = tx.prepare(
"SELECT length(outpoint), outpoint FROM asset_locks WHERE wallet_id = ?1 AND status != 'consumed'",
)?;

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.

🔴 Blocking: Restore the SQLite query-caching invariant for the new queries

The exact-head Rust job fails tc_p1_003_prepare_cached_in_writers on this new tx.prepare call and conn.prepare in core_state::load_spent_claims at line 973. I checked the test and failed-job log: these are its two reported offenders, and neither query matches the explicit read-only allowlist. This is the workspace job's sole reported failure and cancels execution of 1,318 remaining tests. Use prepare_cached at both sites, or explicitly register an eligible read-only query in the existing allowlist, so the workspace gate can complete.

Suggested change
let mut stmt = tx.prepare(
"SELECT length(outpoint), outpoint FROM asset_locks WHERE wallet_id = ?1 AND status != 'consumed'",
)?;
let mut stmt = tx.prepare_cached(
"SELECT length(outpoint), outpoint FROM asset_locks WHERE wallet_id = ?1 AND status != 'consumed'",
)?;

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

Merge the migration-only base without changing the source tree.

<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Hygiene is not checking this pull request: it targets chore/rust-dashcore-1112-v5.1, outside the policy. What was said about its diffs is kept for when it returns; it will queue for a review slot again.

@lklimek
lklimek changed the base branch from v5.1-dev to chore/rust-dashcore-1112-v5.1 October 6, 2026 12:14
@github-actions github-actions Bot removed the waiting-bots Waiting for the review bots to report on this head label Oct 6, 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

Verified the exact head d397edb, its tree equivalence to the previous review head, and all 29 prior findings against the source. Four in-scope blockers remain: statement-cache gate compliance, restored claims missing from subsequently added accounts, malformed amounts admitted before replay validation, and missing claimant provenance for fresh mempool spends. This was a static review; no builds or tests were run, and the supplied CI snapshot still has Rust workspace tests pending.

🔴 4 blocking

4 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: 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); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — The diff makes intricate changes to spent-output restoration and replay in packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs and introduces the historical-accounting storage migration in src/sqlite/migrations/legacy_v019.rs, directly affecting coin selectability and persisted wallet state.
  • 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 — 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-storage/src/sqlite/schema/asset_locks.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/asset_locks.rs:209-211: Restore the SQLite query-caching invariant for the new queries
  (existing thread: https://github.com/dashpay/platform/pull/5150#discussion_r4194208705)
  The source-scanning test `tc_p1_003_prepare_cached_in_writers` rejects schema `.prepare()` calls unless their SQL matches `READ_ONLY_PREPARE_ALLOWED`. Neither this `remove_swept` query nor `core_state::load_spent_claims` at line 973 matches an allowed entry, so the current tree deterministically violates that gate. The relevant evidence is this statement-cache test, not the separate load query-count bound cited in one reviewer explanation. Use `prepare_cached` at both sites, or add narrowly justified read-only exceptions to the existing allowlist; fixing only this query leaves the second offender. The disclosed targeted test results do not establish that this gate passes.

In `packages/rs-platform-wallet-storage/src/sqlite/persister.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/persister.rs:1905-1908: Keep restored spend claims effective for later-added funding accounts
  (existing thread: https://github.com/dashpay/platform/pull/5150#discussion_r4194208698)
  At the exact pinned rust-dashcore revision `870af146`, `ManagedWalletInfo::restore_spent_outpoints` only distributes claims to existing funding accounts. It retains no wallet-level claim ledger, and its documentation explicitly requires reapplication after adding funding accounts. This loader discards the loaded vector after that call, while `PlatformWalletInfo` account-add methods delegate upstream and `register_contact_account` inserts a fresh `DashpayReceivingFunds` account without transferring the claims. Consequently, a recordless or unknown-owner durable spend protects accounts present at startup but not a subsequently added account owning the funding script; funding redelivery can credit the already-spent output, especially after the temporary observed-spend guard has been pruned. Retain and propagate the durable claims at a wallet-wide boundary, or reapply them on every funding-account addition. Add a load → add matching account → redeliver funding → coin-selection regression.

In `packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/rehydrate.rs:634-642: Reject malformed mempool amounts before they can poison wallet startup
  (existing thread: https://github.com/dashpay/platform/pull/5150#discussion_r4185552401)
  The new operand and aggregate checks are enforced on load, but ordinary ingestion and persistence do not enforce the same acceptance domain. In the pinned dependency, `MempoolManager::handle_tx` forwards peer transactions to the wallet checker without a monetary-range check. A transaction containing a small owned receipt and a foreign output valued at `1_u64 << 63` produces fitting wallet-relative accounting: the account checker sums matched outputs, the record retains the complete transaction body, and SQLite repair computes only owned outputs minus owned inputs. `core_state::apply` can therefore commit that body successfully. On the next load, this raw-output validation returns `IntegerOverflow`; Strict load aborts, while Recovery omits the affected wallet. The existing amount regressions inject malformed stored blobs rather than exercising this admission path. Validate replay-relevant amounts and totals before accepting or committing incoming records, retain this replay guard as corruption defense, and provide a safe disposition for already-stored invalid unconfirmed records. Cover peer/event ingestion followed by reopening.

In `packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs`:
- [BLOCKING] packages/rs-platform-wallet-storage/src/sqlite/schema/core_history.rs:222-225: Record claimant provenance for newly persisted mempool spends
  (existing thread: https://github.com/dashpay/platform/pull/5150#discussion_r4185552392)
  Fresh mempool spends reach `core_state::apply` through `cs.spent_utxos`, whose materialized-row writer only sets `spent = 1`; its upsert also leaves new claimant provenance NULL. This repair branch skips attribution for mempool records, even when the incoming record identifies the spender. The new replay repair then correctly treats those NULL rows as independent unknown durable claims and vetoes their release. If a pending loser spends A and B but its settled competitor spends only A, replay removes the loser and identifies B as released, yet SQLite keeps B spent and subsequently restores its unknown guard. The loser history is deleted, leaving no attributable claim to remove. `assert_conflict_restart` masks this ordinary write-path state by manually assigning the loser txid before load. Attribute newly accepted spend reservations atomically from incoming record/input evidence, without overwriting pre-existing unknown or independent claims. Keep the stale-mempool repair guard and add an event-produced conflict/restart regression without SQL provenance patching; this is distinct from the deferred reservation-expiry policy.
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.

  • Expire or revalidate unauthenticated mempool reservations — The peer-processing path still permits unauthenticated observations to reserve wallet inputs, and replay preserves those reservations without expiry. The implementation explicitly documents this behavior, and the PR assigns provenance-aware validation, expiry, and coordinated removal to #5225. This remains a concrete availability exposure, separate from the attributable-conflict release defect retained above.
    • Follow-up: Complete #5225 with coordinated persisted and in-memory release semantics that preserve confirmed and InstantSend-locked claims.
  • Complete shared history restoration across persistence backends — Confirmed-history replay remains SQLite-specific: the shared manager consumes projected wallet state, and FFI restoration carries the separate unconfirmed-outgoing replay bodies rather than complete recorded history. This backend gap predates the PR and is explicitly separated into #5220 because closing it changes the FFI ABI and Swift load path.
    • Follow-up: Validate #5220 separately, including its full FFI funding-redelivery and spend-guard restoration coverage.

@lklimek

lklimek commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Two cases where a free coin stays marked spent after a restart

Found while reviewing the wallet side (rust-dashcore #1112) against this PR at d397edb0. Both come from reading the code on the two sides together; neither was executed. Both hide a real coin for good — in the wallet for the session and in core_utxos permanently — and both hold at the current pin (870af146) as well as at #1112's head.

1. A pending spend is restored as an unknown, permanent claim

  • A mempool spend marks its input with UPDATE core_utxos SET spent = 1 … and no spender (schema/core_state.rs:187). The history repair that writes spent_in_txid skips mempool records on purpose (schema/core_history.rs:222-240, "Stale mempool rows cannot overrule a later sweep's release"). The only other writer is the sweep, which writes the winner.
  • load_spent_claims passes every spent = 1 row (schema/core_state.rs:969-990), so a pending spend reaches restore_spent_outpoints as (outpoint, None). In key-wallet None is never released.

Sequence: T = [o, c] is pending; restart; T is replayed and (o, None) restored. A final W = [c] sweeps T. The wallet drops T's own mark on o, but the None claim still guards it, so released_outpoints is empty. The sweep handler then writes spent = 1, spent_in_txid = W for o (schema/core_state.rs:822-833). o was never spent on chain. Without the restart the same sweep releases o.

Possible fix on this side: at load, pass Some(spender) for a NULL row whose outpoint is an input of a stored mempool record, so the claim goes when that transaction is removed.

2. A held input is attributed to the sweep's winner, not to the transaction still spending it

The live sweep path attributes every non-released input of a removed transaction to superseded_by (schema/core_state.rs:822-833). apply_replay_sweeps already corrects this for replay ("The generic sweep attributes held inputs to its winner; retain the original provenance"); the live path does not.

Sequence: L = [c1, o] and S = [c2, o] are both recorded. W = [c1] sweeps L; the wallet withholds o because S still spends it; the row becomes spent_in_txid = W. Restart: (o, Some(W)) is restored. W2 = [c2] later sweeps S: the claim names W, which is not removed, so o stays guarded and spent = 1. The swept event cannot say which transaction holds a withheld input, so the spender has to be looked up among the stored records here.

Also noted

  • restore_spent_outpoints returns the restored outpoints the wallet already holds as UTXOs; the call at persister.rs:1908 drops it. With utxos trimmed of spent outpoints first (rehydrate.rs:570-575) it should always be empty, so it would work as an assertion that the spent and unspent sets do not overlap.
  • Kept outputs for input matching can only be seeded from a funding record the wallet holds in full. The replay rewrites blocks at or below the last ChainLock to InChainLockedBlock, and keep-finalized-transactions is off, so older fundings are txid-only at restore: a spender delivered by a later rescan is recorded as if the coin were not the wallet's.
  • fix(dashmate): platform should not be enabled on mainnet #1112 is being reworked to hold claims once at wallet level. The signature of restore_spent_outpoints stays the same and one call per load is enough, also for accounts added later. Claims no longer pass to a surviving spender; that case goes back to dev's behaviour (rust-dashcore#1114).

🤖 Co-authored by Claudius the Magnificent AI Agent

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants