From 5d2779d75b48787313f695da84e2cf1c02e2d5e4 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 7 May 2026 15:10:02 +0200 Subject: [PATCH 1/3] =?UTF-8?q?test(platform-wallet):=20CR-001=20=E2=80=94?= =?UTF-8?q?=20SPV=20mn-list=20sync=20readiness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins the SPV-readiness contract per TEST_SPEC: wait <= 180s for mn-list sync, assert height > 0 and SPV runtime is started. Note: spec mentions SpvHealth::status() -> Enum but that accessor does not exist in the codebase yet; using SpvRuntime::is_started() as the available proxy with an inline comment. Co-Authored-By: Claude Sonnet 4.6 --- .../cr_001_spv_mn_list_sync_readiness.rs | 112 ++++++++++++++++++ .../rs-platform-wallet/tests/e2e/cases/mod.rs | 1 + 2 files changed, 113 insertions(+) create mode 100644 packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs diff --git a/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs b/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs new file mode 100644 index 0000000000..f0ed18dd50 --- /dev/null +++ b/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs @@ -0,0 +1,112 @@ +//! CR-001 — SPV mn-list sync readiness. +//! +//! Spec: `tests/e2e/TEST_SPEC.md` (### Core (CR) → CR-001). +//! +//! Pins the SPV-readiness contract: the mn-list manager reaches +//! `SyncState::Synced` within 180 s, the synced height is > 0, and the +//! SPV runtime is in a started (running) state on return. +//! +//! The harness already calls `wait_for_mn_list_synced` during +//! `E2eContext::build`; this test re-asserts the same contract from the +//! test-body perspective to keep the pin explicit and independently +//! verifiable. The call returns immediately when the harness already +//! cleared the gate. +//! +//! Mirrors DET's `test_spv_sync_and_create_wallet` at +//! `dash-evo-tool/tests/backend-e2e/spv_wallet.rs:14`. + +use std::time::Duration; + +use crate::framework::prelude::*; +use crate::framework::spv::wait_for_mn_list_synced; + +/// Maximum time this test body will wait for mn-list sync. The +/// harness gate already ran at init — this is an independent ceiling +/// that fires only if the sync regresses between init and the test body +/// (extremely unlikely, but the spec pins <= 180 s explicitly). +const MN_LIST_SYNC_TIMEOUT: Duration = Duration::from_secs(180); + +#[ignore = "CR-001 — needs testnet + SPV runtime. \ + Set PLATFORM_WALLET_E2E_DISABLE_SPV=0 (or unset) and supply \ + DAPI endpoints via PLATFORM_WALLET_E2E_DAPI_ADDRESSES. \ + Mirrors DET's test_spv_sync_and_create_wallet."] +#[tokio_shared_rt::test(shared, flavor = "multi_thread", worker_threads = 12)] +async fn cr_001_spv_mn_list_sync_readiness() { + let _ = tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "info,platform_wallet=debug".into()), + ) + .with_test_writer() + .try_init(); + + // Respect the operator escape hatch — when SPV is disabled the mn-list + // will never sync; skip with an informative message rather than burn + // the full timeout. + let ctx = E2eContext::init().await.expect("E2eContext::init failed"); + + if ctx.config.disable_spv { + tracing::info!( + target: "platform_wallet::e2e::cases::cr_001", + "PLATFORM_WALLET_E2E_DISABLE_SPV is set — skipping CR-001 \ + (mn-list will never sync without a live SPV runtime)" + ); + return; + } + + let s = crate::framework::setup().await.expect("setup failed"); + + // Step 1: assert the SPV runtime is live. The harness only populates + // `ctx.spv()` when `disable_spv` is false, so `None` here is a + // harness bug worth surfacing with a clear message. + let spv = s.ctx.spv().expect( + "PRE-pin violated: ctx.spv() is None but PLATFORM_WALLET_E2E_DISABLE_SPV \ + is not set — SPV runtime was not started by the harness", + ); + + // Step 2: wait <= 180s for mn-list sync. The harness already ran this + // during init; this call returns immediately if already synced. + wait_for_mn_list_synced(spv, MN_LIST_SYNC_TIMEOUT) + .await + .expect("wait_for_mn_list_synced failed within 180 s"); + + // Step 3: read the mn-list height from the live sync progress. + let progress = spv.sync_progress().await.expect( + "PRE-pin violated: sync_progress() returned None after \ + wait_for_mn_list_synced succeeded — SPV client must be running", + ); + let mn = progress + .masternodes() + .expect("SyncProgress::masternodes() failed after successful mn-list sync"); + let mn_height = mn.current_height(); + + tracing::info!( + target: "platform_wallet::e2e::cases::cr_001", + mn_height, + state = ?mn.state(), + "CR-001: mn-list synced" + ); + + // Assertion 1: mn-list height > 0 (proves the client synced real data, + // not just initialised with a zero-height placeholder). + assert!( + mn_height > 0, + "POST-pin violated: mn-list height is 0 after sync — \ + the mn-list manager must advance at least one block to report Synced. \ + Check SPV peer connectivity and mn-list initial-sync logic." + ); + + // Assertion 2: SPV runtime is started (running). `is_started()` returns + // `true` when the internal DashSpvClient is initialised and the sync + // loop is live. This is the available proxy for the spec's "Ready state" + // contract (SpvHealth is not yet a public type — see TEST_SPEC.md CR-001 + // harness-extensions note). + assert!( + spv.is_started(), + "POST-pin violated: SpvRuntime::is_started() is false after \ + wait_for_mn_list_synced returned Ok — the runtime must remain \ + started (running) throughout the test session." + ); + + s.teardown().await.expect("teardown"); +} diff --git a/packages/rs-platform-wallet/tests/e2e/cases/mod.rs b/packages/rs-platform-wallet/tests/e2e/cases/mod.rs index e316c2a97e..0b9e09ea83 100644 --- a/packages/rs-platform-wallet/tests/e2e/cases/mod.rs +++ b/packages/rs-platform-wallet/tests/e2e/cases/mod.rs @@ -6,6 +6,7 @@ //! TEST_SPEC.md priorities (P1, P2, ID-, DP-, DPNS-, TK-, …) follow //! in subsequent PRs. +pub mod cr_001_spv_mn_list_sync_readiness; pub mod cr_003_asset_lock_funded_registration; pub mod dpns_001_register_name; pub mod id_001_register_identity_from_addresses; From f94c26b263b95385cc5152c5c3c6d9c429e1ea48 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Thu, 7 May 2026 21:27:59 +0200 Subject: [PATCH 2/3] fix(platform-wallet/e2e): CR-001 reads PLATFORM_WALLET_E2E_DISABLE_SPV directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Config::disable_spv field lives on fix/...e2e-qa-fixes-v1 (commit d1d81a3294) — not on this branch. Reading the env var inline keeps PR #3614 atomic and self-compiling on its base. Co-Authored-By: Claude Sonnet 4.6 --- .../e2e/cases/cr_001_spv_mn_list_sync_readiness.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs b/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs index f0ed18dd50..fd65d4119a 100644 --- a/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs +++ b/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs @@ -43,9 +43,17 @@ async fn cr_001_spv_mn_list_sync_readiness() { // Respect the operator escape hatch — when SPV is disabled the mn-list // will never sync; skip with an informative message rather than burn // the full timeout. - let ctx = E2eContext::init().await.expect("E2eContext::init failed"); - - if ctx.config.disable_spv { + let disable_spv = std::env::var("PLATFORM_WALLET_E2E_DISABLE_SPV") + .ok() + .map(|v| { + let t = v.trim().to_string(); + t == "1" + || t.eq_ignore_ascii_case("true") + || t.eq_ignore_ascii_case("yes") + || t.eq_ignore_ascii_case("on") + }) + .unwrap_or(false); + if disable_spv { tracing::info!( target: "platform_wallet::e2e::cases::cr_001", "PLATFORM_WALLET_E2E_DISABLE_SPV is set — skipping CR-001 \ From d0107b559617e314fac8731c6314f658619c1c07 Mon Sep 17 00:00:00 2001 From: Lukasz Klimek <842586+lklimek@users.noreply.github.com> Date: Fri, 8 May 2026 15:05:52 +0200 Subject: [PATCH 3/3] docs(rs-platform-wallet): correct CR-001 doc claims + dedupe env parser (#3614 review) Address audit findings on PR #3614 (CR-001 SPV mn-list readiness): - F-001 (LOW): the harness never reads PLATFORM_WALLET_E2E_DISABLE_SPV; E2eContext::build always starts SPV and stores it in ctx.spv(). Reword the PRE-pin comment to drop the misleading harness-gate claim and frame the .expect as defence-in-depth against future harness changes. - F-002 (LOW): MN_LIST_SYNC_TIMEOUT (180 s) is not an enforced ceiling. framework::spv::wait_for_mn_list_synced raises every requested timeout to COLD_CACHE_TIMEOUT_FLOOR (600 s). Update the constant doc and module header to describe the actual effective wait, with the spec's 180 s framed as informational warm-cache target. - F-003 (LOW): promote the 11-line truthy-env parser out of the test body into framework::config so future SPV-gated cases reuse one source of truth. Adds vars::DISABLE_SPV constant, a private is_truthy_env helper shared by SPV-style boolean flags, and a public spv_disabled_from_env() consumed by CR-001. Includes a unit test exercising the truthy/falsy matrix on a unique probe-only env-var key. No production-code changes; SPV is still always started by the harness. The .expect strings keep their PRE/POST-pin invariant framing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cr_001_spv_mn_list_sync_readiness.rs | 56 +++++++-------- .../tests/e2e/framework/config.rs | 70 +++++++++++++++++++ 2 files changed, 98 insertions(+), 28 deletions(-) diff --git a/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs b/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs index fd65d4119a..3c01cbd7d1 100644 --- a/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs +++ b/packages/rs-platform-wallet/tests/e2e/cases/cr_001_spv_mn_list_sync_readiness.rs @@ -3,8 +3,11 @@ //! Spec: `tests/e2e/TEST_SPEC.md` (### Core (CR) → CR-001). //! //! Pins the SPV-readiness contract: the mn-list manager reaches -//! `SyncState::Synced` within 180 s, the synced height is > 0, and the -//! SPV runtime is in a started (running) state on return. +//! `SyncState::Synced`, the synced height is > 0, and the SPV runtime +//! is in a started (running) state on return. The spec's informational +//! warm-cache target is 180 s; the helper internally raises every +//! request to its `COLD_CACHE_TIMEOUT_FLOOR` (600 s) so the actual +//! wait can be longer on a cold testnet cache. //! //! The harness already calls `wait_for_mn_list_synced` during //! `E2eContext::build`; this test re-asserts the same contract from the @@ -17,13 +20,16 @@ use std::time::Duration; +use crate::framework::config::{spv_disabled_from_env, vars}; use crate::framework::prelude::*; use crate::framework::spv::wait_for_mn_list_synced; -/// Maximum time this test body will wait for mn-list sync. The -/// harness gate already ran at init — this is an independent ceiling -/// that fires only if the sync regresses between init and the test body -/// (extremely unlikely, but the spec pins <= 180 s explicitly). +/// Spec's informational warm-cache target for mn-list sync. NOT a hard +/// ceiling: `wait_for_mn_list_synced` raises every request to its +/// internal `COLD_CACHE_TIMEOUT_FLOOR` (600 s — see +/// `framework::spv::wait_for_mn_list_synced`) and emits an `info!` +/// when it does. The real wait is bounded by the floor, not by this +/// constant. const MN_LIST_SYNC_TIMEOUT: Duration = Duration::from_secs(180); #[ignore = "CR-001 — needs testnet + SPV runtime. \ @@ -43,20 +49,11 @@ async fn cr_001_spv_mn_list_sync_readiness() { // Respect the operator escape hatch — when SPV is disabled the mn-list // will never sync; skip with an informative message rather than burn // the full timeout. - let disable_spv = std::env::var("PLATFORM_WALLET_E2E_DISABLE_SPV") - .ok() - .map(|v| { - let t = v.trim().to_string(); - t == "1" - || t.eq_ignore_ascii_case("true") - || t.eq_ignore_ascii_case("yes") - || t.eq_ignore_ascii_case("on") - }) - .unwrap_or(false); - if disable_spv { + if spv_disabled_from_env() { tracing::info!( target: "platform_wallet::e2e::cases::cr_001", - "PLATFORM_WALLET_E2E_DISABLE_SPV is set — skipping CR-001 \ + var = vars::DISABLE_SPV, + "SPV disabled via env — skipping CR-001 \ (mn-list will never sync without a live SPV runtime)" ); return; @@ -64,19 +61,22 @@ async fn cr_001_spv_mn_list_sync_readiness() { let s = crate::framework::setup().await.expect("setup failed"); - // Step 1: assert the SPV runtime is live. The harness only populates - // `ctx.spv()` when `disable_spv` is false, so `None` here is a - // harness bug worth surfacing with a clear message. - let spv = s.ctx.spv().expect( - "PRE-pin violated: ctx.spv() is None but PLATFORM_WALLET_E2E_DISABLE_SPV \ - is not set — SPV runtime was not started by the harness", - ); + // Step 1: bind the SPV runtime. `E2eContext::build` always starts + // SPV unconditionally, so `ctx.spv()` is `Some` in every supported + // configuration; the `expect` is defence-in-depth in case a future + // harness change makes the runtime optional. + let spv = s + .ctx + .spv() + .expect("PRE-pin violated: ctx.spv() is None — harness must always start SPV"); - // Step 2: wait <= 180s for mn-list sync. The harness already ran this - // during init; this call returns immediately if already synced. + // Step 2: re-pin mn-list sync from the test body. The harness + // already ran this at init, so the call returns immediately when + // already synced; on a cold cache the helper waits up to its + // `COLD_CACHE_TIMEOUT_FLOOR` (600 s). wait_for_mn_list_synced(spv, MN_LIST_SYNC_TIMEOUT) .await - .expect("wait_for_mn_list_synced failed within 180 s"); + .expect("wait_for_mn_list_synced failed before the cold-cache floor elapsed"); // Step 3: read the mn-list height from the live sync progress. let progress = spv.sync_progress().await.expect( diff --git a/packages/rs-platform-wallet/tests/e2e/framework/config.rs b/packages/rs-platform-wallet/tests/e2e/framework/config.rs index 903e789f9f..8d96205e08 100644 --- a/packages/rs-platform-wallet/tests/e2e/framework/config.rs +++ b/packages/rs-platform-wallet/tests/e2e/framework/config.rs @@ -52,6 +52,14 @@ pub mod vars { /// that don't need Core duffs; any positive integer overrides the /// timeout (in seconds). pub const BANK_CORE_GATE: &str = "PLATFORM_WALLET_E2E_BANK_CORE_GATE"; + /// Operator escape hatch for SPV-gated cases (CR-001, anything + /// asserting on `SpvRuntime` post-conditions). When truthy + /// (`1` / `true` / `yes` / `on`, case-insensitive), the case body + /// skips with an informative log. The harness itself does NOT + /// read this flag — `E2eContext::build` always starts SPV; the + /// gate is consumed test-side via [`super::spv_disabled_from_env`]. + /// See `TEST_SPEC.md` CR-001 for the SPEC-level reference. + pub const DISABLE_SPV: &str = "PLATFORM_WALLET_E2E_DISABLE_SPV"; } /// Default deadline for the bank Core funding gate when the env var is @@ -359,6 +367,34 @@ pub(crate) fn parse_bank_core_gate(raw: Option<&str>) -> (Option, Bank } } +/// Returns `true` when [`vars::DISABLE_SPV`] is set to a truthy value +/// (`1` / `true` / `yes` / `on`, case-insensitive, surrounding +/// whitespace ignored). Any other value — including unset, empty, or +/// unrecognised — returns `false`. +/// +/// SPV-gated cases (e.g. CR-001) call this at the top of the test body +/// and `return` early when it reports `true`, so the operator can opt +/// out of SPV-only assertions without burning the cold-cache timeout. +/// The harness itself never reads the flag: `E2eContext::build` always +/// starts SPV. +pub fn spv_disabled_from_env() -> bool { + is_truthy_env(vars::DISABLE_SPV) +} + +/// Truthy-env helper shared by SPV-style boolean flags. Reads `key` +/// from the process environment and returns `true` for `1` / `true` / +/// `yes` / `on` (case-insensitive, trimmed); everything else — unset, +/// empty, or unrecognised — returns `false`. +fn is_truthy_env(key: &str) -> bool { + matches!( + std::env::var(key).ok().as_deref().map(str::trim), + Some(v) if v == "1" + || v.eq_ignore_ascii_case("true") + || v.eq_ignore_ascii_case("yes") + || v.eq_ignore_ascii_case("on") + ) +} + /// Parse a network string supporting the canonical dashcore names /// plus the test-harness `local` alias for regtest and an empty /// shorthand for testnet. Used only at [`Config`] construction; @@ -434,4 +470,38 @@ mod tests { assert_eq!(timeout, Some(DEFAULT_BANK_CORE_GATE_TIMEOUT)); assert_eq!(src, BankCoreGateSource::EnvInvalidFallback); } + + /// Process-wide env-var flag used to exercise [`is_truthy_env`]. + /// Distinct from any production var so cargo-test parallelism with + /// the `from_env` callers can never collide. The truthy/falsy + /// matrix is exercised in a single test so the two halves don't + /// race over the same key under parallel cargo-test execution. + const TRUTHY_PROBE_VAR: &str = "PLATFORM_WALLET_E2E_TEST_TRUTHY_PROBE"; + + #[test] + fn is_truthy_env_matrix() { + // SAFETY: single-threaded — the probe key is unique to this + // test, so no parallel test can mutate it underneath us. + std::env::remove_var(TRUTHY_PROBE_VAR); + assert!(!is_truthy_env(TRUTHY_PROBE_VAR), "unset must be falsy"); + + for raw in [ + "1", "true", "TRUE", "True", "yes", "Yes", "YES", "on", "ON", " on ", " 1\t", + ] { + std::env::set_var(TRUTHY_PROBE_VAR, raw); + assert!( + is_truthy_env(TRUTHY_PROBE_VAR), + "{raw:?} should be recognised as truthy" + ); + } + + for raw in ["", " ", "0", "false", "no", "off", "disabled", "abc"] { + std::env::set_var(TRUTHY_PROBE_VAR, raw); + assert!( + !is_truthy_env(TRUTHY_PROBE_VAR), + "{raw:?} must NOT be recognised as truthy" + ); + } + std::env::remove_var(TRUTHY_PROBE_VAR); + } }