Skip to content

test(platform-wallet): integration test framework + first transfer test#3549

Draft
Claudius-Maginificent wants to merge 270 commits into
fix/rs-platform-wallet-auto-select-inputsfrom
feat/rs-platform-wallet-e2e
Draft

test(platform-wallet): integration test framework + first transfer test#3549
Claudius-Maginificent wants to merge 270 commits into
fix/rs-platform-wallet-auto-select-inputsfrom
feat/rs-platform-wallet-e2e

Conversation

@Claudius-Maginificent
Copy link
Copy Markdown
Collaborator

@Claudius-Maginificent Claudius-Maginificent commented Apr 27, 2026

Issue being fixed or feature implemented

rs-platform-wallet had a thin test surface (tests/spv_sync.rs, tests/contact_workflow_tests.rs, tests/thread_safety.rs) and no end-to-end harness exercising the full wallet → SDK → broadcast pipeline. This adds a generalised, future-extractable e2e test framework modelled on dash-evo-tool/tests/backend-e2e/, plus the first test case: passing credits between two platform addresses.

Two notable differences from DET:

  1. Bank uses platform addresses (not Core asset locks) as the funding source — initial bootstrap is operator-managed out-of-band.
  2. Future Core support designed in — directory is tests/e2e/ (not platform_e2e/); SPV runtime + ContextProvider modules retained (currently disabled, see below) for re-enablement when SPV stabilises.

Per user direction, production code is kept as close to upstream v3.1-dev as possible — only one real bug fix lands in src/. All test-only convenience accessors (fee_paid, address_derivation_info) were absorbed inside the test framework.

Note on branch state: at user request, the tree of PR #3585 (fix/dpns-case-and-utxo-race-v3.1-dev) was merged into this branch at commit 02cb61b30d to keep local development coherent. PR #3585 is a separate PR targeting v3.1-dev for independent review and merge. The merge in this branch is not a scope addition — it is a development-coherence integration only.

What was done?

Production code (single bug fix only)

src/wallet/platform_addresses/transfer.rsauto_select_inputs was inserting the FULL address balance as the input contribution. The protocol enforces Σ inputs == Σ outputs (with fee strategy adjusting), so when one input had 500B credits and the requested output was 50M, the balance equation broke. Fix: trim the last selected input to required - prior_accumulated so the contribution map matches what the protocol expects. Includes new unit tests covering the trimming behaviour.

This bug affected every caller using InputSelection::Auto — not just our framework.

Cargo.toml — dev-dependencies for the e2e framework (tokio_shared_rt, tempfile, dotenvy, bip39, fs2, serde_json, simple-signer, rs-sdk-trusted-context-provider, dash-async, parking_lot, tokio-util, async-trait).

git diff origin/v3.1-dev -- packages/rs-platform-wallet/src/ shows ONLY the auto_select_inputs fix plus the items listed below.

Refactoring: removed PlatformAddressChangeSet::fee (commit 63f039a8af)

The pub fee: Credits field was added by 085734a239 to back an e2e test assertion that was refactored out before shipping. Zero test callers reference it; FFI consumers were verified clean (PlatformAddressChangeSetFFI::from only reads cs.addresses). Companion dead code in transfer.rs (fee_paid / input_count / output_count plumbing) was also removed. The is_empty() clause that treated a fee-only changeset as non-empty was a semantic mis-classification — removing it is a small correctness improvement.

Test framework (packages/rs-platform-wallet/tests/e2e/)

  • framework/harness.rsE2eContext lazy-init via OnceCell: workdir slot lock → SDK construction → TrustedHttpContextProviderPlatformWalletManager → bank load → registry open → startup cleanup::sweep_orphans.
  • framework/sdk.rs — SDK constructed with TrustedHttpContextProvider::new(network, None, cache_size). Optional PLATFORM_WALLET_E2E_TRUSTED_CONTEXT_URL env override. SPV-based context provider commented out and tracked for re-enablement.
  • framework/bank.rsBankWallet::load parses BIP-39, syncs, panics with operator-actionable message if under-funded (includes the bank's primary receive address).
  • framework/wallet_factory.rsTestWallet, SetupGuard (synchronous Drop that warns and defers to startup-sweep). setup() registers wallets in the persistent registry BEFORE returning the guard so panics leave a recoverable trail.
  • framework/signer.rsSeedBackedPlatformAddressSigner with eager DIP-17 key cache (derives 0..GAP_LIMIT keys at construction; can_sign_with is honest, no async wallet round-trip).
  • framework/registry.rsPersistentTestWalletRegistry, JSON-backed at <workdir>/test_wallets.json, atomic write-temp-+-rename.
  • framework/cleanup.rssweep_orphans (startup) + teardown_one (per-test). Uses InputSelection::Explicit(full_balances) to bypass auto_select_inputs trim semantics; reserves SWEEP_FEE_ESTIMATE = 30M on a fee-bearer input (covers 1-3 input sweeps based on observed testnet fees).
  • framework/wait_hub.rsWaitEventHub implementing PlatformEventHandler, lock-free tokio::sync::Notify-based event bus integration. wait_for_balance is event-driven (no fixed polling).
  • framework/workdir.rsflock-based slot fallback (DET pattern, up to 10 slots) for cross-process safety.
  • framework/context_provider.rsSpvContextProvider using dash_async::block_on (runtime-flavour-agnostic). Currently disabled; module retained for re-enablement.
  • framework/spv.rs — SPV start + wait_for_mn_list_synced with 600s timeout floor matching tests/spv_sync.rs precedent. Currently disabled.
  • cases/transfer.rs — the first test:
    #[tokio_shared_rt::test(shared, flavor = "multi_thread", worker_threads = 12)]
    #[ignore]
    async fn transfer_between_two_platform_addresses() { /* ... */ }
    Bank funds test wallet's addr_1 with 50M; test wallet self-transfers 10M to addr_2; assertions on balances; explicit teardown sweeps remaining funds back to bank.

Documentation

  • tests/e2e/README.md — operator setup, env-var table, bank pre-funding, multi-process safety, panic-safe cleanup, troubleshooting, future Core support.

How Has This Been Tested?

  • cargo check --tests -p platform-wallet — clean
  • cargo clippy --tests -p platform-wallet -- -D warnings — clean
  • cargo fmt -p platform-wallet — applied
  • Live happy-path: passes end-to-end against testnet in 24.76s:
    test cases::transfer::transfer_between_two_platform_addresses ... ok
    test result: ok. 1 passed; 0 failed; finished in 24.76s
    
    • TrustedHttpContextProvider boot: ~3s
    • Bank funded addr_1 with 50M (2.3s confirmation)
    • Self-transfer addr_1addr_2 (202ms confirmation)
    • Post-transfer assertions: funded=50M received=10M remaining=30755720 fee=9244280
    • Teardown sweep: ✓ clean
  • Panic-recovery verified live: across multiple iteration runs, the startup sweep recovered orphan wallets from prior failed runs (startup sweep recovered orphan wallets from prior runs count=N).
  • QA audit (Marvin, 9 findings): all resolved or documented as intentional. See docs/ private notes.

Breaking Changes

None.

Update: bank-rebalance refactor + spec/test consolidation (v37 → v43)

After the initial framework landed, scope expanded to cover 30+ test cases (ID, TK, PA, DPNS, CR cohorts). E2E runs surfaced a systemic flow problem: every test that funded an identity permanently moved credits from the bank's address pool to the bank's identity pool, but the sweep returned them to the identity sink — never back to the addresses. Bank addresses bled ~10 tDASH per run while ~9.58 tDASH sat trapped at the bank identity.

The fix consolidates Platform-side bank flows around a single operator-facing balance:

The operator maintains exactly ONE address balance — the bank's Platform address (tdash1kzz26…). The harness auto-rebalances Platform↔Core internally.

Eight commits implement this end-to-end, plus a sweep-cache-staleness fix and an identity-sync helper:

Commit Type What
2d77385a26 fix (prod) sweep_identities_with_seed now refreshes Identity balance from chain before computing sweep amount — closes silent-leak window where cached balance > chain balance led to symmetric balance X required Y rejections. Stale-cache leak was up to 52.5% per run (20+ TK identities trapped at 14.5B-34.9B).
ab22eff963 test IdentitySync background helper — 15 s cadence refresh of identity balance/revision/keys during test execution. Matches production sibling cadence (PlatformAddressSync, IdentityTokenSync, ShieldedSync).
1e30f633e8 test IdentitySync cadence 3 s → 15 s + per-tick TRACE (3 s overloaded DAPI, regressed TK-005b/TK-011).
abc376b6b4 test New framework/bank_rebalance.rs (~370 LOC + 3 unit tests). Startup drain_bank_identity_to_addresses + refill_core_from_platform_if_below_threshold (fallback if Core L1 balance < threshold). Sweep destination flipped from bank identity → bank Platform address.
8ae72fd2f5 test provision_transfer_key_if_missing auto-adds a TRANSFER-purpose CRITICAL key to the bank identity via IdentityUpdate signed by MASTER auth key. Idempotent. Without this, the production bank identity (which has only AUTHENTICATION keys) cannot satisfy IdentityCreditTransferToAddresses's DPP-protocol TRANSFER-key requirement. Also fixes id_sweep test to target the bank address (was targeting the now-empty bank identity).
75f23eac16 test Expose sweep delta via SweepReport.swept_identity_credits; id_sweep asserts on the returned value instead of the bank-address absolute balance (which is unobservable under parallel execution — sibling spends drain it ~5000× faster than a single sweep replenishes).
6e0fab6bc1 test TK setup wait 60 s → 120 s; augment wait_for_balance timeout error with last_observed, polls, any_balance_change_observed (disambiguates SPV-lag from broadcast-stall).
147218332b test Route TK-013 + TK-014 setup through TK_SETUP_WAIT_TIMEOUT (they bypass setup_with_token_* helpers due to specialized contract shapes).

Headline win (v39 first run)

INFO bank_rebalance: provisioned TRANSFER key on bank identity
       bank_identity_id=Ge9oaXJg… key_id=2 identity_index=47735
INFO bank_rebalance: drained bank identity credits back to bank Platform address
       pre=9577295537946 post=26455100 drained=9577265537946

9 577 265 537 946 credits (~9.58 tDASH) moved bank identity → bank Platform address in 2.43 s on the first run. Subsequent runs hit the idempotent transfer-key provision no-op short-circuit + drain no-op: at fee reserve. The bank identity is now a permanently empty parking spot.

Suite trajectory (full)

Snapshot PASS / FAIL Notable
v32 27 / 4 Pre-funding-cut baseline
v37 (15 s cadence) 25 / 6 Bank-starvation cascade surfaced (ID-002 + id_sweep regressed)
v38 (bank-rebalance refactor) 26 / 5 ID-001/002/003/005/007 + TK-005b/011 rescued
v39 (TRANSFER key auto-provision) 26 / 5 9.58 tDASH drained on first run
v40 (sweep delta + TK 120 s) 26 / 5 id_sweep PASS via SweepReport
v41 (TK-013/014 routing) 27 / 4 All 4 remaining fails are known carries
v42 (sweep-floor chain-derive + TK funding 20.2B/30.2B) 11 / 19 Over-aggressive — state_transition_min_fees underestimated dynamic fee by ~8×
v43 (revert chain-derive; TK 21B/22B/31B; 500M/2.5B peers; PA-010 docs; PA-004b/009 rescope) 26 / 4 Restoration; PA-004b flipped PASS, PA-009/c surfaces V27-007 prod bug
v44 (ID-002b + AL-001 + 5 Found-* pins + CR-004 first-fix) 29 / 7 on 36 New pins surfaced 4 fresh failures (CR-004, ID-002b, AL-001, Found-006) — all rooted in deeper layers than initial pins assumed
v45 (V27-007 prod fix + QA-009 CR-004 math + QA-011 AL-001 split + spec reclassifications) 32 / 5 on 37 ID-002b green; V27-007 ownership-leak in transfer.rs:160 fixed
v46 (Found-024 regression pin + Found-006 rewrite + QA-014 sync_balances) 33 / 4 on 37 (+ 1 / 0 found_024 separately) V27-007 pin landed; Found-006 RED-by-design confirmed; new QA-014 sync gap surfaced
v47 (final batch: QA-013 found_024 #[ignore], QA-015 AL-001 fee reserve, QA-016 CR-004 −2 500, QA-018 stale msgs, QA-014 full-rescan) 34 / 4 on 38 35 deterministic-PASS + 2 red-by-design pins (Found-006, Found-008) + 1 red-by-design pin (CR-004 pins dash-evo-tool#845) + 1 real fail (AL-001 SPV UTXO visibility) + 1 testnet flake (tk_007)
v48 (AL-001 SPV gate fix; TK flake #[ignore] reasons cite Found-025; Cargo.lock sync; TEST_SPEC v47 audit) 34 / 4 on 38 AL-001 two-phase SPV gate applied (403d29c3c8); TK flake attribution tightened to Found-025; status legend formalised; 17 stale matrix cells reconciled
v49 (Found-025 retarget; PR #3585 merge; PlatformAddressChangeSet::fee removed) 34 / 3 on 37 Found-025 fake-pin deleted (109 → 108 tests); #3585 tree integrated for development coherence; fee field removed from production changeset API
v50 (post-#3585 baseline; full e2e w/ ignored pins) 34 / 6 on 40 All pins reproduce deterministically; Found-022 retarget functions as designed; ID-003 wait-gate gap surfaced (since fixed in fc7e9f80a1)
v51 (rust-dashcore #756 chainlock bump scaffolding; CR-004 dust-threshold first retarget) 97 / 11 on 108 First full --include-ignored run; PA-3040, PA-005b ×3, PA-003, PA-008b surface as unclassified failures pending triage
v52 (Found-008 + Found-012/023 issues filed; CR-004 headroom tweak; rust-dashcore rev bump 5297d61a) 98 / 10 on 108 PA-008b newly green; remaining 10 fails are known red-by-design pins + AL-001 + CR-004 retarget signature shift (QA-V52-001) + PA-003/PA-005b ×3 (test-side bugs) + PA-3040

Detailed evidence and trace pointers in PR comment.

Follow-up commits since v41 (now pushed)

Commit Type What
08bac5ebe9 revert Static sweep floor (50M / 30M fee reserve) restored — chain-derived formula missed dynamic per-output storage costs (~8× under-reserve).
b4d144b68a test Corrected TK funding (TK_OWNER_FUNDING_SIMPLE = 21B, _DISTRIBUTION = 31B, new _CONFIG_UPDATE = 22B for tk_012, TK_PEER_FUNDING = 500M, _ACTIVE = 2.5B).
20ac23ce76 docs Cleaned 3 stale PA-010 doc references (README.md, framework/mod.rs intra-doc link, TEST_SPEC.md).
3d916e31d3 test Rescoped PA-004b + PA-009 trim/assertion to test wallet — bank wallet no longer part of test assertions. PA-009 → PASS; PA-004b flipped PASS in v43.
6ac7d6269c test Removed PA-010 placeholder (no longer planned).
c73ccc5beb test Initial role-specific TK funding (superseded by b4d144b68a after v42 starvation).
7135b4c72e test IdentitySync graceful teardown — shutdown_identity_sync() wired into last-guard SetupGuard::Drop. loop exiting DEBUG now fires once per run.
f71183b923 test First chain-derived sweep floor attempt (REVERTED by 08bac5ebe9).
5f955df6d2 docs TEST_SPEC.md: ID-002b spec (asset-lock-funded top-up of existing identity).
5f1dbdacb1 docs TEST_SPEC.md: CR-001/CR-003 status corrections (matrix said "not implemented" while detail said PASS); STUB/not-implemented terminology normalised; duplicate Found-bug-pins section removed.
351d52ab08 docs TEST_SPEC.md: AL-001 spec (concurrent asset-lock builds from same wallet).
403d29c3c8 test AL-001 SPV-visibility fix: two-phase gate added before concurrent fan-out — (1) aggregate balance via wait_for_core_balance, then (2) poll spendable_utxos(height).len() > N to confirm coin-selection sees the split UTXOs before spawning tasks. Closes the window where all concurrent top_up_identity_with_funding calls raced an empty UTXO list.
fe44be56ff test TK flake attribution: 5 TK #[ignore] reasons updated to cite Found-025 (rs-sdk address-sync race at address_sync/mod.rs:619) as the load-bearing cause, replacing generic "DAPI contention" language.
55288c693e chore Cargo.lock whitespace normalisation + rand 0.8.5 → 0.8.6 (transitive, cosmetic).
e9860beb61 docs TEST_SPEC.md v47 status audit: 17 stale matrix cells reconciled; Found-019/020 matrix rows added; status legend formalised (red-by-design / red-real-fail / passing-as-regression).
e7e5d6daba test Found-025 retarget: delete 176-LOC fake-pin; replace with 70-LOC documented stub (no #[test]). TEST_SPEC status → red-by-design — pending upstream test-hook surface. e2e test count 109 → 108.
02cb61b30d merge Integrate PR #3585 tree (fix/dpns-case-and-utxo-race-v3.1-dev) for development coherence. Conflict resolution: broadcast.rs and dpns_usernames/mod.rs took #3585's version; error.rs was a manual union of disjoint error variants (HEAD's GapLimitExceeded + NoSelectableInputs; theirs' ConcurrentSpendConflict + NoSpendableInputs).
63f039a8af refactor (prod) Remove PlatformAddressChangeSet::fee and companion dead code from transfer.rs.

Production code surface from this update

  • src/wallet/identity/network/auto_select_inputs.rs — sweep refresh from chain (commit 2d77385a26). No public API change.
  • src/wallet/platform_addresses/transfer.rs — ownership guard in post-broadcast ledger update (V27-007 fix, 16636f01c0); dead fee_paid / input_count / output_count plumbing removed (63f039a8af).
  • src/wallet/platform_addresses/changeset.rsfee: Credits field and its is_empty() clause removed (63f039a8af).
  • src/wallet/platform_addresses/withdrawal.rs — defensive ownership guard added (16636f01c0).

No public API additions, no breaking changes.

Current failure classification

Test Classification Why it's red
cr_004_legacy_bip32_utxo_update_after_spend red-by-design Pins upstream dash-evo-tool#845 — BIP-32 post-broadcast UTXO not cleared. Test math (Marvin QA-008/QA-016) and next_unused contract ruled out across 3 fix attempts. Fails deterministically until upstream fix lands.
found_006_topup_index_ignored red-by-design Pins upstream CreditOutputFunding missing top_up_index at asset_lock_builder.rs:41-48. Passes only when upstream key-wallet API addition lands.
found_008_lock_notify_missed_wakeup red-by-design (inverted pin) Cargo PASS = bug confirmed. Notify::notify_waiters() drops a lock event when the notify precedes waiter registration. Fix requires notify_one() semantics.
al_001_concurrent_asset_lock_builds red-real-fail (fix in flight) SPV UTXO-index visibility gap under concurrent load. Two-phase SPV gate applied at 403d29c3c8 — outcome to be confirmed in next testnet run (tracked at task #382).

PA-004b, PA-009, ID-002b all flipped PASS via V27-007 + QA-014 fixes. PA-010 was removed entirely. Found-025 test deleted (count 109 → 108; see "New spec entries" below).

TK-suite flake root cause — pinned (Found-025)

Throughout the saga, TK tests intermittently failed with wait_for_balance timed out + any_balance_change_observed=false (tk_001c/tk_002 v43, tk_006 v44, tk_011 v45, tk_007 v47). Initial hypotheses: DAPI replica lag, bank credit-pool depletion, Drive block-inclusion delay.

Three-phase diagnostic workstream resolved this:

  • Phase 1 (Marvin audit) — identified 6 diagnostic gaps in bank.fund_address / wait_for_balance / DAPI client / bank-balance monitoring.
  • Phase 2 (Bilby commit 5cca0fbd1a) — added trace-level enrichment (logging-only): bank.fund_address broadcast-accepted INFO includes ?target = addr, credits, seq, attempt, elapsed_ms; wait_for_balance timeout error enriched with grep hint pointing to the originating broadcast log + tx hash.
  • Phase 3 (Marvin reproduction) — hit the flake on the first run of cargo test -- cases::tk_ under default parallelism. Pinpointed the exact failure site at address_sync/mod.rs:619.

All three original hypotheses eliminated. The bug is in upstream rs-sdk. Found-025 is documented as a stub (tests/e2e/cases/found/found_025.rs) pending an upstream test-hook surface. The 5 affected TK tests cite Found-025 explicitly in their #[ignore] reason text.

Reinstating a genuine Found-025 pin requires an upstream rs-sdk refactor — either an apply_balance_updates inner-function extraction or a transport-trait seam that lets Sdk::new_mock() return proof-verified responses.

TK suite operational reality: TK-001 through TK-014 are GREEN under live testnet when running with --test-threads=1 or --test-threads=2. Intermittent flakes under higher parallelism are Found-025 — not random noise.

V27-007 production fix

src/wallet/platform_addresses/transfer.rs:160 had an ownership-leak in the post-broadcast ledger-update loop: the SDK response's foreign post-credit balance was written into the source wallet's local ledger via set_address_credit_balance without an ownership check. Symptom chain: total_credits() reported the foreign balance → dust-gate check inflated → spurious sweep attempt → No private key for address P2pkh(...) signer panic. Fix: if account.contains_platform_address(&p2pkh) guard, mirroring fund_from_asset_lock.rs:77. Pinned at Found-024. Same guard added defensively at withdrawal.rs.

Asset-lock subsystem audit (handoff-ready)

Marvin audited upstream key-wallet (rust-dashcore @ d6dd5da) for asset-lock correctness issues. Output: handoff-ready for the rust-dashcore team.

5 upstream findings (1 HIGH + 2 MEDIUM + 2 LOW):

# Severity Issue
1 HIGH CreditOutputFunding plumbs only identity_index; DerivationPath::identity_top_up_path(network, identity_index, top_up_index) exists at key-wallet/src/bip32.rs:1062-1077 but the second index never reaches it. Root cause of Found-006 downstream.
2 MEDIUM TransactionRecord::update_context (transaction_record.rs:181-184) silently drops InstantLock state on InstantSend → InBlock transition.
3 MEDIUM AssetLockBuilder::build marks change-pool index used BEFORE build can fail (asset_lock_builder.rs:242 → :292). Doc-comment "no addresses consumed if build fails" is misleading.
4 LOW Missing find_transaction_record(&Txid) helper forces every consumer to roll their own loop — predictably misses CoinJoin / BIP-32 accounts (Found-012 in downstream).
5 LOW AssetLockFundingType doc-comments cite wrong DIP-9 indices (5'/1' not 5'/0'; 5'/2' not 5'/1').

Refuted hypotheses: Found-008 (notify_waiters) is purely downstream; Found-013 (error swallowing) is downstream flattening rich upstream variants; next_private_key is NOT idempotent (calls mark_index_used before return).

TEST_SPEC.md status taxonomy

The spec now uses three precisely-defined status values for red/failing entries (per v47 audit, commit e9860beb61):

  • red-by-design — test is #[ignore]'d and expected to fail until a specific upstream fix lands; the failure documents the bug contract. (Found-006, Found-008, CR-004)
  • red-real-fail — test exists and runs but fails for a reason that is not a designed pin — a genuine regression or gap under active investigation. (AL-001, TK-007 intermittent)
  • passing-as-regression — test passes today, pinning a now-fixed bug contract. A future regression flips it RED. (Found-024, Found-020)

Retired terms: failing and failing-by-design — replaced by the above.

Found-025 carries a separate status: red-by-design — pending upstream test-hook surface (stub only, no #[test] attribute).

New spec entries (TEST_SPEC.md)

  • ID-002b — Asset-lock-funded top-up of existing identity (P1, blocked on Core bank funding). Coverage gap: top_up_identity_with_funding with TopUpFundingMethod::FundWithWallet has zero e2e test.
  • AL-001 — Concurrent asset-lock builds from same wallet (P1, red-real-fail). New AL category for asset-lock-primitive tests.
  • Found-019 and Found-020 — Added to Found-bug-pins matrix (previously had detail sections but no matrix rows).
  • Found-021 — Implemented as red-by-design test pin. Pure unit test, no harness — fails today against the upstream key-wallet defect (IS-lock dropped on InBlock promotion).
  • Found-022 retarget (262ba3455f) — Original internal_addresses.highest_used assertion was wrong-from-inception: the defect path advances highest_generated, not highest_used, and next_unused early-returns from the gap-limit-pre-populated window without mutating the pool. Empirical diagnostic identified bump_monitor_revision() at managed_core_funds_account.rs:540 as the unconditional side-effect on the failed build_asset_lock path; the test now snapshots monitor_revision pre-/post-build and asserts no advance. Still red-by-design; reproduces deterministically.
  • Found-025 retarget (e7e5d6daba) — Marvin's red-by-design sweep identified the v47-era pin as a fake: the test built a local HashMap from two pre-registered addresses and asserted .get() for a third key — an assertion that could never distinguish bug-present from bug-fixed, and one that never called sync_address_balances. The broken 176-LOC pin was deleted. A 70-LOC documented stub (#[cfg(test)] module, no #[test]) replaces it with a clear blocker note. Status: red-by-design — pending upstream test-hook surface. Reinstating a genuine pin requires either an apply_balance_updates extraction in rs-sdk or a transport-trait seam compatible with Sdk::new_mock().
  • TK-011 / TK-013 / TK-014 wait gates (6951523575, 6bb0017adf, c00f1d43aa) — Added wait_for_token_balance between the token state-transition and the post-state probe. Mirrors the gating pattern already used by TK-001 / TK-004 / TK-007 / TK-008 / TK-009. Eliminates a class of intermittent v48/v49 flakes orthogonal to Found-025.

PR #3585 merge — UTXO race fix + DPNS improvements

Commit 02cb61b30d integrates the fix/dpns-case-and-utxo-race-v3.1-dev tree into this branch at user request. This is a development-coherence merge only — PR #3585 remains a separate PR targeting v3.1-dev for independent review and merge.

What lands from #3585:

  • New wallet/core/reservations.rsOutpointReservations system with RAII guard that prevents concurrent UTXO double-spend at the reservation level rather than detecting it post-build.
  • DPNS case-insensitive .dash handling.
  • 7 new lib tests including concurrent_same_utxo_sends_resolve_via_reservation_set (real tokio::spawn concurrency) and broadcast_failure_releases_reservation_for_retry.

Conflict resolution (3 conflicts):

Marvin audit notes — both resolved:

  • 603b444425's fresh-state revalidation on build is superseded by the reservation guard — defense-in-depth layer gonefixed in bd5eb6120 (backport of 371e2c3742 from PR fix: case-insensitive .dash + atomic state on broadcast failure #3585). Re-fetches spendable_utxos(current_height) after build_signed inside the same write lock; aborts with ConcurrentSpendConflict if any selected outpoint disappeared. Reservation system remains primary defense; this is restored defense-in-depth.
  • GapLimitExceeded variant has no production constructor — candidate for cleanupfixed in 926f0a7e0d. Variant deleted from error.rs; e2e framework now uses a local GapLimitError enum in framework/gap_limit.rs (Exceeded + Wallet(Box<PlatformWalletError>)) — test-only, doesn't pollute the production error type.

Companion PRs

Issues raised from this PR's e2e triage

Upstream defects surfaced and pinned by this branch's e2e suite. Each has a deterministic #[ignore]d test in tests/e2e/cases/ that flips green when the upstream fix lands.

dashpay/platform:

dashpay/rust-dashcore:

Outstanding (deferred)

  • SPV re-enablement (Task fix(sdk): hash is not a function #15 internal): re-enable SPV start + SPV-based ContextProvider once SPV cold-start is reliable on testnet. Modules retained, harness blocks commented out.
  • Per-address transaction history (out of scope): scoped at ~2000-3000 LOC across Drive (state-tree change + breaking platform-version bump), SDK, wallet, FFI, Swift. Recommended as a separate ADR + multi-PR rollout. Full scope analysis kept in private notes.
  • Sweep fee estimator dynamic refinement (latent): SWEEP_FEE_ESTIMATE = 30M is a constant calibrated against observed testnet fees (1-input ~9.5M, 2-input ~21M). Long-term: derive from transfer::estimate_fee_for_inputs (currently private) — needs a small public helper.
  • Found-025 reinstatement (upstream dependency): genuine pin requires either an apply_balance_updates inner-function extraction in rs-sdk or a transport-trait seam compatible with Sdk::new_mock(). Tracked as a follow-up for the rs-sdk team.
  • ID-003 wait-gate (from v50 e2e run, MEDIUM)fixed in fc7e9f80a1. Inserted wait_for_identity_balance_change (canonical post < pre gate already used by TK-006/007/008 — wait_for_identity_balance would have been wrong shape since it's a >= floor helper and the sender's balance debits across an identity-to-identity transfer).

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 (this IS the e2e test framework)
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any (no breaking changes)
  • I have made corresponding changes to the documentation if needed (README + module-level rustdoc)

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Co-authored by Claudius the Magnificent AI Agent

@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 27, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Important

Review skipped

Draft detected.

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bc0ba3d-398a-4dad-8a9d-7e957c926ea1

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

An end-to-end testing framework for rs-platform-wallet is added with shared process context, bank wallet funding, persistent wallet registry, cleanup/orphan sweeping, and complete test infrastructure including configuration management, SDK setup, event notification, and example test cases demonstrating transfers between platform addresses. Related SDK modules are also exposed for external use, and seed-based signer constructors are introduced.

Changes

Cohort / File(s) Summary
E2E Framework Infrastructure
packages/rs-platform-wallet/tests/e2e/framework/config.rs, workdir.rs, registry.rs
Environment variable loading with .env file support, workdir slot reservation with exclusive file locking, and persistent JSON-backed wallet seed registry with atomic writes and corruption recovery.
E2E Harness & Context
packages/rs-platform-wallet/tests/e2e/framework/harness.rs, mod.rs
Process-shared E2eContext initialization via OnceCell, SDK/wallet manager/bank/registry/event-hub injection, setup utilities for seed generation and test wallet creation with registry entry insertion.
Bank Wallet & Funding
packages/rs-platform-wallet/tests/e2e/framework/bank.rs
BIP-39 mnemonic-backed bank wallet with minimum-credit validation, DIP-17 address derivation, and concurrent-safe fund transfers using global async mutex to prevent nonce races.
Cleanup & Lifecycle
packages/rs-platform-wallet/tests/e2e/framework/cleanup.rs, wallet_factory.rs
Orphan wallet sweeping at startup with fund drainage and registry status updates, per-test teardown with unregistration, plus test wallet factory with address selection, balance sync, and cleanup guards.
SDK & Event Infrastructure
packages/rs-platform-wallet/tests/e2e/framework/sdk.rs, wait_hub.rs, context_provider.rs
SDK construction with TrustedHttpContextProvider, testnet DAPI endpoint defaults, WaitEventHub for event-driven async test waits, and SpvContextProvider implementing platform SDK context bridge.
Wait & Polling Utilities
packages/rs-platform-wallet/tests/e2e/framework/wait.rs, spv.rs
Generic polling loop with timeout, event-driven balance-change waiter with sync/notification integration, SPV client startup and masternode-list sync readiness checker with progress logging.
E2E Test Cases & Entry Points
packages/rs-platform-wallet/tests/e2e.rs, cases/mod.rs, cases/transfer.rs, README.md, .env.example
Integration test root module, test case organization, fund-and-transfer example test verifying fee deduction, framework documentation with operator setup requirements and architecture overview, and environment configuration template.
Cargo Dependencies
packages/rs-platform-wallet/Cargo.toml
Dev-dependency additions: tokio-shared-rt, tempfile, dotenv, bip39, fs2, parking_lot, simple-signer, SDK context provider, plus tokio-util rt feature.
SDK Public API Exposure
packages/rs-sdk/src/platform/transition.rs, transition/address_inputs.rs
Module visibility change from crate-restricted to public; functions fetch_inputs_with_nonce and nonce_inc exposed for external callers.
Signer Feature & Constructors
packages/simple-signer/Cargo.toml, signer.rs
New derive feature pulling key-wallet and thiserror dependencies; two seed-based constructors for platform-address and identity signers with BIP-32 derivation and error mapping.

Sequence Diagram(s)

sequenceDiagram
    participant Test as E2E Test
    participant Harness as E2eContext Harness
    participant Registry as Wallet Registry
    participant Bank as BankWallet
    participant TWallet as TestWallet
    participant Manager as PlatformWalletManager
    participant SDK as SDK/PlatformWallet
    participant Cleanup as Cleanup

    Test->>Harness: init() first call
    Harness->>Registry: open(test_wallets.json)
    Harness->>Cleanup: sweep_orphans()
    Cleanup->>Registry: list_orphans()
    Cleanup->>Manager: create from orphan seed
    Cleanup->>SDK: sync & drain to bank
    Cleanup->>Registry: remove_orphan_entry
    Harness->>Bank: load from mnemonic
    Harness->>Bank: sync_balances()
    Harness->>Bank: fund_address(test_addr1, credits)
    Harness->>SDK: transfer via bank wallet
    Test->>Test: setup() generates seed
    Test->>Manager: create TestWallet
    Test->>TWallet: create(seed)
    Test->>TWallet: next_unused_address() → addr2
    Test->>Bank: fund_address(addr2, TRANSFER_CREDITS)
    Test->>SDK: transfer via bank
    Test->>TWallet: wait_for_balance(addr2, expected)
    TWallet->>SDK: sync_balances()
    Test->>SDK: transfer(addr2 → addr1, TRANSFER_CREDITS)
    SDK->>SDK: execute, compute fee
    Test->>TWallet: verify balances & fee
    Test->>Test: teardown()
    Test->>Cleanup: teardown_one(test_wallet)
    Cleanup->>TWallet: drain all addresses to bank
    Cleanup->>Registry: remove_entry
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes


🐰 Hopping through the test warren,
New E2E frames make wallets soar-in',
Bank funds and registry keep,
While cleanup sweeps run deep,
SDK paths now public—hooray, no more hide-n'! 🌙✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding an integration test framework and the first transfer test for platform-wallet, which aligns with the extensive test infrastructure added across the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 feat/rs-platform-wallet-e2e

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 changed the title feat(rs-platform-wallet): integration test framework + first transfer test test(platform-wallet): integration test framework + first transfer test Apr 27, 2026
@lklimek lklimek requested a review from Copilot April 27, 2026 14:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an end-to-end (wallet → SDK → broadcast) integration test harness to rs-platform-wallet and introduces the first live test case (address-funds transfer), alongside a production fix to InputSelection::Auto input selection so generated transitions satisfy protocol structure rules.

Changes:

  • Added a reusable E2E framework under packages/rs-platform-wallet/tests/e2e/ (workdir slot locking, bank wallet, persistent registry, cleanup/sweep, wait hub, signer, SDK wiring).
  • Added the first E2E test case: transferring credits between two platform-payment addresses in a test wallet (ignored by default).
  • Fixed auto_select_inputs in production code to avoid selecting full balances as “input credits”, and added unit tests for the selection logic.

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs Fixes auto input selection; adds pure helper + unit tests for selection behavior.
packages/rs-platform-wallet/tests/e2e.rs Adds the integration test crate root and module wiring for the e2e suite.
packages/rs-platform-wallet/tests/e2e/README.md Operator/setup documentation for running live e2e tests.
packages/rs-platform-wallet/tests/e2e/cases/mod.rs Declares e2e test modules.
packages/rs-platform-wallet/tests/e2e/cases/transfer.rs First e2e test exercising funding + self-transfer + teardown.
packages/rs-platform-wallet/tests/e2e/framework/mod.rs Framework public surface (setup, errors, prelude) and module layout.
packages/rs-platform-wallet/tests/e2e/framework/harness.rs E2eContext singleton init: config, workdir locking, SDK, manager, bank, registry, startup sweep.
packages/rs-platform-wallet/tests/e2e/framework/config.rs Env/.env configuration loader for the harness.
packages/rs-platform-wallet/tests/e2e/framework/sdk.rs Constructs dash_sdk::Sdk with TrustedHttpContextProvider and DAPI address resolution.
packages/rs-platform-wallet/tests/e2e/framework/workdir.rs Cross-process workdir slot selection via flock.
packages/rs-platform-wallet/tests/e2e/framework/panic_hook.rs Installs panic hook to cancel background work on panic.
packages/rs-platform-wallet/tests/e2e/framework/wait_hub.rs Notify-based hub bridging wallet/SPV/platform events to async waiters.
packages/rs-platform-wallet/tests/e2e/framework/wait.rs Async waiting helpers (event-driven balance wait + generic polling).
packages/rs-platform-wallet/tests/e2e/framework/signer.rs Seed-backed Signer<PlatformAddress> with eager DIP-17 key cache.
packages/rs-platform-wallet/tests/e2e/framework/wallet_factory.rs Test wallet factory + SetupGuard (panic-safe registry-backed lifecycle).
packages/rs-platform-wallet/tests/e2e/framework/registry.rs JSON-backed persistent registry for panic-safe orphan recovery.
packages/rs-platform-wallet/tests/e2e/framework/cleanup.rs Startup sweep + per-test teardown draining funds back to bank.
packages/rs-platform-wallet/tests/e2e/framework/bank.rs Loads and syncs a pre-funded bank wallet; serialized funding API.
packages/rs-platform-wallet/tests/e2e/framework/context_provider.rs Retained (disabled) SPV-backed SDK context provider module for future re-enable.
packages/rs-platform-wallet/tests/e2e/framework/spv.rs Retained (disabled) SPV startup/readiness helpers for future re-enable.
packages/rs-platform-wallet/Cargo.toml Adds dev-dependencies needed by the e2e harness.
Cargo.lock Locks new/updated dependencies for the added test tooling.

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

Comment thread packages/rs-platform-wallet/tests/e2e/framework/cleanup.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/README.md Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/README.md Outdated
Comment thread packages/rs-platform-wallet/tests/e2e.rs Outdated
Comment thread packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/registry.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/registry.rs Outdated
lklimek added 2 commits April 27, 2026 17:00
Triages the seven inline comments left by `copilot-pull-request-reviewer`:

* `auto_select_inputs` now keeps Σ inputs == total_output even when the
  tail candidate was added only to satisfy the per-input fee margin.
  The previous trim path dropped the last input but left earlier
  inputs at full balance, allowing Σ inputs > total_output and
  tripping the protocol's `Σ inputs == Σ outputs` invariant. Selection
  state moved to a `Vec` so the result is built front-to-back from
  insertion order, with a regression test
  (`fee_only_tail_input_does_not_inflate_input_sum`).
* `registry.rs` `atomic_write_json` now persists via
  `tempfile::NamedTempFile::persist`, which uses `MoveFileEx` with
  `MOVEFILE_REPLACE_EXISTING` on Windows (cross-platform overwrite),
  and the module / fn docs match the actual no-fsync behavior.
* Stale "Wave 2 skeleton" / "live run not yet executed" / "15M fee
  estimate" / "multi-thread MUST" notes updated in `e2e.rs`,
  `tests/e2e/README.md`, and `tests/e2e/framework/cleanup.rs` to
  match Wave-7+ reality (`TrustedHttpContextProvider` default,
  runtime-flavor-agnostic `dash_async::block_on`, 30M sweep margin,
  successful live testnet run).

Live happy-path test passes in 20s; unit tests (5/5 for select_inputs,
4/4 for the e2e harness modules) green.
@lklimek lklimek changed the base branch from v3.1-dev to fix/rs-platform-wallet-auto-select-inputs April 28, 2026 07:17
Apply claudius:coding-best-practices rule (≤2 lines preferred, 3 mediocre).
PR #3549 introduced verbose framework comments; tighten without losing signal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/rs-platform-wallet/tests/e2e/cases/transfer.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/cases/transfer.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/cases/transfer.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/cases/transfer.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/bank.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/panic_hook.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/sdk.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/wait.rs
Comment thread packages/rs-platform-wallet/tests/e2e/framework/wallet_factory.rs Outdated
Comment thread packages/rs-platform-wallet/tests/e2e/framework/wallet_factory.rs
lklimek and others added 3 commits April 29, 2026 13:38
…SimpleSigner

Replace SeedBackedPlatformAddressSigner trait implementation with composition
over SimpleSigner. Add from_seed_for_platform_address_account and
from_seed_for_identity constructors to simple-signer (gated on a new `derive`
feature) to enable seed-based eager DIP-17 / DIP-9 derivation.

Closes PR #3549 dedup §3.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing to dashcore::FromStr

Collapse three duplicate parse_network impls (bank.rs, sdk.rs, spv.rs)
into framework::config::parse_network. Preserves the `local` alias for
regtest and delegates the rest to <dashcore::Network as FromStr>.

Closes PR #3549 dedup §3.3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nce_inc} to pub

Promotes the address_inputs module and its two main helpers from pub(crate)
to pub so external test harnesses can drive the address-funds path without
re-implementing the nonce-fetch loop.

Consumer landing in PR #3563 (cases stack); this commit isolates the SDK
visibility change so PR #3549 reviewers see it standalone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/rs-platform-wallet/tests/e2e/framework/signer.rs Outdated
lklimek and others added 11 commits April 29, 2026 14:44
…ework-wide

A single test panic no longer cancels SPV / wait helpers for sibling tests.
Per-test child tokens isolate cancellation; the parent token still fires on
framework shutdown.

Resolves PR #3549 thread r-Zzeu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… transfer test

Test runs on the default tokio_shared_rt(shared) runtime without forcing
multi_thread flavor. Confirms the harness works under single-threaded
scenarios.

Resolves PR #3549 thread r-DD2o.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arget

CodeRabbit caught a critical bug on PR #3554's `select_inputs`: the helper
ensured `Σ inputs.credits == Σ outputs.credits` (the protocol's structural
invariant) but did NOT ensure that the address targeted by
`DeductFromInput(0)` had post-consumption remaining balance >= the
estimated fee.

Worked example from CodeRabbit:
  candidates    = [(addr_a, 20M), (addr_b, 50M)]   // addr_a < addr_b lex
  total_output  = 30M
  fee_strategy  = [DeductFromInput(0)]
  Old result    = {addr_a: 20M, addr_b: 10M}       // Σ matches; addr_a drained
  Drive applies DeductFromInput(0) over inputs sorted by key (BTreeMap order),
  hitting addr_a — whose remaining balance is 0 — so `min(fee, 0) = 0`,
  `fee_fully_covered = false`, validator rejects with
  AddressesNotEnoughFundsError.

The Wave-8 single-input live e2e accidentally avoided this because the
fee target had ~1B credits left over after consumption — multi-input
auto-selected transfers would have hit it on first contact.

This rewrite:

- Phase 1 (unchanged): pick smallest DIP-17-ordered prefix covering
  total_output + estimated_fee.
- Phase 2: identify the fee target = lex-smallest address in the prefix
  (= `BTreeMap` index 0, what `DeductFromInput(0)` will hit per
   `rs-dpp/src/address_funds/fee_strategy/.../v0/mod.rs`).
- Phase 3: consume the *minimum* allowed amount from the fee target
  (`max(min_input_amount, total_output − Σ other balances)`) so it
  retains the most remaining balance for fee deduction. Error out
  with a descriptive AddressOperation if even that minimum leaves
  less than `estimated_fee` remaining.
- Phase 4: distribute the rest of `total_output` across the other
  prefix entries in DIP-17 order.
- Phase 5: defensive invariant checks.

`min_input_amount` is fetched from
`platform_version.dpp.state_transitions.address_funds.min_input_amount`
(currently 100k across v1/v2/v3 of platform-version).

For non-`[DeductFromInput(0)]` fee strategies the helper falls back to
the previous "consume from front" distribution that only enforces the
Σ invariant — none of the wallet's call sites use anything else today.

Tests:
- updated `two_input_selection_trims_only_the_last` →
  `two_input_selection_keeps_fee_headroom_at_index_zero` to assert the
  new distribution AND the headroom invariant.
- updated `fee_only_tail_input_does_not_inflate_input_sum`'s expected
  outputs (the tail is no longer dropped — it absorbs the consumption
  the fee target sheds).
- added `fee_target_keeps_remaining_for_fee_deduction` (CodeRabbit's
  exact scenario, with the headroom invariant as the load-bearing
  assertion).
- added `fee_headroom_violation_errors` (lex-smallest address too
  small to retain headroom → descriptive error rather than transition
  the validator will reject).
- `single_input_oversized_balance_trims_to_output_amount`,
  `insufficient_balance_errors`, `no_candidates_errors` pass unchanged.

`cargo test -p platform-wallet --lib` → 117 / 117 green
`cargo clippy -p platform-wallet --tests -- -D warnings` → clean
`cargo fmt -p platform-wallet --check` → clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ee-headroom bug

Adds `pre_fix_buggy_selector_output_is_rejected_by_protocol_fee_deduction`
to the `select_inputs` test module. Reconstructs the exact `inputs` map
the pre-fix `auto_select_inputs` would have returned for CodeRabbit's
example (candidates (20M, 50M), total_output 30M, `DeductFromInput(0)`),
runs the post-consumption remaining balances through the live dpp
fee-deduction code path, and asserts `fee_fully_covered == false` —
i.e. the protocol rejects it with `AddressesNotEnoughFundsError`.

Distinct from `fee_target_keeps_remaining_for_fee_deduction`, which
asserts the new selector's output meets the headroom invariant. This
reproduction proves the bug at the protocol layer rather than merely
asserting "the new output looks different" — it would have stayed red
without the fix in 9ea9e70.

Verification:
- cargo check --tests -p platform-wallet                 OK
- cargo clippy --tests -p platform-wallet -- -D warnings OK
- cargo fmt -p platform-wallet                           OK
- cargo test -p platform-wallet --lib                    118/118

Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
…descending

Internal-only change to `auto_select_inputs`. Candidates were
previously collected in DIP-17 derivation index order; now they
sort by balance descending before being handed to `select_inputs`.

Mirrors the dash-evo-tool allocator
(`src/ui/wallets/send_screen.rs:155-157`). Effects:

- Single largest balance covering `total_output + estimated_fee`
  => 1-input result, no multi-input case, no lex-smallest fee
  headroom logic firing. Common path simplified.
- Multi-input cases (when the largest alone isn't enough) still
  go through the headroom-respecting distribution introduced in
  9ea9e70 — unchanged, still correct.
- No public API change. `transfer()`, `auto_select_inputs`,
  `select_inputs` signatures all identical.

Adds `descending_order_picks_single_largest_when_sufficient` to
the existing test module to lock in the common-path behavior.
Other tests pass candidates directly to `select_inputs` and are
order-agnostic by design — unchanged.

The `fee_headroom_violation_errors` error message now includes
the fee-target address, its balance, required headroom, and
remaining-after-consumption to ease debugging.

Verification:
- cargo check --tests -p platform-wallet                 OK
- cargo clippy --tests -p platform-wallet -- -D warnings OK
- cargo fmt -p platform-wallet                           OK
- cargo test -p platform-wallet --lib                    119/119

Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
…egy, retry on Phase 3 fail

Addresses the second wave of review findings on PR #3554:

1. [BLOCKING] Phase 4 distribution no longer produces inputs below
   `min_input_amount`. `auto_select_inputs` now filters candidates
   with `balance < min_input_amount` upfront — they cannot legally
   appear in the inputs map. In Phase 4, when a non-fee-target
   tail entry would consume less than `min_input_amount`, the
   residue rolls back into the fee target's consumption (which has
   surplus headroom by construction). Returns a descriptive error
   if rollback would violate the fee-target headroom invariant.

2. [BLOCKING] `transfer()` rejects unsupported `fee_strategy`
   shapes for `InputSelection::Auto`. Auto-select currently only
   implements protocol-correct logic for `[DeductFromInput(0)]`;
   any other strategy returns `PlatformWalletError::AddressOperation`
   with a clear message redirecting callers to
   `InputSelection::Explicit`. Explicit paths still accept
   arbitrary strategies (caller's responsibility).

3. [BLOCKING] When Phase 3 (`fee_target_min > fee_target_max`) fails
   in `select_inputs`, the algorithm now extends the prefix with the
   next candidate and retries instead of erroring out. Larger
   prefixes may yield a different lex-smallest fee target with
   sufficient headroom. Errors out only when candidates are
   exhausted and no covering prefix is feasible.

4. [SUGGESTION] `select_inputs` returns an early descriptive error
   when `total_output < min_input_amount` — the protocol forbids
   this regardless of input shape, so an explicit error beats the
   internal "should never trip" branch that some callers were
   reaching.

5. [SUGGESTION] Existing selector tests now also build a minimal
   `AddressFundsTransferTransitionV0` and run `validate_structure`,
   asserting protocol-level validity in addition to the
   `Σ inputs == total_output` invariant. Catches future regressions
   without needing a live node.

Coderabbit findings DUuz (#3554), DUu1 (#3554), E5L5 (#3554),
thepastaclaw findings F9fo, GMHz, GMH5, GMH_, F9fv addressed.
Outdated F9fk references the renamed test from before 9ea9e70.
Nitpicks F9fz/GMID/F9f5/GMIH deferred (unreachable / low value).

Verification:
- cargo check --tests -p platform-wallet                  OK
- cargo clippy --tests -p platform-wallet -- -D warnings  OK
- cargo fmt -p platform-wallet                            OK
- cargo test -p platform-wallet --lib                     121/121

Co-Authored-By: Claudius the Magnificent <noreply@anthropic.com>
… work

Apply claudius:coding-best-practices rules: length cap (<=2 preferred,
3 mediocre), present-state only (no Wave/PR-number history), two-tier
(strict for internal, liberal for public API rustdoc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-select

Extends transfer() / auto_select_inputs to accept [ReduceOutput(0)] in addition
to [DeductFromInput(0)]. Output 0 absorbs the fee, so input selection skips the
fee-headroom reservation. Σ inputs == Σ outputs invariant preserved via last-
input trim.

5 new tests in auto_select_tests cover happy path, multi-input trim, multi-
output isolation, output-too-small error, and structural validation.

Resolves PR #3549 thread r-aCky's production prerequisite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…om key_wallet

Replace the duplicated DEFAULT_ACCOUNT_INDEX / DEFAULT_KEY_CLASS constants
with a default_platform_payment_account_key() helper that destructures
key_wallet's PlatformPaymentAccountSpec::default(), and pin the const _PUB
values to the same canonical struct's fields. A colocated drift test asserts
PlatformPaymentAccountSpec::default() still matches our pinned constants —
preventing silent drift if upstream defaults change.

WalletAccountCreationOptions::Default is a unit variant (the (account, key_class)
shape lives in the BTreeSet variant, not Default itself), so destructuring
Default directly was not viable. Pinning to PlatformPaymentAccountSpec —
the canonical "what does Default mean for a PlatformPayment account" struct
— is the closest equivalent.

Resolves PR #3549 thread r-aA6u.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t(0)

Pay fees by reducing output 0 instead of deducting from input 0. Simpler
to reason about for test authors — recipients see (requested - fee_share),
no input-side reservation needed.

KNOWN BREAKAGE: the existing transfer_between_two_platform_addresses test
asserts an exact recipient balance and will fail under the new default
(recipient receives 10M - fee_share). Test fixture update is a follow-up.

Also re-aligns import ordering in this file with `cargo fmt --all`
defaults (a minor stray drift from the previous commit).

Resolves PR #3549 thread r-aCky.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dex 0

Sweep-back target uses the bank's address-0 deterministically instead of
advancing the unused-address pool every test run. Avoids accumulation of
empty addresses on the bank wallet across test invocations.

Implementation: derive the DIP-17 platform-payment address at index 0
directly from the bank seed (mirroring simple-signer's derivation logic),
side-stepping the AddressPool's "next unused" cursor that would skip
index 0 once it gets marked used.

Resolves PR #3549 thread r-Jhi_.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lklimek and others added 4 commits May 13, 2026 12:49
…t/rs-platform-wallet-e2e

Brings in fix/dpns-case-and-utxo-race-v3.1-dev which supersedes the post-build
revalidation guard in 603b444 with a pre-build OutpointReservations system
(new packages/rs-platform-wallet/src/wallet/core/reservations.rs) plus DPNS
case-insensitive .dash handling.

The earlier 603b444 commit is kept in history; subsequent commits introduce
the reservation-based approach that prevents the race rather than detecting it.

Conflict resolution: where both branches modified packages/rs-platform-wallet/src/wallet/core/broadcast.rs, took #3585's version since that's the superior solution. Test-side edits under tests/e2e/ from #3549 are preserved verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…:fee field

The `pub fee: Credits` field on `PlatformAddressChangeSet` was added by
085734a to back an e2e test assertion that got refactored out before
shipping. Zero test callers reference it today and the FFI / SDK consumers
were verified clean (`PlatformAddressChangeSetFFI::from` only reads
`cs.addresses`; the lone FFI `.fee` hit is on Core's `TransactionRecord`).

Changes:
- Drop the `fee: Credits` field and its docstring from `PlatformAddressChangeSet`.
- Drop the `estimated_min_fee()` accessor along with its rustdoc.
- Drop the `self.fee = self.fee.saturating_add(other.fee)` line from `Merge::merge`.
- Drop the `&& self.fee == 0` clause from `is_empty()`. This is a semantic
  improvement: a changeset carrying only a fee figure (no balance entries,
  no sync watermarks, no compaction marker) is operationally empty — there
  is nothing for a consumer to apply.
- Remove the now-dead `fee_paid`, `input_count`, `output_count`, and the
  per-arm count plumbing in `transfer.rs`; the match now yields
  `address_infos` directly and the `PlatformAddressChangeSet` literal
  collapses to `::default()`.

Quality gates: `cargo fmt`, `cargo check --tests --all-features`,
`cargo clippy --tests --no-deps -- -D warnings`, and `cargo test -p
platform-wallet --lib` (146 passed) all clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QA-001 (LOW) from Marvin's #3585 merge audit: #3585's `OutpointReservations`
pre-build filter closes the in-process concurrent-caller race entirely, and
the existing post-build `selected.is_subset(&spendable_outpoints)` check
catches builder regressions. But that check re-uses the SAME `spendable`
snapshot captured BEFORE `build_signed`, so a future mutator that touches
UTXOs outside the wallet write lock (mempool listener, chain reorg
subsystem, cross-process spend) would slip through.

Restore the defense-in-depth pattern from the obsolete commit `603b444425`:
after `build_signed` returns, re-call `managed_account.spendable_utxos`
within the same lock-acquisition block and confirm every selected outpoint
is still present in the fresh view. If not, surface
`PlatformWalletError::ConcurrentSpendConflict` (the typed retryable variant
#3585 introduced) carrying the missing outpoints — semantically correct
post-build, distinct from the pre-build `NoSpendableInputs` failure.

Today no code path mutates UTXOs without holding the wallet write lock we
hold across build, so this is unreachable by construction. The reservations
guard remains the primary in-process race defense; this is the cross-
subsystem / future-proofing net. Without it, someone introducing a parallel
UTXO mutator later would re-open the race silently.

No unit test: injecting a UTXO disappearance between `build_signed` and the
fresh re-fetch requires test-only plumbing inside the same lock-acquisition
block (no clean seam to mock). The two #3585 concurrency tests
(`concurrent_same_utxo_sends_resolve_via_reservation_set`,
`broadcast_failure_releases_reservation_for_retry`) still pass — semantics
of the reservation path are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QA-001 (LOW) from Marvin's #3585 merge audit: #3585's `OutpointReservations`
pre-build filter closes the in-process concurrent-caller race entirely, and
the existing post-build `selected.is_subset(&spendable_outpoints)` check
catches builder regressions. But that check re-uses the SAME `spendable`
snapshot captured BEFORE `build_signed`, so a future mutator that touches
UTXOs outside the wallet write lock (mempool listener, chain reorg
subsystem, cross-process spend) would slip through.

Restore the defense-in-depth pattern from the obsolete commit `603b444425`:
after `build_signed` returns, re-call `managed_account.spendable_utxos`
within the same lock-acquisition block and confirm every selected outpoint
is still present in the fresh view. If not, surface
`PlatformWalletError::ConcurrentSpendConflict` (the typed retryable variant
#3585 introduced) carrying the missing outpoints — semantically correct
post-build, distinct from the pre-build `NoSpendableInputs` failure.

Today no code path mutates UTXOs without holding the wallet write lock we
hold across build, so this is unreachable by construction. The reservations
guard remains the primary in-process race defense; this is the cross-
subsystem / future-proofing net. Without it, someone introducing a parallel
UTXO mutator later would re-open the race silently.

No unit test: injecting a UTXO disappearance between `build_signed` and the
fresh re-fetch requires test-only plumbing inside the same lock-acquisition
block (no clean seam to mock). The two #3585 concurrency tests
(`concurrent_same_utxo_sends_resolve_via_reservation_set`,
`broadcast_failure_releases_reservation_for_retry`) still pass — semantics
of the reservation path are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lklimek
Copy link
Copy Markdown
Contributor

lklimek commented May 13, 2026

v52 e2e run — b1d35e497e

98 PASS / 10 FAIL on 108 (wall-clock 645 s, --test-threads=14, --include-ignored). rust-dashcore rev 5297d61a confirmed in build (picks up dashpay/rust-dashcore#756 chainlock-driven InBlock → InChainLockedBlock promotion + WalletEvent::TransactionsChainlocked).

Failure classification

Test Class Fingerprint
al_001_concurrent_asset_lock_builds red-real-fail FinalityTimeout(<txid>) at line 306 — unchanged shape vs v51. Found-008 (#3641) wait-gap still drops the lock; rust-dashcore #756 fallback present in binary but InChainLockedBlock / TransactionsChainlocked absent from 1.6 MB TRACE log → fallback not reached before 300 s wait_for_proof timeout.
cr_004_legacy_bip32_utxo_update_after_spend red-by-design (signature shifted) QA-V52-001 — signature shift vs v51: failure mode moved from bip32_count_post=1 to NoSpendableInputs { account_type: BIP32Account, ..., context: "all UTXOs used or reserved by in-flight transactions" } at line 264. Commit 6955138813 (dust 2730→546, headroom 2500→700) shifted the failure into the second-build coin-selection path; assertion still wrong-shape for realized behaviour.
found_006_topup_index_ignored red-by-design upstream CreditOutputFunding missing top_up_index — pinned at dashpay/rust-dashcore#762. Unchanged.
found_021_instant_lock_dropped_on_context_promotion red-by-design update_context naive replace overwrites IS-lock on InBlock promotion — pinned at dashpay/rust-dashcore#763. Unchanged.
found_022_asset_lock_builder_consumes_change_index_on_failure red-by-design set_funding eager next_change_address(add_to_state=true) bumps monitor_revision on failed builds — pinned at dashpay/rust-dashcore#764. Unchanged.
pa_003_fee_scaling test-bug fee_1=9402620, fee_5=8250040 — 5-output fee less than 1-output fee. Test's pre-broadcast estimate baseline is incorrect; not a production defect.
pa_005b_gap_limit_triplet_subcase_a test-bug / harness regression gap_limit-1 must succeed: Exceeded { requested: 19, available: 1, highest_used: Some(0), highest_generated: Some(19), gap_limit: 20 }. Pool pre-populated at construction; test asserts on a window the framework already burned.
pa_005b_gap_limit_triplet_subcase_b test-bug / harness regression same shape as _a at boundary requested=20.
pa_005b_gap_limit_triplet_subcase_c test-bug / harness regression assertion left == right failed: left: 1 right: 20 — same root cause as _a/_b.
pa_3040_reduce_output_chain_time_fee_must_not_exceed_static_estimate red-by-design AddressesNotEnoughFundsError, required 14939900 — pins fee-estimator drift on reduce-output chains.

Δ vs v51

  • pa_008b_two_wallets_six_concurrent_funders newly GREEN — was failing in v51 as unclassified concurrency-race; now passes within the 60+ s window. Either v51 was a flake or one of the intermediate commits stabilised the harness path. No fix specifically targeted at this test — track as "stable until reproduced".
  • No regressions: zero v51-green tests went red in v52. The rust-dashcore rev bump (53130869 → 5297d61a) and the CR-004 headroom retarget did not break any previously passing test.
  • AL-001 unchanged shape: panic site + message byte-identical to v51 (FinalityTimeout at line 306). Found-008's downstream impact remains unblocked under concurrent IS-lock-miss; the new chainlock-fallback branch from rust-dashcore fix: ua-parser-js vulnerability  #756 is reachable in principle but is not entered for AL-001 under current timeout (wait_for_proof expires before chainlock arrives for the racing tx).
  • CR-004 signature shift is the lone real regression-against-expectation: the retarget at 6955138813 was supposed to flip CR-004 green; instead the test still fails but on a different assertion path. The realised behaviour matches the spec but the test's post-condition checks the wrong variant. QA-V52-001: rewrite assertion against NoSpendableInputs instead of TransactionBuild, or recalibrate headroom further.

Confirmed by this run

Follow-up tracked

  • QA-V52-001 (CR-004 assertion rewrite, MEDIUM): align the post-condition with the realised error variant — NoSpendableInputs is the truthful outcome on a fully-drained BIP-32 account; the test as currently written expects TransactionBuild.
  • QA-V52-002 (AL-001 fallback trace visibility, LOW): extend RUST_LOG targets to include the wallet asset-lock proof module explicitly so the chainlock-fallback path is observable from logs. Does not affect test outcome; affects only the diagnostic clarity of "was the fallback reached?".
  • AL-001 full-green: still blocked on upstream LockNotifyHandler switch from notify_waiters()notify_one() (Found-008 / Found-008: LockNotifyHandler::notify_waiters() drops lock events arriving in wait_for_proof's check/await gap (concurrent asset-lock builds stall on FinalityTimeout) #3641).
  • PA-005b ×3: harness regression — gap-limit pre-population by WalletAccountCreationOptions::Default invalidates the triplet's assertion baseline. Test-side fix.
  • PA-003: fee-estimator baseline error in the test. Production behaviour is internally consistent; test is wrong.

Marvin's full report saved locally (~1.5 MB log at /tmp/e2e-run-v52-JKjzwc.log).

🤖 Co-authored by Claudius the Magnificent AI Agent

Marvin's merge audit flagged QA-002 (LOW): PlatformWalletError::GapLimitExceeded
had no production constructor — only the e2e harness (framework/gap_limit.rs)
returned it, and only the e2e test case pa_005b_gap_limit_triplet matched on
it. That made it dead public API on the prod error type.

Cleanup:

- src/error.rs: deleted the `GapLimitExceeded { requested, available,
  highest_used, highest_generated, gap_limit }` variant.
- tests/e2e/framework/gap_limit.rs: introduced a test-only `GapLimitError`
  enum local to the harness with two variants: `Exceeded { .. }` (carries
  the same fields the variant used to carry) and `Wallet(Box<PlatformWalletError>)`
  (boxed to satisfy clippy::large_enum_variant — PlatformWalletError is
  ~2.6 KiB). Both `next_unused_receive_addresses` and the pool-level
  `derive_fresh_unused_addresses` now return `Result<_, GapLimitError>`;
  the ceiling check returns `GapLimitError::Exceeded` instead of the
  deleted variant; underlying wallet/pool failures auto-convert via a
  manual `From<PlatformWalletError>` impl so existing `?` sites keep
  working.
- tests/e2e/cases/pa_005b_gap_limit_triplet.rs (sub-case C): match arm
  swapped from `PlatformWalletError::GapLimitExceeded { .. }` to
  `GapLimitError::Exceeded { .. }`; `PlatformWalletError` import dropped
  (no longer needed).
- tests/e2e/framework/gap_limit.rs unit-test module: match arm and
  import updated the same way.
- tests/e2e/cases/pa_001b_change_address_branch.rs:156: prose comment
  reference renamed to `GapLimitError::Exceeded`.

Production code paths and PlatformWalletError consumers (including
platform-wallet-ffi) are untouched — no non-e2e caller ever existed,
confirmed by `rg 'GapLimitExceeded'` across the workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lklimek and others added 3 commits May 13, 2026 13:24
Pre-existing `matches!(result, Err(_))` patterns trip
`clippy::redundant_pattern_matching` under the workspace's `-D warnings`
gate. Swap to `result.is_err()` so the clippy step stays green for the
crates this PR touches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_for_identity_balance

Marvin v50 e2e report finding QA-906 (MEDIUM): the P0 happy-path
identity-to-identity credit transfer was reading the sender's
post-transfer balance via a raw `Identity::fetch(A)` immediately
after the receiver-side `wait_for_identity_balance` gate cleared.

The receiver-side gate only proves that *some* DAPI replica has
applied the transfer block — gRPC requests are not pinned to a node,
and DAPI replicas have observably-different lag, so the immediately-
following sender fetch can round-robin onto a sibling replica that
still sees the pre-transfer balance. v50 reproduced this with
`pre=100000000 post=100000000` ~413 ms after the receiver gate
cleared; the `post_a < pre_a` assertion panicked. The transfer
itself succeeded (receiver delta was exactly `TRANSFER_AMOUNT`).

Fix mirrors the TK-006/007/008 fee-debit pattern: replace the raw
`Identity::fetch(A)` with `wait_for_identity_balance_change(A, pre_a,
STEP_TIMEOUT)`. The transfer always charges the sender (credits
debit + non-zero fee), so any observed delta clears the gate, and
the helper returns the post-balance for the subsequent fee-math
assertions. Same shape as the TK-011/013/014 gates already landed
on this branch.

Reference: /tmp/marvin-e2e-report-v50.md (QA-906).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eam blocker

Updates the AL-001 matrix row and detail section to reflect the
shifted-failure-mode status stable across v48/v49/v50.

Test file: tests/e2e/cases/al_001_concurrent_asset_lock_builds.rs.
Current failure: FinalityTimeout at al_001_concurrent_asset_lock_builds.rs:299
(task 1 IS-lock wakeup never arrives).

Root cause (Found-008): LockNotifyHandler::notify_waiters() in dash-spv
(rust-dashcore) calls tokio::sync::Notify::notify_waiters(), which does not
store a permit for late-registering waiters. If the IS-lock event fires before
task 1's wait future registers, the wakeup is permanently lost.

Two upstream fix options documented: (A) switch to Notify::notify_one() which
queues a permit when no waiter is present; (B) replace Notify with a
broadcast channel with bounded retention.

Records that the original coin-selection race surface is confirmed closed:
403d29c applied the two-phase gate; PR #3585 OutpointReservations
(integrated via 02cb61b) closes it definitively. Marvin's v50 audit
validated PR #3585 is orthogonal to AL-001's remaining gate. AL-001 stays
in the spec as a canary for Found-008 and a regression guard for the
coin-selection surface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lklimek and others added 7 commits May 13, 2026 15:52
… existing FFI code

Core split PlatformWalletError::NoSelectableInputs into three typed
variants (NoSpendableInputs, OnlyOutputAddressesFunded, OnlyDustInputs).
The FFI matcher still referenced the old variant name and failed to
compile.

Widen the dedicated ErrorNoSelectableInputs (=14) mapping to cover all
three new variants so Swift consumers keep the same numeric code and
the typed Display rendering survives as the message. The FFI ABI is
preserved — no renumber, no new code, just a broadened match arm and
refreshed docs/test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…OnlyOutputAddressesFunded

The is_nonce_class_error_rejects_no_selectable_inputs test still
constructed the now-deleted PlatformWalletError::NoSelectableInputs
variant. That variant was split into NoSpendableInputs /
OnlyOutputAddressesFunded / OnlyDustInputs (see commit dc5e611 which
fixed the FFI layer for the same rename). The test target failed to
compile (E0599), blocking the whole --test e2e suite from running.

Retarget the assertion onto OnlyOutputAddressesFunded — the field
shape is the cleanest match to the old NoSelectableInputs{funded_outputs}
and preserves the original semantic intent: an insufficient-funds-shape
error must NOT be classified as nonce-class (retrying would just churn
against the same empty pool).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…b, fix spec drift, file Found-026, link Found-006 to rust-dashcore#762

PA-003 (`green` → `red-real-fail (test-bug)`): marker pre-funding pollutes
`address_funds` rows so the 5-output transfer pays cheap UPDATE per recipient
while the 1-output transfer pays the one-time CREATE. Observed Δfee ≈ 536k
matches one absent create. Drive's chain-time fee at
`validate_fees_of_event/v0/mod.rs:195` drives the cost off real drive ops,
not the static `state_transition_min_fees` floor. The line-235 invariant is
misformulated for the chosen address-derivation strategy.

PA-005b spec drift resolved → truth is `blocked`. Both prior `PASS` claims
(detailed body at line ~534 and the "PR #3609 merged" changelog entry) were
stale: they landed on 2026-05-11 in commit `5c6baabd8f` without re-running
against the QA-002 setup hook that had landed seven days earlier on
2026-05-04 (commit `94902be73b`). Three-way contract mismatch:
QA-002's `consume_platform_address_index_zero` marks index 0 used while the
DIP-17 pool eagerly generates indices `0..=19` in `AddressPool::new`, and
the headroom helper at `framework/gap_limit.rs:188-207` measures
fresh-past-`highest_generated` rather than any-unused-below-ceiling.

PA-008b (`green` → `red-real-fail (concurrency-only)`): full-suite 14-thread
cohort FAILS at the canonical 120s `wait_for_balance` timeout on the first
marker funding; `--test-threads=1` isolation re-run PASSES in 158s.
Suspected race in `PlatformAddressWallet::next_unused_receive_address`
(`platform_addresses/wallet.rs:223-270`) vs concurrent BLAST syncs from
sibling tests.

Found-026 added (P2, MEDIUM, suspected) — `next_unused_receive_address`
pool-cursor bump may not enqueue the address into the BLAST sync provider's
pending set under concurrent load. Symmetric to Found-025 on the rs-sdk side.

Found-006 upstream issue filed: **dashpay/rust-dashcore#762** — Add
`top_up_index` field to `CreditOutputFunding::IdentityTopUp` (DIP-9
conformance gap). Wallet-side TODO in `top_up.rs` updated to reference the
issue; once it lands, drop the `_` prefix on `topup_index` and forward it
through the derivation path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lklimek and others added 5 commits May 14, 2026 13:03
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… amplifier note

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#763

Add tracking-issue cross-link in three places (file-level docstring,
`#[ignore]` reason, in line with the AL-001 / #3641
pattern from commit 27087f8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#764

Add tracking-issue cross-link in the file-level docstring and the
`#[ignore]` reason. Same pattern as Found-021 / #763 (commit 420250d)
and AL-001 / #3641 (commit 27087f8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
shumkov and others added 2 commits May 14, 2026 15:49
Picks up dashpay/rust-dashcore#756 which adds chainlock-driven
transaction finalization in the wallet layer. Previously,
`WalletInterface` had no `process_chain_lock` method and
`dash-spv`'s `SyncEvent::ChainLockReceived` was emitted but never
consumed, so wallet records were stuck at `TransactionContext::
InBlock(_)` forever even when the network produced a chainlock for
the containing block. The new pin promotes records `InBlock →
InChainLockedBlock` on chainlock arrival and emits a new
`WalletEvent::TransactionsChainlocked` variant carrying the
chainlock proof and per-account net-new finalized txids.

For our `wait_for_proof` poll loop this means the chainlock branch
(`record.context.is_chain_locked()`) actually flips when peers
deliver the chainlock — the iter-4 IS→CL fallback path now resolves
correctly instead of timing out at the secondary 180 s deadline.

The new `WalletEvent` variant forces match-arm coverage in two
sites:

- packages/rs-platform-wallet/src/changeset/core_bridge.rs
  `build_core_changeset` returns `CoreChangeSet::default()` for
  the new variant. The wallet has already mutated the in-memory
  record by the time the event fires (upstream is "mutate-then-
  emit"), and the poll loop reads `record.context.is_chain_locked()`
  directly, so no additional persister projection is needed today.
  A future enhancement could persist `WalletMetadata::
  last_applied_chain_lock` for crash recovery, but that's out of
  scope here.

- packages/rs-platform-wallet/src/wallet/core/balance_handler.rs
  `BalanceUpdateHandler::on_wallet_event` returns early for the
  new variant. Chainlocks promote finality (`InBlock →
  InChainLockedBlock`) without changing UTXO state, so there's no
  balance update to deliver.

Extracted from PR #3634 commit 4184a42 onto feat/rs-platform-wallet-e2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 546 (QA-901)

TRACE re-investigation 2026-05-14 confirmed the earlier deterministic
failure was a test-side dust-threshold mismatch (test assumed 2,730 duffs;
upstream `transaction_builder.rs:294`, rev `5313086…`, uses 546). Headroom
changed from 2,500 → 700; new change range [200, 474] is fully sub-dust
across the observed [226, 500] testnet fee range, so the builder folds it
into the fee and the BIP-32 account is truly drained.

CR-004 reclassified red-by-design (dash-evo-tool#845) → passing-as-regression.
The test now pins the symmetric BIP-32 spent-marking path
(TransactionRouter → ManagedAccountCollection → check_transaction_for_match →
update_utxos) plus the upstream sub-dust fold contract. The dash-evo-tool#845
reference is retained as a historical footnote — the symmetric spent-marking
path was confirmed working; any remaining DET surface lives in dash-evo-tool's
own UI refresh path, outside this suite's contract.

TEST_SPEC.md updates: matrix row, detailed body (Layer 2 description + bug
repro note), changelog entry, post-v47 status section, and counts annotation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lklimek and others added 2 commits May 14, 2026 16:02
Cross-link the newly-filed #3642 from the 5 hard-coded
BIP-44 lookup sites in proof.rs + recovery.rs (TODO comments, no logic
change) and from TEST_SPEC.md (matrix rows + detail front-matter +
changelog entry).

Found-012 and Found-023 unify on the same downstream-only fix: replace
`info.core_wallet.accounts.standard_bip44_accounts.get(&account_index)`
with iteration over `all_funding_accounts()` — no upstream change
required; SPV-side tracking already covers all account types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…(already fixed)

Both pins describe contracts that are already satisfied in HEAD:

- Found-019 (SeedBackedIdentitySigner ECDSA_HASH160 re-hash) — fix
  landed at packages/rs-platform-wallet/tests/e2e/framework/signer.rs:148-154
  in commit 59cba08 (PR #3563). identity_key_lookup branches on
  key_type; ECDSA_HASH160 uses key.data() as-is, no re-hash. Production
  packages/simple-signer/src/signer.rs does NOT have the bug shape
  (different storage models).
- Found-020 (PA-001b output_change_address spec/impl drift) resolved
  via spec realignment in PR #3609 (option a). PA-001b rewritten to
  match implicit-change semantics. The parameter doesn't exist in
  production and isn't planned.

Knowledge preserved in memcan; spec clutter dropped. Total Found-bug
pins 26 → 24.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants