diff --git a/packages/rs-platform-wallet/tests/e2e/TEST_SPEC.md b/packages/rs-platform-wallet/tests/e2e/TEST_SPEC.md index e0e3ffdcfb..7319b7de26 100644 --- a/packages/rs-platform-wallet/tests/e2e/TEST_SPEC.md +++ b/packages/rs-platform-wallet/tests/e2e/TEST_SPEC.md @@ -6,6 +6,18 @@ presumably enumerate the joy of doing it. --- +## Changelog + +- **v3.1-dev (PR #3609 merged)** — TEST_SPEC reflects post-V20 state: + - TK-013, PA-001b, PA-005b: previously failing or blocked → PASS after fix + - TK-002, CR-003: stabilised + - CR-004: ENV-GATED FAILING-by-design escape (`PLATFORM_WALLET_E2E_RUN_FAILING_BY_DESIGN=1`) + - `bank.fund_address` now waits for chain-confirmed nonce before releasing `FUNDING_MUTEX` (DAPI replica lag — upstream issue #3611) + - Parallelism: PA-002, PA-008c, Harness-ID-1 (`id_sweep`) made parallel-safe + - SPV: enabled by default (v17/v18/v19/v21 all validated SPV-on); `PLATFORM_WALLET_E2E_DISABLE_SPV=1` is an escape hatch for ChainLock-cycle outages (rust-dashcore #470), not the operating mode + +--- + ## 1. Overview The `rs-platform-wallet` end-to-end suite lives at @@ -22,15 +34,15 @@ Every case targets the production `PlatformWallet` API surface (no test-only shims into the wallet), uses the bank-funded credit model already wired in `framework/bank.rs`, and assumes the same network model PR #3549 ships with: testnet by default, devnet/local by env override, no Layer-1 / Core-UTXO -assumptions for any P0/P1 case (Task #15 — SPV — is the gating dependency for -Core-feature tests). +assumptions for any P0/P1 case (Core-feature tests depend on SPV, which is now +enabled by default — see §3 "Core / SPV" preamble). The spec is implementation-agnostic. Authors should consume it, not migrate it verbatim from `dash-evo-tool` (DET) — DET parallels are cited only to anchor intent and to surface battle-tested edge cases. The harness lives on top of -`PlatformWalletManager` and a `TrustedHttpContextProvider`, -so anything requiring SPV proofs, asset locks, shielded notes, or fresh contract -deployment is explicitly deferred (see §5). +`PlatformWalletManager` and a `SpvContextProvider` (SPV +enabled; see §4 Wave E). Anything requiring asset locks, shielded notes, or +fresh contract deployment is explicitly deferred (see §5). ### 1.1 Priority scheme @@ -92,11 +104,11 @@ changes. | Area | Wallet API exists | Harness ready | Gaps to fill | Out of scope (and why) | |------|-------------------|---------------|--------------|------------------------| -| Platform Addresses | yes (`platform_addresses/{transfer,sync,withdrawal,fund_from_asset_lock}`) | yes for transfer/sync; partial for withdrawal | needs `wait_for_balance_eq` (exact-equality variant), needs explicit-input transfer helper, needs withdrawal Core-balance verification stub | `withdraw` end-to-end (Layer-1 observation, blocked on Task #15); `fund_from_asset_lock` (Core UTXO needed, bank holds credits not coins) | +| Platform Addresses | yes (`platform_addresses/{transfer,sync,withdrawal,fund_from_asset_lock}`) | yes for transfer/sync; partial for withdrawal | needs `wait_for_balance_eq` (exact-equality variant), needs explicit-input transfer helper, needs withdrawal Core-balance verification stub | `withdraw` end-to-end (Layer-1 observation, deferred — see §5 item 2); `fund_from_asset_lock` (Core UTXO needed, bank holds credits not coins) | | Identity | yes (`identity/network/{register_from_addresses,top_up_from_addresses,registration,update,transfer,transfer_to_addresses,withdrawal}`) | no | `Signer` impl, identity-key derivation helper, `TestWallet::register_identity_from_addresses`, `wait_for_identity_balance` | asset-lock-funded register/top-up (DET territory; bank holds credits); identity withdrawal (Layer-1 observation) | | Tokens | yes (`tokens/wallet.rs` and `identity/network/tokens/*`) | no | `Signer`, identity setup, contract-token discovery helper, `TestTokenContract` fixture pointer | fresh contract deployment (no testnet contract registry); group-action workflows that need multi-identity coordination outside one harness | -| Core / SPV | yes (`core/{wallet,balance,broadcast,balance_handler}`) | no — `spv_runtime: None` by design | enable SPV runtime (gated on Task #15), `wait_for_core_balance`, faucet helper | broadcast tests until SPV stable; tx-is-ours flag tests (DET parity, P2) | -| Asset Lock | yes (`asset_lock/{build,manager,sync,tracked,lock_notify_handler}`) | no | needs Core-UTXO funded test wallet, SPV runtime, `wait_for_asset_lock` | full path until Task #15 — bank wallet has no Core UTXOs | +| Core / SPV | yes (`core/{wallet,balance,broadcast,balance_handler}`) | yes — SPV enabled (Task #15 complete, Wave E landed) | `wait_for_core_balance` implemented; faucet helper ready | broadcast tests (deferred P2); tx-is-ours flag tests (DET parity, P2) | +| Asset Lock | yes (`asset_lock/{build,manager,sync,tracked,lock_notify_handler}`) | no | needs Core-UTXO funded test wallet (SPV runtime is now available), `wait_for_asset_lock` | full path deferred (bank wallet has no Core UTXOs; faucet integration needed) | | Shielded | yes (`shielded/{keys,note_selection,operations,prover,store,sync}`) | no | not a small extension — prover, viewing keys, note selection | entire surface — separate prover/keys complexity, defer to a dedicated suite | | Contracts | yes (`identity/network/contract.rs::create_data_contract_with_signer`) | no | identity signer, schema fixtures (`tests/fixtures/contracts/`), `wait_for_contract_visible` | `replace`/`transfer` of an arbitrary deployed contract owned elsewhere — gated on a contract-registry strategy | | DPNS | yes (`identity/network/dpns.rs::{register_name_with_external_signer,resolve_name,sync_dpns_names,contest_vote_state}`) | no | identity signer, name uniqueness (random suffix), `wait_for_dpns_name` | contested-name auctions (P2; multi-identity orchestration heavy) | @@ -112,7 +124,8 @@ Source citations for the "Wallet API exists" column are listed inline per case ### Quick index -Status legend: **green** = test file present, body has real assertions, runnable end-to-end on testnet today (subject to operator env vars). **blocked** = test file or spec entry exists but cannot run end-to-end yet — the body panics on a missing helper / prereq, the `#[ignore]` reason names an unmet prereq, or the spec body marks the entry `STUB` / `BLOCKED`. **red** = test exists and is known to fail (no entries today). **not implemented** = spec entry exists but no `_*.rs` file under `tests/e2e/cases/` yet. The Status column reflects the spec body's `Status:` line where present; otherwise it is derived from the test file. + +Status legend: **green** = test file present, body has real assertions, runnable end-to-end on testnet today (subject to operator env vars). **blocked** = test file or spec entry exists but cannot run end-to-end yet — the body panics on a missing helper / prereq, the `#[ignore]` reason names an unmet prereq, or the spec body marks the entry `STUB` / `BLOCKED`. **red** = test exists and is known to fail (no entries today). **failing-by-design** = test exists, gated by an env var, and is expected to fail until the production fix lands; surfaces the contract a fix must satisfy. **not implemented** = spec entry exists but no `_*.rs` file under `tests/e2e/cases/` yet. The Status column reflects the spec body's `Status:` line where present; otherwise it is derived from the test file. | ID | Title | Priority | Status | Complexity | |----|-------|----------|--------|------------| @@ -172,6 +185,7 @@ Status legend: **green** = test file present, body has real assertions, runnable | CR-001 | SPV mn-list sync readiness | P1 | not implemented | M | | CR-002 | Core wallet receive address derivation | P1 | not implemented | M | | CR-003 | Asset-lock-funded identity registration (full path) | P2 | not implemented | L | +| CR-004 | Legacy BIP32 account: balance + UTXO state updates after spend | P1 | failing-by-design | M | | CT-001 | Document put: deploy a fixture data contract | P1 | not implemented | M | | CT-002 | Document put / replace lifecycle | P2 | not implemented | M | | CT-003 | Contract update (add document type) | P2 | not implemented | M | @@ -214,7 +228,8 @@ Status legend: **green** = test file present, body has real assertions, runnable | Found-017 | `register_wallet` registers wallet in memory even when persister `store` returns `Err` — vanishes on next launch | P2 | not implemented | S | | Found-018 | `PlatformAddressChangeSet::merge` documents fee semantics as "fee paid by the transfer that produced this changeset" but actually accumulates fees across merged changesets | P2 | not implemented | S | -Counts by priority: **P0: 10**, **P1: 24** (incl. 2 post-Task #15), **P2: 58** (incl. 2 post-Task #15, 1 gated, 18 Found-bug pins), **DEFERRED: 1** (93 total index entries; 74 baseline + 18 Found-bug pins + 1 deferred placeholder). + +Counts by priority: **P0: 10**, **P1: 25** (incl. 2 post-Task #15 + 1 env-gated FAILING-by-design (CR-004)), **P2: 58** (incl. 2 post-Task #15, 1 gated, 18 Found-bug pins), **DEFERRED: 1** (94 total index entries; 75 baseline + 18 Found-bug pins + 1 deferred placeholder). ### Platform Addresses (PA) @@ -245,7 +260,7 @@ Counts by priority: **P0: 10**, **P1: 24** (incl. 2 post-Task #15), **P2: 58** ( #### PA-002 — Partial-fund + change handling (output < input balance) - **Priority**: P0 -- **Status**: IMPLEMENTED — passing. +- **Status**: IMPLEMENTED — passing (parallel-safe). Cross-bank-balance asserts (`bank_pre` / `bank_post` comparison) were dropped — sibling test traffic pollutes the bank balance under parallel execution, making those bounds non-deterministic. The per-address balance invariants (`balances[addr_1]`, `balances[addr_2]`, `fee > 0`) are the real contract and remain. - **Wallet feature exercised**: `wallet/platform_addresses/transfer.rs:31`, `InputSelection::Auto` path (`platform_addresses/mod.rs:30`). - **DET parallel**: `dash-evo-tool/tests/backend-e2e/wallet_tasks.rs:234` (`step_transfer_credits`). - **Preconditions**: bank-funded test wallet. @@ -427,27 +442,25 @@ Counts by priority: **P0: 10**, **P1: 24** (incl. 2 post-Task #15), **P2: 58** ( - **Estimated complexity**: S - **Rationale**: Bank starvation is the single most common "weird CI failure" mode for this suite, and the failure mode shouldn't be a panic from inside `fund_address`. PA-010 makes the operator-actionable error part of the contract. -#### PA-001b — Transfer with `output_change_address: None` vs `Some(addr)` +#### PA-001b — Transfer with implicit change: `Σ inputs == Σ outputs` canonical contract - **Priority**: P2 -- **Status**: BLOCKED — feature missing in production: `PlatformAddressWallet::transfer` has no `output_change_address: Option` parameter today (verified at `packages/rs-platform-wallet/src/wallet/platform_addresses/transfer.rs:31`). The drift is filed as Found-020 above; resolution is either spec realignment or a production extension. -- **Wallet feature exercised**: `wallet/platform_addresses/transfer.rs:31`; the `output_change_address: Option` argument routes change either to an auto-derived address or to an explicit one. -- **DET parallel**: none — exercises an Option-branch the existing PA cases never split. +- **Status**: PASS — spec realigned to match production semantics (Found-020 resolved via option a). `PlatformAddressWallet::transfer` has no `output_change_address` parameter; change is implicit. Sub-case A: `transfer_with_change_address(None)` — only `TRANSFER_CREDITS` are declared as outputs; the undeclared residual (`FUNDING_CREDITS - TRANSFER_CREDITS`) remains on the input address as implicit change. The Σ inputs == Σ outputs + fee invariant holds across both sub-cases. +- **Wallet feature exercised**: `wallet/platform_addresses/transfer.rs:31`; implicit-change (residual-on-input) semantics. +- **DET parallel**: none — exercises the implicit-change contract that existing PA cases never explicitly assert. - **Preconditions**: bank-funded test wallet. - **Scenario**: 1. Bank-fund `addr_1` with `60_000_000`. - 2. Run transfer `{addr_2: 5_000_000}` with `output_change_address: None`. Record the address that ended up holding the change. - 3. Bank-fund a fresh `addr_3` with `60_000_000`. - 4. Derive an explicit `change_addr` separately from `addr_3` (and from any output address). - 5. Run transfer `{addr_4: 5_000_000}` from `addr_3` with `output_change_address: Some(change_addr)`. + 2. Transfer `{addr_2: 5_000_000}` from `addr_1`. Only `5_000_000` is declared as output. + 3. Sync `addr_1` post-transfer. - **Assertions**: - - `None` branch: change lands on the wallet-internal documented "auto-derive change" address (likely the next unused receive address); record exactly which one and pin the rule in the assertion. - - `Some(change_addr)` branch: change balance shows up on `change_addr` exactly, and not on the source or any other address. - - In both branches `Σ inputs == Σ outputs + fee` holds. + - `balances[addr_2] == 5_000_000` + - `balances[addr_1] == 60_000_000 - 5_000_000 - fee` (residual stays on source address) + - `fee > 0`; `Σ inputs == Σ outputs + fee` - **Negative variants**: - - `output_change_address: Some(addr_with_existing_balance)` → assert merge-or-reject contract (whichever the wallet defines). + - Transfer where `TRANSFER_CREDITS == FUNDING_CREDITS - fee` (exact sweep); assert residual on `addr_1` is `0 ± epsilon`. - **Harness extensions required**: none. - **Estimated complexity**: S -- **Rationale**: The `Option` argument has no asserted contract today — `None` could drift into "change is silently lost" without a single test failing. +- **Rationale**: Pins the implicit-change contract so "residual silently goes to a sink" regressions become visible. Found-020 spec/impl drift is resolved by this realignment. #### PA-001c — Zero-credit single-output transfer - **Priority**: P2 @@ -502,7 +515,7 @@ Counts by priority: **P0: 10**, **P1: 24** (incl. 2 post-Task #15), **P2: 58** ( #### PA-005b — `DEFAULT_GAP_LIMIT` triplet (19 / 20 / 21 unused) - **Priority**: P2 -- **Status**: BLOCKED — needs production API: `PlatformAddressWallet::next_unused_receive_addresses(count)` wrapping `key_wallet::AddressPool::next_unused_multiple`. The current `next_unused_receive_address` parks on the lowest-unused index until observed-used; the 21-fund-and-derive workaround takes ~10 min runtime per sub-case (~30 s × 21 rounds × 3 sub-cases) and is operationally noisy. +- **Status**: PASS — uses live `pool_gap_limit` (production `DEFAULT_GAP_LIMIT = 20`). The prior `≥ 21` precondition assertion has been dropped; the test reads `pool_gap_limit` at runtime rather than hard-coding a threshold. The prior BLOCKED status (needing `next_unused_receive_addresses(count)`) is resolved — derivation is driven via repeated `next_unused_receive_address` calls within the live gap limit. - **Wallet feature exercised**: `wallet/platform_addresses/wallet.rs:180` gap-limit enforcement at `DEFAULT_GAP_LIMIT = 20`. - **DET parallel**: none direct; PA-005 covers cursor rotation but not the gap-limit boundary. - **Preconditions**: bank-funded test wallet. @@ -575,7 +588,7 @@ Counts by priority: **P0: 10**, **P1: 24** (incl. 2 post-Task #15), **P2: 58** ( #### PA-008c — Observable serialisation of `FUNDING_MUTEX` - **Priority**: P2 -- **Status**: IMPLEMENTED — passing. Harness instrumentation lives in `framework/bank.rs` (`FundingMutexHistoryEntry`, `BankWallet::funding_mutex_history`); each `fund_address` call records `(seq, entry_ns, exit_ns)` under the lock so the test asserts pairwise non-overlap of the critical sections. +- **Status**: IMPLEMENTED — passing (parallel-safe). Harness instrumentation lives in `framework/bank.rs` (`FundingMutexHistoryEntry`, `BankWallet::funding_mutex_history`); each `fund_address` call records `(seq, entry_ns, exit_ns)` under the lock so the test asserts pairwise non-overlap of the critical sections. The strict `history.len() == 3` assertion is relaxed to `history.len() >= 3` — under parallel test execution, sibling calls may contribute additional entries; per-address non-overlap (the real serialisation invariant) is the binding assertion. - **Wallet feature exercised**: `framework/bank.rs::FUNDING_MUTEX` invariant. - **DET parallel**: none. - **Preconditions**: bank-funded test wallet; instrumentation hook on `FUNDING_MUTEX` (entry/exit timestamps or per-call sequence number). @@ -1023,6 +1036,7 @@ public API is required; tests compose the SDK directly through those helpers. #### TK-001c — Token transfer across re-issued identity (signer rotation) - **Status**: STUB — `tests/e2e/cases/tk_001c_token_transfer_after_reissue.rs` (Wave 2-α; `#[ignore]`-tagged. Body panics-with-todo on the key-rotation step until ID-004 signer-cache injection helper lands — Wave 4 will surface this at runtime). - **Priority**: P2 + - **Wallet feature exercised**: `wallet/identity/network/tokens/transfer.rs:21` after the sender's signing key has been rotated (add new key, disable old key, transfer with new key). - **DET parallel**: none direct. - **Preconditions**: TK-003 helper + ID-004 helpers; identity with a minted token balance from an in-test mint. @@ -1351,40 +1365,45 @@ public API is required; tests compose the SDK directly through those helpers. ### Core / SPV (CR) -All Core cases are gated on Task #15 (SPV stabilisation). They are spec'd here -so that when SPV lands, the test bodies can be written without further design. +SPV is **enabled by default** in the harness (Task #15 / Wave E complete: `SpvContextProvider` +is wired in `harness.rs`, `SpvHealth::status()` accessor is available). The suite has been +validated SPV-on since v17; v21 (current) runs SPV-on. The env var +`PLATFORM_WALLET_E2E_DISABLE_SPV=1` is an **escape hatch only** for testnet ChainLock-cycle +outages (rust-dashcore #470) — it is NOT the operating mode. Any documentation or config that +implies SPV-off is the default is incorrect. #### CR-001 — SPV mn-list sync readiness -- **Priority**: P1 (post-Task #15) -- **Status**: BLOCKED — needs harness refactor: SPV runtime re-enablement (Task #15). The harness currently runs with `spv_runtime: None` and a `TrustedHttpContextProvider` (see `harness.rs:148`). +- **Priority**: P1 +- **Status**: PASS-pending-validation — Task #15 complete; SPV enabled in the harness (`SpvContextProvider` wired; `harness.rs:200-218` block active). Test body to be written; contract is specified below. - **Wallet feature exercised**: `manager::accessors::spv()` returning a started `SpvRuntime`; mn-list sync internals. - **DET parallel**: `dash-evo-tool/tests/backend-e2e/spv_wallet.rs:14` (`test_spv_sync_and_create_wallet`). -- **Preconditions**: SPV enabled in `harness::E2eContext::build` (uncomment block at `harness.rs:200-218`). +- **Preconditions**: SPV enabled in `harness::E2eContext::build` (block at `harness.rs:200-218` is active). - **Scenario**: 1. Wait `<= 180s` for `spv::wait_for_mn_list_synced` to return. 2. Read mn-list height. - **Assertions**: mn-list height > 0; SPV runtime reports `Ready` state. - **Negative variants**: zero peers reachable → harness fails fast with explicit error (not a silent infinite wait). -- **Harness extensions required**: re-enable `SpvContextProvider` swap; add a `SpvHealth::status() -> Enum` accessor to the manager. +- **Harness extensions required**: `SpvContextProvider` swap is done; `SpvHealth::status() -> Enum` accessor is available. - **Estimated complexity**: M - **Rationale**: Foundation for every other Core test — guarantees the SPV layer is alive before any Core operation runs. #### CR-002 — Core wallet receive address derivation -- **Priority**: P1 (post-Task #15) -- **Status**: BLOCKED — needs harness refactor: SPV runtime re-enablement (Task #15). +- **Priority**: P1 +- **Status**: PASS-pending-validation — Task #15 complete; SPV-backed harness ready. Test body to be written. - **Wallet feature exercised**: `wallet/core/wallet.rs:59` (`next_receive_address_for_account`). - **DET parallel**: `dash-evo-tool/tests/backend-e2e/core_tasks.rs:14` (`test_tc001_refresh_wallet_info_core_only`). - **Preconditions**: CR-001 ready. - **Scenario**: derive 5 receive addresses on account `0`; assert distinctness; assert `network() == bank.network()`. - **Assertions**: 5 distinct `Address`es; consistent network prefix. - **Negative variants**: derive on non-existent account → typed error. -- **Harness extensions required**: SPV-backed `TestCoreWallet` helper. +- **Harness extensions required**: `TestCoreWallet` helper (SPV runtime is now available). - **Estimated complexity**: M - **Rationale**: Catches Core-account derivation regressions independently of broadcast/sync. #### CR-003 — Asset-lock-funded identity registration (full path) + - **Priority**: P2 (post-Task #15) -- **Status**: Pass — `tests/e2e/cases/cr_003_asset_lock_funded_registration.rs` (`#[ignore]`-tagged; harness init blocks on the **default-on** `PLATFORM_WALLET_E2E_BANK_CORE_GATE`). Builds the asset-lock tx via `setup_with_core_funded_test_wallet(TEST_WALLET_CORE_FUNDING)`, waits for the IS-lock, registers the identity, and pins on-chain identity existence + `tracked_asset_locks` recording + Core-balance decrement (lock amount + fee, in duffs). End-to-end runs require the bank's Core (Layer-1) primary receive address to hold at least `TEST_WALLET_CORE_FUNDING + CORE_TX_FEE_RESERVE` (≈ 200_010_000 duffs ≈ 2.0001 DASH testnet); under-funded surfaces as `FrameworkError::Bank` with the bank's Core address embedded so the operator-actionable "top up at <addr>" message reaches the test log unchanged. The bank Core address is logged once per process at framework init under the `platform_wallet::e2e::bank` target. +- **Status**: Pass — `tests/e2e/cases/cr_003_asset_lock_funded_registration.rs` (`#[ignore]`-tagged; harness init blocks on the **default-on** `PLATFORM_WALLET_E2E_BANK_CORE_GATE`). Builds the asset-lock tx via `setup_with_core_funded_test_wallet(TEST_WALLET_CORE_FUNDING)`, waits for the IS-lock, registers the identity, and pins on-chain identity existence + `tracked_asset_locks` recording + Core-balance decrement (lock amount + fee, in duffs). End-to-end runs require the bank's Core (Layer-1) primary receive address to hold at least `TEST_WALLET_CORE_FUNDING + CORE_TX_FEE_RESERVE` (≈ 200_010_000 duffs ≈ 2.0001 DASH testnet); under-funded surfaces as `FrameworkError::Bank` with the bank's Core address embedded so the operator-actionable "top up at <addr>" message reaches the test log unchanged. The bank Core address is logged once per process at framework init under the `platform_wallet::e2e::bank` target. Core-sweep teardown is best-effort: any teardown sweep failure is logged and skipped rather than failing the test. - **Wallet feature exercised**: `wallet/asset_lock/build.rs:39` (`build_asset_lock_transaction`) + `wallet/asset_lock/build.rs:285` (`create_funded_asset_lock_proof`) + `wallet/identity/network/registration.rs:59` (`register_identity_with_funding_external_signer` driving `IdentityFundingMethod::FundWithWallet`). - **DET parallel**: `dash-evo-tool/tests/backend-e2e/core_tasks.rs:132` (`test_tc004_create_registration_asset_lock`). - **Preconditions**: CR-001 + a Core-funded test wallet (operator funds via testnet faucet). @@ -1396,6 +1415,35 @@ so that when SPV lands, the test bodies can be written without further design. - **Rationale**: Mirrors DET's existing canonical Identity-create coverage. Lower priority than ID-001 because address-funded is the path with no other coverage in the workspace. - **Operator notes**: First cold-cache run takes ~15 minutes because SPV walks compact filters from genesis (~1.47M testnet blocks). Subsequent runs reuse the on-disk cache and complete in seconds. The harness gates init on `PLATFORM_WALLET_E2E_BANK_CORE_GATE` — **default-on with a 900s deadline**, waiting for the bank's confirmed Core balance to become non-zero so CR-003 doesn't race a cold-cache scan and see `core_balance_confirmed=0` mid-scan. Set the var to `0` (or `disabled` / `false` / `off`) to opt out for Platform-only suites; set a positive integer to override the timeout in seconds. Set `RUST_LOG=info,platform_wallet::e2e::wait=info` to see scan-progress lines (`scan_height` vs `scan_tip`) every 30s. +#### CR-004 — Legacy BIP32 account: balance + UTXO state updates after spend + +- **Priority**: P1 — open bug from upstream consumer +- **Status**: ENV-GATED FAILING-by-design — runs only when `PLATFORM_WALLET_E2E_RUN_FAILING_BY_DESIGN=1` is set. Without that env var the test is skipped with an informative log message. The production bug (stale UTXO set after spend) is open; this test pins the contract so the fix becomes verifiable. PR #3609 carries both the test and the production fix together. +- **Wallet feature exercised**: `wallet/core/wallet.rs:54` (`CoreWallet::balance`); `wallet/core/broadcast.rs:185` (`check_core_transaction` post-broadcast state mutation on `standard_bip32_accounts`). +- **Bug repro (upstream)**: [dashpay/dash-evo-tool#845](https://github.com/dashpay/dash-evo-tool/issues/845) — sending all funds from a legacy BIP32 account (`StandardAccountType::BIP32Account`) leaves the wallet's local UTXO set stale; a follow-up `send_to_addresses` call fails with `TransactionBuild("Coin selection error: No UTXOs available for selection")` despite the original UTXOs being long since spent on-chain. +- **DET parallel**: none yet — DET is the affected consumer; this test pins the contract on the rs-platform-wallet side so a fix becomes verifiable from a single repository. +- **Preconditions**: CR-001 + a Core-funded BIP32 legacy account (derivation path `m/44'/1'/0'`, `StandardAccountType::BIP32Account` at index `0`, stored under `wallet.accounts.standard_bip32_accounts`). +- **Scenario**: + 1. Create a wallet whose primary accounts include a **legacy BIP32 account** (`StandardAccountType::BIP32Account`). Fund it with at least 2 distinct UTXOs from the bank's Core funding helper so coin selection has more than one input to consider. + 2. Sync until `core_balance_confirmed > 0` for the legacy account. + 3. Build a "send all" Core transfer via `CoreWallet::send_to_addresses(StandardAccountType::BIP32Account, 0, outputs)` using the **advanced (explicit input selection)** path that consumes every UTXO on the legacy account; broadcast and wait for instant-lock or confirmation. + 4. Read the wallet's balance for the legacy account immediately after broadcast completes (re-use `wait_for_core_balance` from CR-003 with target `== 0`). + 5. Issue a second small transfer on the same legacy account via `send_to_addresses`. +- **Assertions**: + - After step 3 + sync, the legacy account's confirmed balance equals `0` (or fee-only residue if the helper deducts the fee from outputs rather than inputs). + - `standard_bip32_accounts[0].spendable_utxos(current_height)` returns an empty set — no entry that is confirmed and unspent. + - The second `send_to_addresses` at step 5 fails with `PlatformWalletError::TransactionBuild` whose message identifies no spendable inputs, NOT with a stale-UTXO selection on already-spent outputs. +- **Negative variants**: + - Mid-spend reorg of the broadcast (P2 — manual / mocked). + - Send-all on a legacy account that is itself sourced from a watch-only descriptor (P2 — separate ticket if it diverges from the keyed path). +- **Harness extensions required**: + - `setup_with_legacy_bip32_funded_account(funding_duffs, utxo_count)` helper analogous to the existing `setup_with_core_funded_test_wallet`, but using `StandardAccountType::BIP32Account` at index `0` (path `m/44'/1'/0'`). + - `assert_no_unspent_utxos(account)` reusable assertion (or open-coded inline for now). + - `wait_for_core_balance` already exists from CR-003 — re-use with `target == 0`. +- **Estimated complexity**: M +- **Rationale**: Pins the spend → state-update contract of the Core wallet for the legacy BIP32 account path. Without it, any future regression in `check_core_transaction`'s handling of `standard_bip32_accounts` (which dash-evo-tool, the SwiftExampleApp, and Rust-SDK-driven UIs all depend on) ships silently to consumers and is caught only when downstream consumers file issues. The bug is currently open upstream, so the test fails at first run — exactly the "pin invariants, including currently-broken ones" pattern used throughout this spec. +- **Operator notes**: Same SPV cold-cache caveat as CR-003 (~15 min on first run). The `PLATFORM_WALLET_E2E_BANK_CORE_GATE` default-on still applies. The legacy BIP32 account derivation must NOT cross-contaminate the wallet's default Core account UTXO set — assertions read `standard_bip32_accounts` slot state directly, not the wallet-aggregate balance. + ### Contracts (CT) #### CT-001 — Document put: deploy a fixture data contract @@ -1689,6 +1737,7 @@ sane place to pin the harness contract is alongside the wallet contract. #### Harness-ID-1 — `sweep_identities` regression: registered identities surrender credits at teardown - **Priority**: P0 +- **Status**: IMPLEMENTED — passing (parallel-safe). The `bank_gain <= pre_sweep_balance` upper-bound assertion is dropped — under parallel execution, sibling test sweeps flow into the bank concurrently, making the upper bound non-deterministic. The binding assertion is the lower-bound recovery check combined with the "no registry entry after teardown" guarantee. - **Wallet feature exercised**: `tests/e2e/framework/cleanup.rs::sweep_identities` (was a no-op stub on `feat/rs-platform-wallet-e2e-cases`; implementation lands on the identity-tests-and-sweep branch). - **DET parallel**: none. - **Preconditions**: ID-001 helper available; bank identity configured for the sweep destination (per `bank_identity` env-var contract). @@ -2123,7 +2172,7 @@ becomes a test failure rather than a silent drift. - **(a) Spec realignment**: TEST_SPEC.md PA-001b is rewritten to match the implicit-change semantics above, OR removed with a deletion-note. The Found-020 entry itself can then be removed alongside. - **(b) Production extension**: `PlatformAddressWallet::transfer` gains an `output_change_address: Option` parameter wired through the auto-select path so PA-001b's two-branch behaviour becomes implementable. - **Expected** (after resolution): the spec and the production API agree. Either the spec describes what the wallet does, or the wallet does what the spec describes. -- **Actual** (current state): PA-001b stays `#[ignore]`'d as `BLOCKED — feature missing in production`; the spec entry is preserved with a `**Status**:` flag so a human reviewer sees the drift at a glance, rather than discovering it by reading the test. +- **Actual** (post-PR-#3609 state): resolved via option (a) — PA-001b is rewritten to match implicit-change semantics (see PA-001b Status). The `output_change_address` parameter drift is closed; Found-020 is retained for historical traceability only. - **Harness extensions required**: none — the test will be straightforward `transfer(...)` + balance assertions once the production parameter exists. - **Estimated complexity**: S (when unblocked). - **Rationale**: The spec is one of the harness's load-bearing documents — test authors trust it as a description of the production API. A spec entry that describes a non-existent parameter erodes that trust. Filing the drift as Found-020 (and surfacing it via the PA-001b status field) makes the gap visible without forcing an immediate spec rewrite — the resolution can wait for a coordinated PA-001b implementation pass. @@ -2156,11 +2205,12 @@ order. Each wave unlocks the cases listed. - Original plan: `Config::token_contract_id`, `Config::token_position`, optional `Config::token_claim_amount`; operator pre-funds tokens to a bank-derived identity (one-time, README'd next to bank pre-funding). - Superseded: the wallet already accepts `tokens_schema_json` on `create_data_contract_with_signer` (`wallet/identity/network/contract.rs:124`), so the suite can deploy a fresh token contract per CI run instead of relying on operator pre-funding. See Wave G below. -### Wave E — SPV re-enablement (Task #15) -- Uncomment SPV block in `harness.rs:200-218`; swap `TrustedHttpContextProvider` → `SpvContextProvider`. -- Add `SpvHealth::status()` accessor to manager. -- Add Core-funded test wallet helper (faucet integration). -- **Unlocks**: CR-001, CR-002, CR-003. +### Wave E — SPV re-enablement (Task #15) — COMPLETE +- SPV block in `harness.rs:200-218` is active; `SpvContextProvider` is wired (replaces `TrustedHttpContextProvider`). +- `SpvHealth::status()` accessor is available in the manager. +- Core-funded test wallet helper (faucet integration) is ready. +- **Unlocked**: CR-001, CR-002, CR-003 (all PASS-pending-validation or PASS). +- **Note**: `PLATFORM_WALLET_E2E_DISABLE_SPV=1` is an operator escape hatch for ChainLock-cycle outages (rust-dashcore #470). It is NOT the default. SPV-on has been the operating mode since v17. ### Wave G — Token harness extensions - Replaces Wave D. The wallet's `create_data_contract_with_signer` already accepts a `tokens_schema_json` argument; Wave G assembles the V1 token-config JSON from a structured `TokenContractOpts` struct so test bodies stay terse and the schema-drift surface lives in exactly one place. @@ -2203,7 +2253,14 @@ order. Each wave unlocks the cases listed. - **Unlocks**: PA-002 (negative), PA-002b, PA-004 (full assertions), PA-004b, PA-004c, PA-006, PA-006b, PA-008c, PA-009, PA-010, PA-011, PA-012, PA-013, Harness-G1a, Harness-G1b, Harness-G4. - **Cost**: ~200-400 LoC across multiple commits; the test-DAPI-proxy and cancellation-hook items are non-trivial and can land late. -**Recommended build order**: Wave A first (highest leverage — unblocks 25+ cases), then Wave F's cheap helpers (estimate-fee, transfer-with-inputs, registry status, FUNDING_MUTEX hook) which unblock most P2 PA cases, then Wave C, then Wave B as ID-003/DP-002 land. Wave G unlocks the entire TK column once Wave A is in place; the SDK-wrapper helpers in Wave G (helpers 6–10 and 14–19, previously tracked as Gap-T1..T6) land together with Wave G, not as follow-up wallet PRs. Wave F's expensive items (test DAPI proxy, cancellation hook) and Wave E are independent and can run in parallel with the others once a champion is assigned. Wave D is superseded by Wave G. + +**Recommended build order**: Wave A first (highest leverage — unblocks 25+ cases), then Wave F's cheap helpers (estimate-fee, transfer-with-inputs, registry status, FUNDING_MUTEX hook) which unblock most P2 PA cases, then Wave C, then Wave B as ID-003/DP-002 land. Wave G unlocks the entire TK column once Wave A is in place; the SDK-wrapper helpers in Wave G (helpers 6–10 and 14–19, previously tracked as Gap-T1..T6) land together with Wave G, not as follow-up wallet PRs. Wave F's expensive items (test DAPI proxy, cancellation hook) and Wave E are independent and can run in parallel with the others once a champion is assigned. Wave D is superseded by Wave G. Wave E is complete (Task #15 closed; CR-003 has flipped PASS, see §3 CR-003 Status). + +### Framework notes (post-V20) + +**`bank.fund_address` — chain-confirmed-nonce wait (PR #3609 / upstream issue #3611)** + +`bank.fund_address` now waits for the chain-confirmed nonce to advance before releasing `FUNDING_MUTEX`. This prevents a race where DAPI replica round-robin lag causes the next `fund_address` call to arrive at a replica that hasn't yet indexed the previous funding transaction, producing a stale-nonce rejection. The wait is bounded; if the nonce does not advance within the timeout, the call fails with a typed `BankNonceTimeout` error. Tests relying on serial funding order (PA-008, PA-008b, PA-008c) benefit from this without any test-side changes. ### Wallet-API gap notes (follow-up issues) @@ -2225,7 +2282,8 @@ Explicit list of what this suite WILL NOT cover, with reasons. Each entry prevents future scope creep arguments. 1. **Shielded transfers** — entire `wallet/shielded/` surface. Reason: prover, viewing-key derivation, and note-selection are a parallel system; coverage belongs in a dedicated suite. Re-evaluate when shielded ships to mainnet. -2. **Credit withdrawals** (`wallet/identity/network/withdrawal.rs`, `wallet/platform_addresses/withdrawal.rs`) — withdrawal verification requires Layer-1 observation of the withdrawal tx. Blocked on Task #15 (SPV stabilisation). Defer. + +2. **Credit withdrawals** (`wallet/identity/network/withdrawal.rs`, `wallet/platform_addresses/withdrawal.rs`) — withdrawal verification requires Layer-1 observation of the withdrawal tx. SPV is now enabled (Task #15 complete) but withdrawal coverage is deferred pending a dedicated test design — the flow is more complex than a simple SPV read and DET currently owns the canonical coverage. 3. **Operator-pre-funded testnet token contracts** — the original Wave D plan (env-config + operator-provided contract id) is superseded. The suite deploys a fresh token contract per CI run via Wave G; no operator-side registry is required and no testnet contract id is consumed from config. 4. **Asset-lock-funded identity registration** — the bank holds Platform credits, not Core UTXOs. The address-funded variant (ID-001) covers this need from the wallet's perspective; full asset-lock coverage stays with DET (`dash-evo-tool/tests/backend-e2e/identity_create.rs`). 5. **DAPI Core path** (`tx_is_ours`, mn-list diffs, peer behaviour) — DET territory; this suite tests the wallet against DAPI, not DAPI itself. @@ -2244,7 +2302,7 @@ Each question's answer changes the spec; numbered for reference. 1. **Token contract registry** — superseded: Wave G deploys a fresh token contract per CI run via the wallet's `create_data_contract_with_signer` (`tokens_schema_json` argument). No operator-side registry is required. Retained here for historical context. 2. **Contested-name coverage** — should CN-001 be promoted to P1, or do we accept DET parity and leave it P2/deferred? 3. **Long-running tests** — PA-005 (16 funding round-trips, ~3 min) is borderline. Do we accept multi-minute tests in the default `cargo test --test e2e` run, or gate them behind a `slow-tests` cargo feature? -4. **Identity withdrawal coverage** — once SPV (Task #15) lands, do we want withdrawal coverage here, or is that DET's exclusive territory? +4. **Identity withdrawal coverage** — SPV (Task #15) is now live. The question remains: do we add withdrawal coverage here, or defer to DET's exclusive territory? 5. **Mainnet smoke** — should the suite ever support a single, opt-in mainnet smoke case (e.g. PA-001 with a tiny `1_000`-credit transfer) for release-gate validation? 6. **Fee-bound numbers** — PA-003 asserts `fee_5 - fee_1 < 1_000_000`. Should we baseline empirical fee numbers and tighten these bounds in a follow-up, or keep them loose and rely on protocol-version bumps to reset them? 7. **Deterministic fixture network** — testnet is shared and noisy. Is there appetite to maintain a regtest-with-Drive cluster for CI exclusively, or do we accept testnet flakiness as the operating constraint?