Skip to content

chore(deps): bump platform to v3.1.0-dev.8 (9e00c8b894)#862

Merged
lklimek merged 6 commits into
v0.10-devfrom
chore/bump-platform-v3.1-dev
May 29, 2026
Merged

chore(deps): bump platform to v3.1.0-dev.8 (9e00c8b894)#862
lklimek merged 6 commits into
v0.10-devfrom
chore/bump-platform-v3.1-dev

Conversation

@lklimek

@lklimek lklimek commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Bumps dash-sdk and rs-sdk-trusted-context-provider from platform pin 54048b9352 to v3.1-dev HEAD 9e00c8b894 (v3.1.0-dev.8).

This is a prerequisite step ahead of the v0.10-alpha release. It is an L-sized, semantic migration (537 commits / 300 files upstream), dominated by an async Signer<K> rework, the reworked identity state-transition API, and a rust-dashcore/key-wallet 0.42 → 0.43 sideways move — not a mechanical version bump.

What changed (27 files, +414 / −439)

Theme Kind Notes
A DocumentQuery gained select / group_by / having mechanical 15 sites
B Signer is now async + generic Signer<K> with sign_create_witness semantic both DET impls → #[async_trait] + async fn; every .sign() caller now .await
C Identity ST API renames semantic top_up_identity_with_private_key, put_to_platform_and_wait_for_response_with_private_key, try_from_identity_with_private_key, try_from_identity_with_signer_and_private_key (async), sign_external_with_options (async)
D TransactionBuilder reshaped semantic set_selection_strategy / set_current_height / add_inputs + build_unsigned(); removed old string-classification
E ManagedAccount* / WalletManager semantic managed_account_type(), next_change_address, next_receive_address, ManagedAccountTrait import
F AddressProvider associated types semantic Tag = AddressIndex, Address = PlatformAddress; iterator returns + callbacks reshaped
G WalletManager per-wallet synced_height, DashSpvClient<W,N,S> 3 params, run()/stop() lifecycle semantic

Verification

  • cargo build --all-features
  • cargo clippy --all-features --all-targets -- -D warnings (lib + bins + kittest + e2e + backend-e2e)
  • cargo +nightly fmt --all
  • ⚠️ Network/#[ignore] test suites not executed — see pre-merge gates below.

⚠️ Behavioural risk — the dashcore move is sideways, not forward

The dashcore bump is a divergent move (~90 commits theirs we lacked; 1 squashed commit ours they lack), so compile-clean ≠ behaviour-equal. The compiler cannot catch regressions in these areas — they require a runtime smoke test:

  • SPV filter-height gating / rescan-from-genesis trigger
  • Coin selection, fee, and change-output parity
  • Address derivation
  • Orchard 0.12 → 0.13 shielded path
  • SPV client cancellation/lifecycle

Behaviour-ambiguous sites flagged for audit (TODO(v3.1-bump))

  1. src/spv/manager.rs:822 — global filter-height → per-wallet synced_height reset on data clear; verify rescan-from-genesis still fires.
  2. src/ui/wallets/shield_screen.rs:328 — Halo2 proof gen moved off spawn_blocking onto the async worker (builder is now async); watch for runtime starvation.
  3. backend_task/core/mod.rs — change-address: index-peek → next_change_address(…, add_to_state=false).
  4. model/wallet/mod.rs current_balances — now skips stored balances whose index left the pending window.

Pre-merge gates

Co-authored by Claudius the Magnificent AI Agent

Summary by CodeRabbit

  • Bug Fixes

    • Fixed missing async/await operations in identity creation, updates, and contract state transitions
    • Improved wallet address synchronization and per-wallet sync height tracking behavior
    • Enhanced document query field initialization for consistent data retrieval across operations
    • Corrected SPV wallet address derivation metadata and synchronization workflow
  • Chores

    • Updated dash-sdk and trusted-context-provider dependencies to latest platform commit

Review Change Stack

lklimek and others added 3 commits May 29, 2026 09:51
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DocumentQuery: add select/group_by/having fields (15 sites)
- Signer: migrate both impls to async generic Signer<K>; await callsites
- identity ST API: top_up_identity/put_to_platform/try_from_identity_* renamed
- shielded build path now async (build_shield_credit/transition)
- key-wallet 0.43: TransactionBuilder, ManagedAccount*, AddressProvider, run()/stop()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- backend-e2e: await now-async try_from_identity_with_signer
- spv unit test: per-wallet synced_height replaces global filter_committed_height
- core/mod.rs: drop redundant closure call after removing ? early returns
- cargo +nightly fmt --all

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thepastaclaw

thepastaclaw commented May 29, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit d510bcd)

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

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

🗂️ Base branches to auto review (2)
  • master
  • v1.0-dev

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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe5184fa-821b-4931-81a4-f1c0d592ec88

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
📝 Walkthrough

Walkthrough

SDK dependency updates align the codebase with trait signature changes in dash-sdk. Async signer implementations, address provider refactoring, and systematic DocumentQuery field initialization propagate throughout backend tasks and wallet models. SPV transaction building now uses ManagedAccountTrait for account-derived change addresses and UTXO collection, while the SPV manager updates event handling and per-wallet synced height tracking.

Changes

SDK async trait integration and SPV account refactoring

Layer / File(s) Summary
SDK dependency and platform commit updates
Cargo.toml
dash-sdk and rs-sdk-trusted-context-provider are bumped to a new platform commit SHA.
Async signer trait implementations
src/model/qualified_identity/mod.rs, src/model/wallet/mod.rs
Signer<IdentityPublicKey> for QualifiedIdentity and Signer<PlatformAddress> for Wallet are converted to async methods via #[async_trait].
AddressProvider iterator-based refactoring
src/model/wallet/mod.rs, src/backend_task/wallet/fetch_platform_address_balances.rs
AddressProvider implementation switches to iterator-based associated types and return values instead of materialized Vecs; highest-found-index is now derived from result keys.
DocumentQuery explicit field initialization
src/backend_task/{contract,dashpay/contact_requests,document,identity/*,platform_info}.rs, src/ui/tokens/view_token_claims_screen.rs
select, group_by, and having fields are systematically initialized to defaults/empty values across query construction sites.
Missing async awaits in identity operations
src/backend_task/identity/{add_key_to_identity,register_identity,top_up_identity}.rs, tests/backend-e2e/z_broadcast_st_tasks.rs
Adds missing .await calls to async transition construction and platform broadcast operations; refactors identity registration and top-up retry logic to use private-key variants.
SPV transaction building with ManagedAccountTrait
src/backend_task/core/mod.rs, src/context/wallet_lifecycle.rs
build_spv_unsigned_transaction_multi and build_unsigned_payment_tx now derive change addresses from managed account and collect UTXOs via account methods; account type is read via managed_account_type() instead of direct field access.
Shielded credit async transition building
src/backend_task/shielded/bundle.rs, src/ui/wallets/shield_screen.rs
build_shield_credit is converted to async; wallet is cloned before awaiting transition construction to avoid holding RwLock guards across await points; caller switches from spawn_blocking to direct .await.
SPV manager lifecycle and event handler refactoring
src/spv/manager.rs
Event handlers are wired as a vector of Arc<dyn EventHandler> instead of single-handler generic; per-wallet synced heights replace global filter-commit height on clear; cancellation token handling is reworked; receive-address derivation uses new API.
SPV test regression for per-wallet synced height
src/spv/tests.rs
Test is updated to verify per-wallet synced height tracking via update_wallet_synced_height() and wallet_synced_height() instead of global filter-commit height.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • dashpay/dash-evo-tool#832: Changes to SPV manager clearing behavior and run/stop cancellation handling relate to transient quorum/cache issues and synced height state management.

Possibly related PRs

  • dashpay/dash-evo-tool#818: Adds missing .await in E2E broadcast tests for IdentityUpdateTransition construction, which this PR also fixes in z_broadcast_st_tasks.rs.
  • dashpay/dash-evo-tool#791: Both PRs update SPV wallet lifecycle logic in src/context/wallet_lifecycle.rs, specifically account type derivation and address synchronization.

Poem

🐰 The SDK whispered "async now," so the signers obeyed,
Wallets cloned themselves in the sunshine and shade,
Addresses flowed like iterators through the breeze,
While queries spoke clearly of group_by and select, if you please!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: a dependency bump of the platform to a specific version (v3.1.0-dev.8) with the exact commit SHA.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/bump-platform-v3.1-dev

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

❤️ Share

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

@lklimek

lklimek commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

QA audit (Marvin) — 1 CRITICAL blocker, fixes in progress

Full audit traced the behaviour-ambiguous sites. Not mergeable as-is — one critical regression, proven by the branch's own failing unit test.

🔴 QA-001 (CRITICAL) — "Clear SPV Data" no longer triggers rescan-from-genesis — src/spv/manager.rs:833-841

The old unconditional update_filter_committed_height(0) was replaced with per-wallet update_wallet_synced_height(&id, 0). In key-wallet 0.43 that setter is forward-only monotonic (wallet_interface.rs:127-129: "a value below the current is silently ignored"; process_block.rs:269 guards if height > info.synced_height()). So 0 > N is false → the reset is a no-op for any synced wallet. After clearing data, the in-memory height stays stale, the next session declares itself synced, skips genesis rescan, and balance reads stale/zero. clear_data_dir clears det_wallets but never removes the wallet from the WalletManager, so the monotonic guard defeats the reset. Proven: the branch's own test_clear_data_dir_resets_filter_committed_height fails 5000 ≠ 0.

🟠 QA-002 (HIGH) — failing unit test shipped — src/spv/tests.rs:660-695

Plain #[tokio::test] (not #[ignore]) → cargo test --lib is red. The change set was compiled (clippy --all-targets) but its non-ignored tests were never run.

🟡 QA-003 (MEDIUM, suspected) — Halo2 shielded proof blocks a tokio worker — shield_screen.rs:328, bundle.rs:88

Async build_shield_transition runs multi-second CPU proving inline (no spawn_blocking) on the awaiting worker → runtime-starvation risk under batch shielding.

Cleared (PASS)

Change-address derivation, current_balances window, TransactionBuilder fee/change parity, AddressProvider iteration, and the async Signer migration (byte-identical signing; sign_create_witness is a real impl) all verified behaviour-preserving.

Low / informational

QA-004 (latent balance-window drop), QA-005 (change-peek mutates account state on fee estimation), QA-006 (insufficient-funds now includes fee — arguably a fix).

Status: QA-001/002/003 sent back to dev; branch will be updated. Runtime smoke test (regtest/testnet: sync, send, identity top-up, shielded path) remains a hard pre-merge gate given the sideways dashcore move.

Co-authored by Claudius the Magnificent AI Agent

lklimek and others added 2 commits May 29, 2026 10:38
…001)

key-wallet 0.43's update_wallet_synced_height is forward-only monotonic, so
update_wallet_synced_height(id, 0) was a silent no-op for any synced wallet —
the genesis rescan was skipped after a data clear, leaving stale/zero balances.
Write metadata.synced_height/last_processed_height directly to force the rewind.

Test now asserts the real invariant: post-clear the wallet is height 0 AND
wallets_behind() flags it for rescan. cargo test --lib green (468 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_shield_transition runs its CPU-heavy Halo2 proving synchronously inline
before its first await, so awaiting it on a tokio worker starved the runtime.
Drive the build via spawn_blocking + block_on so proving stays off the async
worker pool. Also document the intentional behaviour changes flagged by review:
change-address peek now bumps monitor revision (QA-005), current_balances
filter is lossless for current usage (QA-004), build_unsigned fee-coverage
check now fails earlier (QA-006).

Co-Authored-By: Claude Opus 4.8 (1M context) <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.

Code Review

Large platform v3.1.0-dev.8 migration with careful TODO markers and explicit smoke-test gating on the behavior-ambiguous wallet/SPV/shielded paths. The mechanical adaptations (async Signer awaits, DocumentQuery field expansion, TransactionBuilder reshape, identity ST renames) look consistent. I found no verified blocking regression, but there are a few non-blocking follow-ups worth addressing or explicitly covering in the planned smoke run.

🟡 3 suggestion(s) | 💬 1 nitpick(s)

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

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

In `src/backend_task/core/mod.rs`:
- [SUGGESTION] src/backend_task/core/mod.rs:744-748: Consider add_to_state=true now that the API exposes the choice — current `false` preserves a latent issue
  The new `next_change_address(account_xpub, add_to_state)` makes the address-pool registration explicit. With `add_to_state=false`, the address-pool path in key-wallet generates a new change address (when no pre-generated unused change address exists) without inserting it into `addresses` / `address_index` / `script_pubkey_index` (`address_pool.rs:507-516`). Because DET's SPV registration walks `account.all_addresses()` (`src/context/wallet_lifecycle.rs:518`), and that returns only entries actually in `self.addresses`, a freshly-derived change address used in a send is not added to the SPV watch set.

  Important context for the author and reviewers: this is NOT a fresh regression. The prior code (`wallet_account.derive_change_address(change_index)`) was an immutable `&` borrow and did not register either. The new API simply makes the trade-off visible. Two observations:

  1. The comment correctly notes parity with the prior behavior, and the PR description marks this for smoke testing. If smoke runs confirm that subsequent UTXO scans recover the change output (because the wallet records its own outgoing tx outputs), the existing `false` is defensible.
  2. If the smoke test reveals the change output is not picked up by SPV until a manual recovery/rescan, the simplest fix is `add_to_state=true`. Re-using the same change address across retries inside the `loop` would still work (the address-pool returns the same unused entry on subsequent calls until it's marked used).

  At minimum, the outcome of this smoke check should be documented in the PR before merge, since this site sits right on the boundary the new API was designed to clarify.

In `src/model/wallet/mod.rs`:
- [SUGGESTION] src/model/wallet/mod.rs:2662-2671: pending_addresses() and current_balances() silently drop conversion failures — possible silent sync hang
  Both `pending_addresses()` (2662-2671) and `current_balances()` (2730-2743) use `filter_map(... .ok())` to drop entries where `PlatformAddress::try_from(core_address)` fails. The SDK consumes `pending_addresses()` to learn what is still to be resolved, but `has_pending()` (2724-2728) iterates `self.pending` directly — so any entry that fails conversion never reaches the SDK, never gets `on_address_found` / `on_address_absent`, never gets inserted into `self.resolved`, and `has_pending()` stays `true` forever. Result: silent sync hang with no error and no log line.

  In practice the conversion should not fail (each `pending` entry came from `derive_address_at_index`, which already round-tripped `PlatformAddress::try_from` at insertion), but the silent-drop branch is the kind of invariant-breakage that is very hard to diagnose later. At minimum, log at `error!` when conversion fails and either force-resolve the offending index (so the loop makes progress) or `unwrap()` to surface the invariant violation. The same applies to `current_balances()` at 2738-2742, where the silent skip on a missing-from-`pending` index would corrupt the SDK's view of the stored baseline for incremental catch-up.

In `src/spv/manager.rs`:
- [SUGGESTION] src/spv/manager.rs:1188-1198: Cancellation path can block forever if client.stop() errors without flipping the run flag
  The prior code used `run_cancel.cancel()` — a synchronous, infallible signal — that guaranteed `run()` would unwind. The new path now relies on `client.stop().await` to flip the client's internal running flag, then `(&mut run_future).await` to let `run()` observe it. If `stop()` returns an error in a path that did not actually flip the flag, the subsequent unconditional `(&mut run_future).await` blocks indefinitely, hanging the SPV worker on shutdown or network switch. The error from `stop()` is only logged at `warn!` level today.

  Two straightforward defenses: (a) wrap the post-stop `(&mut run_future).await` in a `tokio::time::timeout` and drop/abort on expiry, or (b) when `stop()` errored, return `Outcome::RunCompleted(Err(...))` immediately without awaiting `run_future`. Adding a `TODO(v3.1-bump)` here — matching the four already-flagged sites — would also give the smoke-test pass a hook to verify the cancellation contract explicitly.

Comment on lines +744 to +748
let change_addr = account
.next_change_address(Some(&account_xpub), false)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Consider add_to_state=true now that the API exposes the choice — current false preserves a latent issue

The new next_change_address(account_xpub, add_to_state) makes the address-pool registration explicit. With add_to_state=false, the address-pool path in key-wallet generates a new change address (when no pre-generated unused change address exists) without inserting it into addresses / address_index / script_pubkey_index (address_pool.rs:507-516). Because DET's SPV registration walks account.all_addresses() (src/context/wallet_lifecycle.rs:518), and that returns only entries actually in self.addresses, a freshly-derived change address used in a send is not added to the SPV watch set.

Important context for the author and reviewers: this is NOT a fresh regression. The prior code (wallet_account.derive_change_address(change_index)) was an immutable & borrow and did not register either. The new API simply makes the trade-off visible. Two observations:

  1. The comment correctly notes parity with the prior behavior, and the PR description marks this for smoke testing. If smoke runs confirm that subsequent UTXO scans recover the change output (because the wallet records its own outgoing tx outputs), the existing false is defensible.
  2. If the smoke test reveals the change output is not picked up by SPV until a manual recovery/rescan, the simplest fix is add_to_state=true. Re-using the same change address across retries inside the loop would still work (the address-pool returns the same unused entry on subsequent calls until it's marked used).

At minimum, the outcome of this smoke check should be documented in the PR before merge, since this site sits right on the boundary the new API was designed to clarify.

source: ['claude', 'codex']

Comment thread src/model/wallet/mod.rs
Comment on lines +2662 to 2671
fn pending_addresses(&self) -> impl Iterator<Item = (Self::Tag, Self::Address)> + '_ {
self.pending
.iter()
.filter(|(index, _)| !self.resolved.contains(index))
.map(|(index, (key, _))| (*index, key.clone()))
.collect()
.filter_map(|(index, (_, core_address))| {
PlatformAddress::try_from(core_address.clone())
.ok()
.map(|pa| (*index, pa))
})
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: pending_addresses() and current_balances() silently drop conversion failures — possible silent sync hang

Both pending_addresses() (2662-2671) and current_balances() (2730-2743) use filter_map(... .ok()) to drop entries where PlatformAddress::try_from(core_address) fails. The SDK consumes pending_addresses() to learn what is still to be resolved, but has_pending() (2724-2728) iterates self.pending directly — so any entry that fails conversion never reaches the SDK, never gets on_address_found / on_address_absent, never gets inserted into self.resolved, and has_pending() stays true forever. Result: silent sync hang with no error and no log line.

In practice the conversion should not fail (each pending entry came from derive_address_at_index, which already round-tripped PlatformAddress::try_from at insertion), but the silent-drop branch is the kind of invariant-breakage that is very hard to diagnose later. At minimum, log at error! when conversion fails and either force-resolve the offending index (so the loop makes progress) or unwrap() to surface the invariant violation. The same applies to current_balances() at 2738-2742, where the silent skip on a missing-from-pending index would corrupt the SDK's view of the stored baseline for incremental catch-up.

source: ['claude']

Comment thread src/spv/manager.rs
@lklimek

lklimek commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

✅ QA re-verify (Marvin) — both blockers resolved at the mechanism level

Re-checked commits 6d1dfe93 (QA-001) and 84ab1a50 (QA-003). cargo test --all-features --lib468 passed, 0 failed.

QA-001 (CRITICAL) — CONFIRMED-FIXED (src/spv/manager.rs:837-844): direct rewind of synced_height/last_processed_height (the monotonic setter cannot express a downward reset). Traced: FiltersManager::tick() reads wallets_behind(committed)reset_for_rescan() → re-downloads from the rewound height; cold-start seeds committed height from the cross-wallet min synced_height. Zeroing both heights is consistent (on-disk UTXO/tx state is wiped, so from-genesis reprocessing rebuilds idempotently). fs::remove_dir_all deletes the disk store, so no persisted height can re-defeat the rescan. Rewritten test asserts the production predicate wallets_behind(PRESEED).contains(id) — genuine, not tautological.

QA-003 (MEDIUM) — CONFIRMED-FIXED (src/backend_task/shielded/bundle.rs:137-170): spawn_blocking + Handle::block_on runs the heavy Halo2 prove off the worker pool on a blocking thread (where block_on is legal — panic path unreachable). Captured inputs are owned/'static; no Send/deadlock issue.

Remaining pre-merge gates (external to code):

Co-authored by Claudius the Magnificent AI Agent

Comment thread src/model/wallet/mod.rs

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

Code Review

Latest delta (commits 6d1dfe9 and 84ab1a5) addresses two QA items cleanly: clear_data_dir now rewinds per-wallet SPV sync metadata with a regression test, and shielded Halo2 proving is offloaded onto the blocking pool via spawn_blocking + Handle::block_on. No new defects in the delta. Three prior findings from the cumulative PR remain valid and carried forward (change-address peek not registering state, SPV cancellation hang on stop() error, dead highest_found field). One prior finding (PlatformAddress conversion drops in pending_addresses/current_balances) is now mitigated by the explanatory comment plus the P2PKH-only construction invariant; dropped as outdated.

🟡 2 suggestion(s) | 💬 1 nitpick(s)

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

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

In `src/spv/manager.rs`:
- [SUGGESTION] src/spv/manager.rs:1194-1200: SPV cancellation can hang indefinitely when client.stop() returns Err
  Carried forward from the prior review — still valid in the current source. On cancellation the code calls `client.stop().await`, logs any error via `tracing::warn!`, and then unconditionally awaits `(&mut run_future).await` to let `run()` observe the stop flag and unwind. The whole termination guarantee rides on `stop()` having flipped the client's internal running flag before it returned. If `stop()` returns `Err` before clearing that flag (e.g. an internal guard-acquisition failure or shutdown handshake error in the SDK), `run_future` has no other cancellation signal to observe and this await blocks forever — stalling SPV shutdown, network switches, and clear_data_dir on the exact error path where graceful shutdown matters most. The previous design used `run_cancel.cancel()`, a synchronous infallible signal that guaranteed unwind. Treat a `stop()` error as a hard cancellation: either `tokio::select!` the final await against `stop_token` again with a short timeout, or skip the await entirely on `Err` and let `run_future` drop. At minimum, verify upstream `DashSpvClient::stop()` always clears the running flag before any fallible work.

In `src/backend_task/core/mod.rs`:
- [SUGGESTION] src/backend_task/core/mod.rs:750-754: Change-address peek with add_to_state=false leaves newly derived change unregistered
  Carried forward from the prior review — still valid. This site still calls `next_change_address(Some(&account_xpub), false)`. In key-wallet 0.43, `next_change_address` delegates to `address_pool::next_unused(..., add_to_state)` which will derive a brand-new change address whenever the internal pool has no pre-generated unused entry. With `add_to_state = false`, that fresh address is returned but not inserted into the account's internal address pool, so the transaction can pay change to an address the wallet has never reserved/registered for monitoring. The wallet can then miss tracking its own change output until some later rescan or unrelated address-generation step discovers it. The new TODO comment correctly explains why the existing `true` path has an unwanted monitor-revision side effect, but does not address this correctness risk — and because this PR migrates DET onto the new explicit `add_to_state` API, closing this hole is part of the migration surface. Either flip to `true` and accept the bloom-filter rebuild, or take a different non-mutating peek that still registers the address before use.

Comment thread src/spv/manager.rs
Comment on lines +750 to +754
let change_addr = account
.next_change_address(Some(&account_xpub), false)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Change-address peek with add_to_state=false leaves newly derived change unregistered

Carried forward from the prior review — still valid. This site still calls next_change_address(Some(&account_xpub), false). In key-wallet 0.43, next_change_address delegates to address_pool::next_unused(..., add_to_state) which will derive a brand-new change address whenever the internal pool has no pre-generated unused entry. With add_to_state = false, that fresh address is returned but not inserted into the account's internal address pool, so the transaction can pay change to an address the wallet has never reserved/registered for monitoring. The wallet can then miss tracking its own change output until some later rescan or unrelated address-generation step discovers it. The new TODO comment correctly explains why the existing true path has an unwanted monitor-revision side effect, but does not address this correctness risk — and because this PR migrates DET onto the new explicit add_to_state API, closing this hole is part of the migration surface. Either flip to true and accept the bloom-filter rebuild, or take a different non-mutating peek that still registers the address before use.

Suggested change
let change_addr = account
.next_change_address(Some(&account_xpub), false)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;
let change_addr = account
.next_change_address(Some(&account_xpub), true)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;

source: ['codex']

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR bumps the pinned Dash Platform Rust dependencies (dash-sdk and rs-sdk-trusted-context-provider) to v3.1.0-dev.8 and applies the required API migrations across DET, primarily around async Signer<K>, updated identity transition APIs, updated SPV/key-wallet APIs, and expanded DocumentQuery fields.

Changes:

  • Updated dependency pins to platform commit 9e00c8b894 and refreshed the lockfile accordingly.
  • Migrated signing/state-transition call sites to the new async + generic Signer<K> and updated identity state transition APIs.
  • Adapted SPV + wallet/address sync code to key-wallet 0.43 API changes (per-wallet sync height, address provider associated types/iterators, client run/stop lifecycle) and updated DocumentQuery initialization to include select/group_by/having.

Reviewed changes

Copilot reviewed 26 out of 27 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
tests/backend-e2e/z_broadcast_st_tasks.rs Awaits updated async identity update transition builder.
src/ui/wallets/shield_screen.rs Uses async shield build API and relies on upstream/offloaded proving.
src/ui/tokens/view_token_claims_screen.rs Initializes new DocumentQuery fields.
src/spv/tests.rs Updates regression test to key-wallet 0.43 per-wallet sync checkpoint semantics.
src/spv/manager.rs Adapts SPV manager to key-wallet 0.43 client lifecycle and per-wallet sync height reset behavior.
src/model/wallet/mod.rs Updates Signer<K> to async and reshapes AddressProvider implementation to new associated types/iterator API.
src/model/qualified_identity/mod.rs Updates identity signer implementation to async Signer<IdentityPublicKey>.
src/context/wallet_lifecycle.rs Updates managed-account APIs (managed_account_type(), all_addresses()).
src/backend_task/wallet/fetch_platform_address_balances.rs Adjusts address sync result typing and highest-found index computation.
src/backend_task/update_data_contract.rs Awaits updated async transition creation API.
src/backend_task/shielded/bundle.rs Makes shield build async and offloads proof generation to blocking pool; updates broadcast flow.
src/backend_task/platform_info.rs Initializes new DocumentQuery fields for withdrawal queries.
src/backend_task/identity/top_up_identity.rs Migrates to top_up_identity_with_private_key API.
src/backend_task/identity/register_identity.rs Migrates to put_to_platform_and_wait_for_response_with_private_key + async retry reconstruction.
src/backend_task/identity/register_dpns_name.rs Initializes new DocumentQuery fields.
src/backend_task/identity/refresh_loaded_identities_dpns_names.rs Initializes new DocumentQuery fields.
src/backend_task/identity/load_identity.rs Initializes new DocumentQuery fields.
src/backend_task/identity/load_identity_from_wallet.rs Initializes new DocumentQuery fields.
src/backend_task/identity/load_identity_by_dpns_name.rs Initializes new DocumentQuery fields.
src/backend_task/identity/discover_identities.rs Initializes new DocumentQuery fields.
src/backend_task/identity/add_key_to_identity.rs Awaits updated async transition creation API.
src/backend_task/document.rs Initializes new DocumentQuery fields for document operations.
src/backend_task/dashpay/contact_requests.rs Initializes new DocumentQuery fields for DPNS lookup.
src/backend_task/core/mod.rs Adapts transaction building + managed-account change/receive address APIs and builder reshaping.
src/backend_task/contract.rs Initializes new DocumentQuery fields for keyword search contract fetch.
Cargo.toml Bumps platform git revisions for dash-sdk and rs-sdk-trusted-context-provider.
Cargo.lock Updates the dependency graph for the new platform commit (dashcore/key-wallet/orchard/etc.).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/spv/tests.rs
Comment thread src/model/wallet/mod.rs
Comment on lines +2662 to +2670
fn pending_addresses(&self) -> impl Iterator<Item = (Self::Tag, Self::Address)> + '_ {
self.pending
.iter()
.filter(|(index, _)| !self.resolved.contains(index))
.map(|(index, (key, _))| (*index, key.clone()))
.collect()
.filter_map(|(index, (_, core_address))| {
PlatformAddress::try_from(core_address.clone())
.ok()
.map(|pa| (*index, pa))
})
Comment thread src/model/wallet/mod.rs
Comment on lines +2739 to +2745
self.stored_balances
.iter()
.filter_map(|(index, _key, funds)| {
let (_, core_address) = self.pending.get(index)?;
let pa = PlatformAddress::try_from(core_address.clone()).ok()?;
Some((*index, pa, *funds))
})
Comment thread src/backend_task/shielded/bundle.rs Outdated
Comment thread src/backend_task/shielded/bundle.rs Outdated
Comment thread src/backend_task/shielded/bundle.rs Outdated
Comment thread src/backend_task/shielded/bundle.rs
Comment thread src/backend_task/shielded/bundle.rs Outdated
Comment thread src/backend_task/shielded/bundle.rs Outdated
Comment thread src/backend_task/shielded/bundle.rs
@lklimek

lklimek commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Testnet smoke (Marvin) — core behaviour GREEN, one test-infra regression

Live testnet, serial backend-e2e on the bump branch.

Functionally PASS (dashcore 0.42→0.43 behaviour preserved)

  • test_spv_sync_and_create_wallet, test_spv_transactions_is_ours_flag — SPV sync + address derivation
  • test_send_and_receive_funds — coin selection / fee / change parity (reworked TransactionBuilder)
  • test_tc001/tc002_refresh_wallet_info — balance reporting
  • test_create_identity — async-Signer registration (the production registration path)
  • test_tc005_create_top_up_asset_lock — functionally correct (passes with adequate stack)
  • test_withdraw_from_identity
  • SPV health GREEN: peers connect ~2s, full sync ~10-20s, balance resolved across ~8 harness restarts.

🟠 New regression (test-infrastructure only) — being fixed

test_tc005_create_top_up_asset_lock overflows the default 2 MB thread stack (SIGABRT) when nested inside the suite's SHARED_IDENTITY.get_or_init fixture. Bisected: passes on v1.0-dev, overflows on the bump branch; RUST_MIN_STACK=32M makes it pass → it's stack size from the deepened async-Signer<K> future graph, not recursion or a logic bug. The production registration path is NOT affected (test_create_identity runs the real task without fixture nesting and passes at default stack). Fix: Box::pin the deep fixture futures (test-harness).

Not validated

  • Shielded lifecycle (QA-003 / orchard 0.13): BLOCKED — shielded ops are feature-gated OFF on this testnet. Needs a shielded-enabled network. Static re-verify earlier confirmed the offload is structurally correct.
  • QA-001 (Clear-SPV-Data rescan): no e2e coverage — manual GUI verification pending.

Remaining pre-merge work

  • Stack fix + full suite re-run to completion (never finished end-to-end on this branch)
  • Shielded path on a shielded-enabled network
  • Manual GUI check of Clear-SPV-Data rescan

Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Correction: the "stack overflow" was a missed run-flag, not a regression

Follow-up to the smoke report above. The earlier SIGABRT was operator error in the smoke run, not a defect:

  • tests/backend-e2e/main.rs:7-12 documents RUST_MIN_STACK=16777216 (16 MB) as required for backend-e2e (the SDK uses deep stacks for state-transition construction; default 8 MB is insufficient). The first smoke run omitted it.
  • Re-run at the documented 16 MB: test_tc005_create_top_up_asset_lock PASSES (asset lock broadcast, TX 54df4898…, 22 s, no SIGABRT).
  • The bump did NOT push past the documented 16 MB requirement. No code change, no docs change, no production impact. Branch tip unchanged (84ab1a50).

Net: the dashcore 0.42→0.43 behaviour is sound on testnet, and there is no stack regression. Full serial suite re-run (with the required env var) is in progress for end-to-end confirmation.

Co-authored by Claudius the Magnificent AI Agent

@lklimek

lklimek commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

✅ Full backend-e2e suite GREEN — zero new regressions from the bump

Full serial run, live testnet, documented RUST_MIN_STACK=16777216. Suite reached completion end-to-end (first time on this branch), no overflow/SIGABRT.

Tally: 55 passed / 4 failed / 0 ignored (647 s).

The 4 failures — all pre-existing or environmental (bisected against v1.0-dev, fail identically there)

Test Bucket
tc_037, tc_043 KNOWN-PREEXISTING — documented "incoming contact request not associated with sender"
tc_014_wallet_platform_lifecycle Pre-existing test-data/funding artifact (AddressesNotEnoughFunds) — reproduces on v1.0-dev
tc_018_fund_platform_address_from_asset_lock Environmental — InstantLock proof propagation timeout (broadcast succeeded); reproduces on v1.0-dev

None are caused by the bump. The dashcore 0.42→0.43 InstantLock/SPV path did not regress.

Priority tests (exercise the changed code) — all PASS

SPV sync, send/receive (coin-selection/fee/change via reworked TransactionBuilder), is-ours/address derivation, balance reporting (TC001/TC002), identity registration (async Signer), top-up asset lock (tc005), withdrawal.

Remaining manual gates (coverage gaps, not failures)

  • Shielded lifecycle (QA-003 / orchard 0.13) — BLOCKED here: shielded ops are feature-gated OFF on this testnet. Needs a shielded-enabled network. (Offload statically re-verified correct.)
  • Clear-SPV-Data rescan (QA-001) — unit-verified; manual GUI check pending.

Net

Code-complete; build/clippy/lib green; QA-audited + re-verified; runtime-validated on testnet with zero new regressions to the extent this network allows. Outstanding items are the two manual gates above.

Co-authored by Claudius the Magnificent AI Agent

@lklimek lklimek changed the base branch from v1.0-dev to v0.10-dev May 29, 2026 10:27
…et clone, test name)

- C: SPV cancellation no longer awaits run_future when client.stop() errors —
  on Err the run flag may not have flipped, so awaiting could hang shutdown;
  treat stop()-Err as a hard cancel and drop the future.
- D: distinguish JoinError cancellation from panic in the shield offload error.
- E: pass Arc<RwLock<Wallet>> into the shield offload and take the read guard on
  the blocking thread instead of deep-cloning the whole Wallet (incl. seed).
- F: rename test_clear_data_dir_resets_filter_committed_height →
  test_clear_data_dir_rewinds_wallet_synced_height.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lklimek lklimek merged commit b93b6b1 into v0.10-dev May 29, 2026
5 checks passed
@lklimek lklimek deleted the chore/bump-platform-v3.1-dev branch May 29, 2026 10:51

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

Code Review

Latest delta (d510bcd) fixes prior finding #1 (SPV cancellation hang): src/spv/manager.rs:1192-1208 now matches client.stop() and only awaits run_future on Ok, treating Err as hard cancellation with a warn log. The shielded offload (src/backend_task/shielded/bundle.rs:140-179) also distinguishes JoinError::is_cancelled from a panic and passes Arc<RwLock> instead of deep-cloning. Prior findings #2 (change-address peek with add_to_state=false) and #3 (highest_found dead state) remain STILL VALID: the new TODO comment at src/backend_task/core/mod.rs:726-734 explicitly acknowledges that the pool is not advanced but does not change runtime behavior, and the derived change address is still used as the real change output on line 771 (not just for fee estimation). Carrying both forward.

🟡 1 suggestion(s) | 💬 1 nitpick(s)

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

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

In `src/backend_task/core/mod.rs`:
- [SUGGESTION] src/backend_task/core/mod.rs:749-755: Change-address derived with add_to_state=false is used as the real change output, leaving it unregistered for monitoring
  Carried forward from the prior review and re-validated at d510bcd3. `account.next_change_address(Some(&account_xpub), false)` is called at line 750-751, and the returned address is then passed to `builder.set_change_address(change_addr.clone())` at line 771 and broadcast as the actual change output — this is not just a fee-estimation peek. In key-wallet 0.43, `next_change_address` forwards `add_to_state` to `address_pool::next_unused`; when no pre-generated unused internal address remains, the fallback `generate_address_at_index(next_index, key_source, add_to_state)` only inserts the new address into the managed account's pool when `add_to_state` is `true`. The new comment at lines 726-734 acknowledges the pool is not advanced and argues the monitor-revision bump is acceptable for bloom-filter rebuild reasons, but a bloom rebuild does not help if the address is not in the wallet's tracked set; the monitoring concern is unresolved. Two consecutive sends before SPV sees the first change tx can also derive the same change index and reuse it. The TODO(v3.1-bump) tag is appropriate, but as long as the runtime behavior persists this stays a correctness/privacy risk.

Comment on lines 749 to +755
let utxos: Vec<_> = account.utxos.values().cloned().collect();
let change_index = account.get_next_change_address_index().unwrap_or(0);
(utxos, change_index)
let change_addr = account
.next_change_address(Some(&account_xpub), false)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;
(utxos, change_addr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Change-address derived with add_to_state=false is used as the real change output, leaving it unregistered for monitoring

Carried forward from the prior review and re-validated at d510bcd. account.next_change_address(Some(&account_xpub), false) is called at line 750-751, and the returned address is then passed to builder.set_change_address(change_addr.clone()) at line 771 and broadcast as the actual change output — this is not just a fee-estimation peek. In key-wallet 0.43, next_change_address forwards add_to_state to address_pool::next_unused; when no pre-generated unused internal address remains, the fallback generate_address_at_index(next_index, key_source, add_to_state) only inserts the new address into the managed account's pool when add_to_state is true. The new comment at lines 726-734 acknowledges the pool is not advanced and argues the monitor-revision bump is acceptable for bloom-filter rebuild reasons, but a bloom rebuild does not help if the address is not in the wallet's tracked set; the monitoring concern is unresolved. Two consecutive sends before SPV sees the first change tx can also derive the same change index and reuse it. The TODO(v3.1-bump) tag is appropriate, but as long as the runtime behavior persists this stays a correctness/privacy risk.

Suggested change
let utxos: Vec<_> = account.utxos.values().cloned().collect();
let change_index = account.get_next_change_address_index().unwrap_or(0);
(utxos, change_index)
let change_addr = account
.next_change_address(Some(&account_xpub), false)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;
(utxos, change_addr)
let utxos: Vec<_> = account.utxos.values().cloned().collect();
let change_addr = account
.next_change_address(Some(&account_xpub), true)
.map_err(|e| TaskError::WalletPaymentFailed {
detail: format!("Failed to derive change address: {e}"),
})?;
(utxos, change_addr)

source: ['claude', 'codex']

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.

3 participants