Skip to content

test(platform-wallet): shielded (Orchard) e2e suite — spec + Wave H harness#3727

Merged
lklimek merged 33 commits into
feat/rs-platform-wallet-e2efrom
test/rs-platform-wallet-shielded-e2e
Jun 10, 2026
Merged

test(platform-wallet): shielded (Orchard) e2e suite — spec + Wave H harness#3727
lklimek merged 33 commits into
feat/rs-platform-wallet-e2efrom
test/rs-platform-wallet-shielded-e2e

Conversation

@lklimek

@lklimek lklimek commented May 22, 2026

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Imagine you are a wallet engineer about to ship Orchard shielded transfers to real users. You can shield, transfer privately, and unshield — but how confident are you that the credits actually land where they should, that a broken backing store fails loudly instead of silently eating funds, and that a note you received before binding your wallet is still spendable? Right now that confidence rests on hope. This PR lays the spec for the test suite that turns hope into a green (or honestly-red) checkmark.

This is the spec-first slice of the shielded (Orchard) e2e suite for rs-platform-wallet, targeting the merged feat/rs-platform-wallet-e2e branch (#3549).

What was done?

Adds the shielded e2e test specification to packages/rs-platform-wallet/tests/e2e/TEST_SPEC.md (single-file change):

  • New ### Shielded (SH) test area in §3 — SH-001..SH-019 covering all five shielded transition types (shield/transfer/unshield/shield-from-asset-lock/withdraw-to-L1) plus store/note-selection/sync correctness pins.
  • §2 capability matrix Shielded row rewritten from "out of scope" → "in scope behind --features shielded + Wave H".
  • §5 out-of-scope item 1 flipped to in-scope.
  • New Wave H harness plan in §4 (warmed CachedOrchardProver OnceCell, FileBacked bind_shielded helper, wait_for_shielded_balance, best-effort teardown unshield-sweep to the bank address).

Scope of this PR: spec only. The Wave H harness and the SH test implementations land in follow-up commits on this branch — no test code or production code is touched here.

Findings the suite is designed to prove (verified against the merged v3.1-dev feat tree):

Finding Sev What it proves Pin
Found-027 HIGH InMemoryShieldedStore::witness() unconditionally returns Err (store.rs:409-416) — spends are structurally non-functional on the in-memory store while FileBackedShieldedStore::witness() works; a silent backing-store-dependent split SH-005 (red-by-design)
Found-028 HIGH shielded_add_account (platform_wallet.rs:439-457) updates only the per-wallet keys slot and never calls coordinator.register_wallet with the expanded set — notes for the added account never sync SH-006 (red-by-design)
Found-030 LOW anchor-semantics doc drift between extract_spends_and_anchor (operations.rs:601-611) and FileBackedShieldedStore::witness (file_store.rs:162-165) — depth-0 described two different ways SH-030 doc note
Found-029 (FIXED) pre-bind notes were permanently unwitnessable; fixed by v3.1-dev #3603 (sync.rs now marks every commitment position) SH-007 GREEN regression guard

DX gap noted: there is no public PlatformWallet::shielded_shield_from_asset_lock wrapper for the Type-18 (shield-from-asset-lock) path — SH-018 has to reach through lower-level plumbing. Worth a first-class wrapper as a follow-up.

How Has This Been Tested?

Not applicable to this commit — it adds a Markdown specification only. The findings it cites were each verified by inspection against the merged feat tree (288ea92): Found-027/028/030 confirmed still-live, Found-029 confirmed fixed-by-#3603.

Note on test intent: these tests are designed to prove issues. The Found-027 and Found-028 pins (SH-005, SH-006) are red-by-design — they are expected to fail and will be left red for triage. A failing test here is a feature, not a regression.

Breaking Changes

None. Documentation-only change.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Adversarial / break-the-backend cases (SH-020..SH-035)

Marvin sharpened the suite with an abuse pass. The purpose of these cases is not to confirm happy paths — it is to attack Drive's consensus / state-transition validation and the Orchard proof verifier with malformed, forged, and replayed shielded transitions. A RED is the deliverable: a failing assertion here means the backend accepted a transition it should have rejected — i.e. a real consensus/proof-verification hole. A green means the backend correctly refused the attack.

The 16 adversarial cases (SH-020..SH-035) each construct a deliberately-invalid shielded transition and assert the backend rejects it. The six that are CRITICAL if they go red (backend accepted the attack):

Case Attack
SH-020 Double-spend — same nullifier spent in two transitions
SH-022 Value not conserved — outputs exceed inputs (mint-from-nothing)
SH-025 Forged proof — bundle carries a proof that does not verify against its public inputs
SH-033 Duplicate nullifier within a single bundle
SH-034 Tampered binding signature
SH-035 Replayed Type-18 asset-lock — re-use of an already-consumed AssetLockProof

Mechanism — [INJECT] seam: these cases bypass the client-side guards (which would refuse to build an invalid bundle), reach into the dpp builder, mutate the SerializedBundle directly, and submit via broadcast_raw. This is what lets the test hand Drive a transition the honest client would never produce. The whole adversarial cohort is gated behind the PLATFORM_WALLET_E2E_SHIELDED_ADVERSARIAL environment flag (off by default).

These findings only materialize in a LIVE run against Drive — they exercise server-side consensus and proof verification, which a local/unit run cannot reproduce. Until the suite runs against a live Drive node, SH-020..SH-035 are spec-only intent.

Failed Tests

Living ledger — updated after every live run. A RED for an adversarial (SH-020..SH-035) case is a backend finding, not a test defect.

Run 5 — 2026-06-08, paloma devnet (Core 23.1.2, proto 70240): SPV blocker RESOLVED, adversarial sweep GREEN

The run-2 SPV P2P handshake blocker is gone on paloma — the shielded functional + adversarial suite ran live against Drive. Headline: every attempted adversarial vector was correctly REJECTED by the backend. Zero P0; no consensus or proof-verification holes.

ID Attack Backend verdict Stage
SH-020 Double-spend — same nullifier, two transitions REJECTEDcredited_count == 1 (post-execution state delta) consensus
SH-021 Nullifier replay after restart REJECTED check_tx
SH-022 Value not conserved (mint-from-nothing) REJECTED check_tx
SH-025 Forged proof REJECTED check_tx
SH-033 Duplicate nullifier within one bundle REJECTED check_tx
SH-034 Tampered binding signature REJECTED check_tx
SH-035 Replayed Type-18 asset-lock proof REJECTEDoutput 0 already completely used consensus

Instrumentation added this commit (80f6c597d1): each probe now emits a greppable ADV-VERDICT probe=<id> stage=<check_tx\|consensus\|build> result=<rejected\|accepted\|unobserved> line, so a live run's verdicts are auditable with one grep. SH-020 was reworked to assert on post-execution STATE (before/after fetch_credits on two distinct destinations → credited_count == 1), not mere mempool admission — a check_tx Ok is not consensus acceptance. SH-035's funding was corrected: it now shields less than the asset lock (SHIELD_DUFFS < ASSET_LOCK_DUFFS) so Drive has fee headroom, letting the first shield commit and the replay leg reach the single-use check.

Also on this branch (a9135b865d): an interim HRP unblock so a tdash-encoded devnet recipient (which the bech32m decoder lossily maps to Testnet) is accepted on a Devnet/Regtest wallet. This is a stopgap for live devnet testing — superseded by the network-agnostic decode in #3781.

Historical:

  • Run 2, 2026-05-22 (porter devnet): genesis fixed; blocked at SPV P2P handshake — superseded by the paloma run above.
ID Test Expected Actual Comment
found_021 InstantLock dropped on context promotion RED-by-design FAILED as designed (offline) Expected red; not a backend finding.
found_022 asset-lock builder consumes change index on failed build RED-by-design FAILED as designed (offline) Expected red; not a backend finding.

🤖 Generated with Claude Code

…ification

Verified the proposed shielded (Orchard) test cases against the MERGED v3.1-dev
feat tree and applied user-approved full scope:

- Found-027 (InMemory witness Err) STILL LIVE — SH-005 stays red-by-design.
- Found-028 (shielded_add_account skips coordinator.register_wallet) STILL LIVE
  — SH-006 stays red-by-design.
- Found-029 (pre-bind notes unwitnessable) FIXED by #3603 (sync.rs marks every
  commitment position; verified sync.rs:291-310). Dropped as a red pin;
  SH-007 repurposed into a GREEN regression guard locking in the fix.
- Found-030 (anchor-semantics doc drift) STILL LIVE — SH-030 doc note.
- Coupling recorded: Found-027 (in-memory witness) is independent of #3603;
  the fix only helps the FileBacked path, which all spend-side SH cases use.

- SH-018 (Type 18 shield-from-asset-lock) and SH-019 (Type 19 withdraw to L1)
  un-deferred to P1, gated on a new Core-L1 harness requirement (asset-lock
  funding + Layer-1 payout observation); may run RED until plumbing lands.

- Wave H gains a best-effort + logged teardown shielded fund-sweep (unshield
  residual balance back to the bank platform address) to prevent bank-fund
  leak; RED-by-design / broken-witness cases must NOT fail teardown.

Changelog, §2 matrix, quick index, Found-NNN table, §4 Wave H, §5 register all
updated. Tally: 2 HIGH live (027, 028) + 1 LOW (030) = 3 live findings + 1
guarded-fix regression test (SH-007/Found-029). Spec only — no test
implementation, no production code touched.

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

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

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

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f45d316b-1905-46f0-8f45-32284c4d23e3

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/rs-platform-wallet-shielded-e2e

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 and others added 13 commits May 22, 2026 11:20
…H-020..SH-035)

Rewrites the suite's stated purpose: attempt to BREAK THE BACKEND (Drive
consensus / state-transition validation + Orchard proof verifier), not confirm
happy paths. Adds 16 adversarial cases, each asserting backend rejection / safe
behavior; a RED is the deliverable (proves a malformed transition was accepted
or mishandled).

Cases: SH-020 double-spend, SH-021 nullifier replay after restart, SH-022 value
not conserved, SH-023 fee underpayment, SH-024 u64/i64 boundary, SH-025 forged
proof, SH-026 anchor mismatch (Found-030 dynamic probe), SH-027 malformed note
serde, SH-028 interrupt-sync, SH-029 reorg/out-of-order/rescan-from-0, SH-030
cross-network/own-address/self-transfer, SH-031 rebind-different-seed, SH-032
exact-change boundary, SH-033 intra-bundle duplicate nullifier, SH-034 tampered
binding sig, SH-035 replayed asset-lock proof. Consensus-critical attacks
(020/022/025/033/034/035) re-ranked P0/P1, CRITICAL-if-they-fail.

Methodology: client-side wallet guards must NOT mask the backend test —
[INJECT]-marked cases construct/mutate transitions at the protocol boundary
(public dpp::shielded::builder build_*_transition -> mutable SerializedBundle
{anchor,proof,value_balance,binding_signature} -> BroadcastStateTransition) and
broadcast directly, bypassing PlatformWallet::shielded_* guards.

Wave H gains an adversarial injection hooks block (raw build/broadcast, bundle-
byte mutation, TamperingProver, build-against-known-note, store-seed-malformed-
note, scriptable mock sync source, asset-lock reuse) behind a
PLATFORM_WALLET_E2E_SHIELDED_ADVERSARIAL gate.

Changelog, SH intent note, quick index, Wave H updated. Spec only — no test
implementation, no production code touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…wait, sweep, inject hooks)

Adds the framework/shielded.rs module unlocking the SH (Orchard) area:

- shielded_prover(): process-wide warmed CachedOrchardProver behind the
  prover module's OnceLock — warm once, borrow &'static everywhere.
- bind_shielded(): per-test FileBacked NetworkShieldedCoordinator over a
  fresh per-call SQLite path under the workdir slot, plus a ShieldedHandle
  (sync(true) driver + per-account balances). FileBacked is mandatory —
  the in-memory store's witness() is a hard Err (Found-027).
- new_file_backed_coordinator(): bind-free coordinator for SH-007's
  controlled bind-ordering hook.
- in_memory_store(): InMemory backing for SH-005's witness split.
- wait_for_shielded_balance(): force-sync poller mirroring the
  tokens::wait_for_token_balance shape + STEP_TIMEOUT.
- shielded_default_address_43(): SH-003 transfer-recipient plumbing.
- teardown_sweep_shielded(): best-effort, log-on-error unshield of
  residual shielded balance back to the bank platform address. Swallows
  every error (broken-witness cases must NOT fail teardown).

Adversarial injection hooks (scaffolded for the SH-020..SH-035 follow-up,
gated behind PLATFORM_WALLET_E2E_SHIELDED_ADVERSARIAL): build_raw_shielded_transition,
broadcast_raw, mutate_serialized_bundle, TamperingProver, build_against_note,
seed_malformed_note, reuse_asset_lock_proof, MockSyncSource. The seams pin
the inputs the abuse cases need; live bodies land in the follow-up wave.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the functional/baseline shielded (Orchard) tier per TEST_SPEC.md
§3 '### Shielded (SH)'. All gated behind the e2e feature (pulls shielded);
no #[ignore]. Tests assert CORRECT behavior — RED-by-design cases are left
failing to pin live bugs.

GREEN (happy-path + correctness):
- SH-001 shield from account (Type 15)
- SH-002 shield→unshield round-trip (Type 15→17)
- SH-003 shielded transfer between accounts (Type 16)
- SH-004 shielded_balances reflects note only after sync
- SH-008 unshield insufficient-balance typed error + reservation release
- SH-009 zero-amount rejection (RED arm if transfer/unshield lack a guard)
- SH-010 double-spend guard: concurrent spends reserve disjoint notes
- SH-011 note-selection convergence + u64::MAX overflow guard
- SH-012 sync watermark idempotency (double-sync stable + spendable)
- SH-013 bind empty accounts → typed ShieldedKeyDerivation
- SH-014 spend before bind → ShieldedNotBound; unbound account → KeyDerivation
- SH-007 GREEN regression guard: pre-bind note witnessable/spendable (#3603)

RED-by-design (pin live bugs — do NOT fix from inside tests):
- SH-005 InMemory witness() hard-Err vs FileBacked success (Found-027)
- SH-006 shielded_add_account never re-registers on coordinator (Found-028)

Core-L1 gated (MAY run RED until plumbing exists — documents the seam):
- SH-018 shield from asset lock (Type 18) — flags two production gaps:
  no public shielded_shield_from_asset_lock wrapper, and no test seam
  returning the one-time asset-lock private key.
- SH-019 shielded withdraw to L1 (Type 19) — shielded-side asserted
  unconditionally; L1 payout observation left as a documented TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o test/rs-platform-wallet-shielded-e2e

# Conflicts:
#	packages/rs-platform-wallet/tests/e2e/cases/mod.rs
Implements the adversarial/abuse tier per TEST_SPEC.md §3 — each ATTACKS
the protocol boundary and asserts the BACKEND must reject (or behave
safely). All gated behind e2e + shielded + PLATFORM_WALLET_E2E_SHIELDED_ADVERSARIAL
(no-op pass when the env is unset, so the default suite stays green). No
#[ignore]. Tests assert CORRECT rejection — no weakened assertions.

Live backend/wallet-reaching (achievable via public API, no prod-seam change):
- SH-027 malformed note serde: seeds a non-115-byte note via the public
  ShieldedStore trait and drives operations::unshield → deserialize_note;
  asserts a typed error (no panic = no DoS, no silent corruption).
- SH-030 cross-network/wrong-HRP/malformed recipient: client parse +
  network-mismatch guard fires with a typed ShieldedBuildError.
- SH-031 rebind-different-seed: asserts seed_A's note does NOT leak into
  seed_B's balance and re-discovers cleanly on rebind-back (no key mix).
- SH-032 exact-change boundary: note == amount+fee leaves ZERO change;
  amount+fee-1 is rejected ShieldedInsufficientBalance.

Harness hooks fleshed out: broadcast_raw (StateTransition deserialize +
broadcast, gated), seed_malformed_note (live via ShieldedStore trait).

RED-by-gap (flagged production-seam gaps — NOT fixed, per instructions):
- SH-020/021/022/023/024/025/026/033/034: reaching Drive with a
  valid-except-for-the-tamper transition needs a build-only shielded
  capture seam (shielded operations::* build AND broadcast internally;
  extract_spends_and_anchor / reserve_unspent_notes / build_spend_bundle
  are private; the public dpp build_*_transition enforce value/fee/overflow
  guards internally). See framework::shielded::ADVERSARIAL_SEAM_MISSING.
- SH-028/029: no injectable sync source (sync_notes_across is pub(super),
  fetches from the SDK directly) — needs a SyncSource production seam.
- SH-035: stacks the SH-018 Core-L1 private-key gap + the asset-lock-proof
  reuse seam.

The 6 CRITICAL-if-red consensus attacks (SH-020/022/025/033/034/035) and
the HIGH-if-red ones are pinned with their attack + expected consensus
error (NullifierAlreadySpentError 40901, ShieldedInvalidValueBalanceError
10822, AnchorMismatch) ready to assert once the capture seam lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… seams + wire SH-018/020-035

Closes the production-seam gaps the adversarial wave needed, then wires the
abuse cases to actually reach Drive.

GAP 1 — build/broadcast split (production):
- operations.rs: each spend gains a build_*_st entrypoint returning the
  signed StateTransition WITHOUT broadcast (build_shield_st,
  build_shield_from_asset_lock_st, build_unshield_st, build_transfer_st,
  build_withdraw_st) + a shared broadcast_st. The existing combined
  shield/unshield/transfer/withdraw/shield_from_asset_lock are now thin
  build-then-broadcast wrappers — PlatformWallet::shielded_* and all
  callers unchanged.

GAP 4 — public Type-18 wrapper (production):
- PlatformWallet::shielded_shield_from_asset_lock added, mirroring the
  other four spend wrappers; delegates to operations::shield_from_asset_lock.

GAP 2 + GAP 5 — test-utils feature (NOT in default; pulled by e2e):
- New  cargo feature. operations::test_utils exposes
  reserve_unspent_notes_for_test, extract_spends_and_anchor_for_test,
  unspent_notes_for_test (build-against-chosen-note / skip-reservation),
  and derive_asset_lock_private_key (seed,path -> one-time key, Gap 5).

Harness (framework/shielded.rs): broadcast_raw now takes a StateTransition;
mutate_serialized_bundle tampers proof/binding_signature/anchor/amount via
the public V0 fields (no byte offsets); capture_unshield_st +
build_unshield_st_against_notes + unspent_notes build real transitions
through the new seams. Removed the stub MockSyncSource / RawShieldedKind /
ADVERSARIAL_SEAM_MISSING.

Adversarial cases now REACH the backend (assert backend rejection; RED iff
accepted/mishandled):
- SH-022/024/025/026/034: capture a valid unshield, byte-tamper
  value/proof/anchor/binding-sig, broadcast_raw.
- SH-020/021/033: build against a chosen note skipping reservation
  (double-spend, replay-after-confirm, intra-bundle duplicate nullifier).
- SH-018/035: public Type-18 wrapper + Gap-5 key helper +
  create_funded_asset_lock_proof (Core-L1 gated, may run RED).
- SH-023: client fee-floor asserted; backend-floor arm flagged as a
  residual gap (no post-build fee seam).
- SH-027/030/031/032: unchanged (already reached wallet/backend).

BLOCKED + removed: SH-028/SH-029 (no injectable sync-source seam —
sync_notes_across is pub(super), fetches from the SDK directly). Marked
BLOCKED in TEST_SPEC.md. SH-018 spec line restored to implemented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The TODO blamed an upstream protocol-version mismatch (dash-spv 70237
vs Dash Core 70240). That diagnosis was wrong — rust-dashcore sets
PROTOCOL_VERSION=70237 and dash-spv's peer-acceptance floor is 60001,
so no version-based rejection applies. The actual cause was a broken
Core on the porter devnet (now fixed); no SPV-side workaround was ever
warranted.

Removing the speculation so future readers don't pursue a phantom
upstream protocol bump.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…et-shielded-e2e

# Conflicts:
#	packages/rs-platform-wallet/Cargo.toml
#	packages/rs-platform-wallet/src/wallet/shielded/operations.rs
… + shielded operations

Combine the e2e `serde` feature with the shielded `test-utils` feature, and
keep the shielded Type-18 shield-from-asset-lock helpers. Resolves leftover
conflict markers from the v3.1-dev cascade merge.

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

The v3.1-dev cascade merge auto-combined the import header of
shielded/operations.rs, dropping `build_shield_from_asset_lock_transition`
from the builder import group and duplicating a SecretKey alias, which broke
the `shielded` feature build. Restore the canonical shielded version of the
file so `--all-features` compiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lklimek and others added 2 commits June 1, 2026 10:21
…ormWalletManager::new

The cascade kept #3727's PlatformWalletManager::new(.., app_handlers:
Vec<Arc<dyn PlatformEventHandler>>) signature but brought in the
shielded_sync / shielded_sync_paloma examples from v3.1-dev, which
still passed a single Arc<dyn PlatformEventHandler>. Wrap the handler
in vec![..] to match the multi-handler API every other call-site
(basic_usage, ffi, e2e harness, spv_sync) already uses. Fixes the
--all-features example build (E0308 expected Vec, found Arc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lklimek lklimek added this to the v4.1.0 milestone Jun 1, 2026
lklimek and others added 11 commits June 2, 2026 10:37
…et-shielded-e2e

# Conflicts:
#	packages/rs-platform-wallet/tests/e2e/framework/spv.rs
…floor so adversarial probes reach Drive

Every SH case funded only 90M (FUNDING_CREDITS) and shielded 50M, but the
protocol's minimum shield fee (compute_minimum_shielded_fee) is folded into
the spend requirement as `amount + fee`, so each shield/unshield bounced in
note_selection with ShieldedInsufficientBalance BEFORE broadcast — 0 backend
coverage despite the gate working.

Per-group funding (fee headroom sized to clear the floor with margin):

- Shield succeeds, no real-API spend (raw INJECT seam or shield-only):
  SH-001/004/031/020/022/023/024/025/026/033/034 — FUNDING_CREDITS → 1.2e9,
  SHIELD_AMOUNT stays 50M (adversarial payload amounts untouched).

- Real fee-checked spend (unshield/transfer/withdraw): SH-002/003/005/006/
  007/012/019/021 — SHIELD_AMOUNT → 1.12e9 (covers the ≤20M spend + fee),
  FUNDING_CREDITS → 2.22e9 (covers the shield + its fee).

- Boundary / insufficiency cases, intent preserved:
  - SH-008: SHIELD 1.12e9 so the SATISFIABLE 3M unshield can pay its fee from
    the pool; OVERDRAW raised to 2.0e9 so it still exceeds the shielded
    balance and trips ShieldedInsufficientBalance (at the old 50M it would
    now be satisfiable and break the test).
  - SH-032: only FUNDING → 2.3e9 (covers the dynamic exact_note shield); the
    note size is derived from the REAL compute_minimum_shielded_fee at
    runtime, so the boundary semantics are already fee-correct.
  - SH-010: each note 1.11e9 so the two concurrent single-note unshields
    each cover UNSHIELD_EACH + fee.
  - SH-011: SHIELD_EACH 600M, MULTI_NOTE_UNSHIELD 650M (raw amount > a single
    note → forces multi-note selection independent of the exact fee), FUNDING
    1.7e9.

- Asset-lock cases SH-018/035: ASSET_LOCK_DUFFS → 1.2M (1.2e9 credits, above
  Drive's 100k-duff asset-lock floor AND the shield fee), TEST_WALLET_CORE_FUNDING
  → 1.4M duffs (lock + L1 fee), so the shield-from-asset-lock — and SH-035's
  REPLAY leg — execute against Drive.

Client-guard / no-broadcast cases (SH-009/013/014/027/030) need no funding
change — they reject before reaching a fee floor.

Bank Platform min: each SH test now draws up to ~2.3e9 from the bank's
Platform pool. Use the existing env knob PLATFORM_WALLET_E2E_MIN_BANK_CREDITS
(maps to Mins.platform via mins_from_config; default 500M) — set it to ~3e9
at run time so the planner asset-locks enough Core→Platform for the largest
single (serial) SH test. No constant change needed; bank Core ~102 tDASH
covers it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (adversarial gate, real shield fee, quorum-retirement SPV caveat, AL-001/PA-007/ID-002b status)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ield fee so malformed-spend probes can build (Fix 1)

The adversarial cases shielded a 50M note then tried to capture/build a
Type-17 unshield to tamper with, but the real Orchard unshield fee dwarfs
50M (1-action 111_548_800; 2-action 123_097_600), so the spend's
value check failed (`available 50000000, required 131548800`) and the
malformed transition was never built — 0 backend coverage.

Raise each note above its case's spend + fee so the build succeeds and the
probe reaches the broadcast/INJECT path:

- 1-action probes (capture/build-against one note, unshield 20M):
  SH-020 (double-spend two transitions), SH-022 (value-not-conserved),
  SH-024 (u64 overflow), SH-025 (forged proof), SH-026 (anchor mismatch),
  SH-034 (tampered binding sig): SHIELD_AMOUNT 50M → 200M
  (>= 20M + 111.5M fee + headroom), FUNDING_CREDITS 1.2e9 → 1.4e9 so the
  shield still clears the ~1e9 client fee reserve.

- 2-action probe SH-033 (duplicate nullifier in one bundle, [note, note]
  sum = 2×note, unshield 60M): SHIELD_AMOUNT 50M → 200M so 2×200M=400M
  covers 60M + 123M 2-action fee; FUNDING 1.2e9 → 1.4e9.

Intent preserved: SH-020 still spends one note across two transitions
(note covers ONE unshield); SH-022 still forges FORGED_AMOUNT (1e9) far
above the now-200M note; SH-033's duplicate-nullifier bundle still
"covers" its value so the probe is the dup, not insufficiency. Stale
"50M note" literals in SH-022/SH-033 comments updated to reference the
constants (present-state).

Scope: only these 7 note-size adversarial cases. The HRP-blocked cases
(SH-002/005/007/008/012/021/032) are untouched (separate investigation);
Fix 2 (Testnet/Devnet HRP mismatch) still gates the unshield destination,
so these probes don't reach Drive yet — not run live here.

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

Blanket RUST_LOG=trace against a live devnet floods logs from Orchard's
shardtree and the h2 crate at hot-loop volume (~8.4GB in ~4min of SPV sync),
filling disk and stalling the run. Document the shardtree=warn,h2=warn (or
narrow-scope) suppression mitigation in the e2e "Running tests" section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…avoid per-case full re-sync

Each sh_* case minted a fresh empty commitment-tree DB, so its watermark
was 0 and the suite re-streamed the entire ~1M-note Orchard history from
position 0 every case. Route all cases (except SH-007/SH-013, which need a
controlled private tree) through one process-shared NetworkShieldedCoordinator
over one persisted tree, and seed each freshly-bound account's watermark to
the shared tree_size so cases 2..N fetch only the tip delta — ~25-30x on the
Orchard-scan portion of the suite. teardown unregisters the wallet to keep
the shared registry bounded; the chain-wide tree is left intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arify SH-007 failure label

SP-001: unregister_wallet ran only from teardown_sweep_shielded, so the four
shared-coordinator cases that don't sweep (SH-009/030/035, SH-014 step 2)
leaked SubwalletId registrations, taxing every later case's per-batch
trial-decrypt. Lift the shared coordinator to a module OnceCell and call an
idempotent unregister from the universal SetupGuard::teardown — bounds the
registry for all 30 cases; no-op for non-shielded and SH-007/013 private trees.
SP-004: SH-007's unshield expect now surfaces the real error instead of
presuming a mark-every-position witness regression (correct pre/post #3781).
SP-002: document the shared OnceCell's serial/single-process/single-network
assumption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ChainLock liveness finding (needs re-repro before reporting)

Expand the AL-001 detail block + Quick-index row + changelog with the run-4
evidence (paloma 2026-06-02: 2/3 concurrent asset-lock txs missed IS-locks in
300s, ChainLock fallback also missed -> FinalityTimeout; solo build got IS-lock
in 0.67s). Frame the server-side liveness conclusion as the working hypothesis
and mark it OBSERVED — needs re-repro + root-cause before any upstream report;
NOT reported.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…0 (SD-002); soften quorum-gap-fragile 40901 readback assertion

Port the SD-002 SH-020 from verify-run4 (1014782039): add the wait_commit_raw
harness helper and judge the double-spend on the AUTHORITATIVE post-execution
on-chain STATE delta (both distinct destinations credited = double-spend; exactly
one = correct). Soften the secondary "rejected leg failed nullifier-already-spent
(40901)" check from a hard assert to a logged best-effort observation: on devnet
the rejected ST never commits, so its proof-verified readback times out, and the
rust-dashcore quorum-by-hash gap can mask the 40901 reason — a hard assert there
false-REDs even though credited_count==1 already proves the double-spend was
rejected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…est wallets (interim HRP unblock; superseded by #3781)

The bech32m decoder lossily maps `tdash` -> Testnet, so a devnet recipient
decodes as Testnet and the strict equality guard in shielded_unshield_to
rejected it, blocking all devnet unshield/transfer e2e cases. Relax the guard
to accept a Testnet-decoded address on a Devnet/Regtest wallet; the
genuine-mismatch branch (e.g. Mainnet recipient on a devnet wallet) is
unchanged. Superseded by #3781's network-agnostic decoder + HRP-class guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…V-VERDICT verdicts + fix SH-035 shield amount

- Add the ADV-VERDICT helper + wait_commit_raw to the shielded e2e framework
  so every adversarial probe emits a greppable consensus verdict.
- Wire per-probe ADV-VERDICT verdict logging into SH-020, SH-021, SH-022,
  SH-025, SH-033, SH-034, SH-035.
- SH-035: shield strictly less than the lock (SHIELD_DUFFS=1_400_000 vs
  lock 1_500_000) so the remainder covers Type 18's asset-lock processing
  fee; the replay reuses the same proof to hit Drive's single-use check.
- SH-020: tighten the secondary-corroboration block to hard-assert the
  rejected leg failed nullifier-already-spent (code 40901) when a consensus
  error is surfaced; the credited-count state delta remains the verdict.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lklimek and others added 4 commits June 9, 2026 18:25
…et-shielded-e2e

Cascade-merge feat/rs-platform-wallet-e2e (carrying the latest v3.1-dev)
up into the shielded-e2e test branch.

Conflicts resolved in packages/rs-platform-wallet:
- Cargo.toml: kept the test branch's `test-utils` feature and
  `e2e = ["shielded", "test-utils"]`; took feat's expanded `serde`
  feature documentation.
- src/wallet/shielded/operations.rs: kept BOTH the test branch's
  build/broadcast capture seams (build_unshield_st/build_transfer_st/
  build_withdraw_st/broadcast_st + the `test-utils` module) AND feat's
  IdentityCreateFromShieldedPool operation. Adapted the seams to the
  merged dpp builder API (builders now compute the fee and return
  (StateTransition, Credits); added the fee-agreement debug_assert and
  the new surplus_output arg on build_shield_from_asset_lock). Routed
  the unshield/transfer/withdraw bodies through the seams. Unioned the
  imports across both sides.

Follow-up merge-drift fixes (non-conflicted files) so the e2e harness
compiles: compute_minimum_shielded_fee now returns Result (sh_020,
sh_021, sh_032, sh_033) and PersistenceError::Backend is now a struct
variant (found_017, switched to the PersistenceError::backend ctor).

cargo check -p platform-wallet --features e2e --tests passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… gate default-on, withdrawals contract reg

- Right-size shielded funding constants (sh_007/010/011/018/021/023/031/032/035) so each shield clears amount + 1e9 fee reserve + protocol fee
- Register withdrawals system contract in TrustedHttpContextProvider (fixes sh_019 UnknownContract proof error)
- Default the adversarial gate (PLATFORM_WALLET_E2E_SHIELDED_ADVERSARIAL) to ON; keep env override (set =0 to disable)
- Relax over-strict debug_assert_eq fee guards in shielded build_*_st seams (panicked adversarial cases in debug builds); log fee mismatch at trace, dpp builder fee is authoritative
- Reword misleading bank shielded WARN (binding unimplemented, not prover warm-up)

Validated against paloma devnet: shielded subset 24 passed / 6 failed (remaining = 1 RED-by-design bug-pin + devnet-timing flakes). Merge-reconciliation seams exercised at runtime; backend enforced double-spend/replay/value-conservation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t-tolerant reason check; sh_024/025/034 per-probe note funding)

sh_020: gate the secondary wrong-reason corroboration behind an is_timeout
check so a wait_commit_raw timeout/elapsed on the rejected double-spend is
not misread as a coded consensus rejection. The authoritative state-delta
verdict (exactly one of the two spends materialises) is unchanged.

sh_024/025/034: shield one Orchard note per adversarial probe (NUM_PROBES=2)
since capture_unshield_st reserves a note and never releases it, and bump
FUNDING_CREDITS to 1_725_702_400 = 2 x (SHIELD_AMOUNT + compute_minimum_shielded_fee(2))
+ 1e9 reserve, sized against the latest shielded fee constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…es (CODE-001..006, DOC-001..005)

CODE-002/005: shielded adversarial cases (sh_022/024/025/026/034) now gate
PASS/FAIL on the consensus rejection REASON via a new load-bearing
assert_adv_rejected helper — accept rejection at check_tx OR consensus, fail
only on consensus-accepted. Removes the transport-Err false-pass and the
check_tx-pass/consensus-reject false-RED of asserting bare broadcast_raw().is_err().

CODE-006: wait_for_token_balance and the mint supply-gate route through the
streak-based wait_for_token_predicate (>=2 consecutive distinct-replica hits),
matching the address/identity/contract waiters so DAPI round-robin lag can't
yield a false pass.

CODE-001: bank-floor skip across all 17 tk_* cases is centralized in
E2eContext::skip_if_bank_floor_unmet, which emits a loud WARN + E2E-SKIP
stderr marker so a drained-bank run is distinguishable from a real pass
(honest skip, not a hard failure — a drained CI bank is legitimate).

CODE-003: found_004/012/013 empty scaffolds are now #[ignore]d with reasons
and their misleading 'stays red' / 'gated behind e2e feature' docs corrected.

CODE-004: found_024 drives the REAL build_transfer_persistence_entries (exposed
via a test-utils seam) instead of an inline copy of the guard, so deleting the
production ownership guard turns the regression pin red.

DOC-001: TEST_SPEC changelog corrected — adversarial pass is ON-by-default
(opt out with a falsy value), not off-by-default. DOC-002: dropped 4 dead
/tmp reviewer-scratch references. DOC-003: shield-fee comments corrected to
the real V8 value 162,851,200 (~1.63e8). DOC-004: cases/mod.rs header now
describes present state. DOC-005: README RUST_LOG crate name platform_wallet.

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

Copy link
Copy Markdown
Collaborator

Paloma devnet validation + grumpy-review — shielded e2e stack

Branch brought up to date with v3.1-dev through the full stack (v3.1-dev#3549#3727) and validated against the paloma devnet (DAPI :1443, Core P2P :20001).

✅ Runtime validation (paloma)

  • Robustness re-run — 5/5 passed: sh_020 (double-spend), sh_024 (value-boundary overflow), sh_025 (forged proof), sh_032 (exact-change boundary), sh_034 (tampered binding signature). Adversarial probes all rejected for the right reason (InvalidShieldedProofError, value-balance). sh_020's new is_timeout guard relaxes only the secondary reason-corroboration — the authoritative credited_count == 1 state-delta verdict is untouched; the 2-note funding math is exact at validation version V8 (2×(2e8 + 1.628e8) + 1e9 = 1,725,702,400).
  • A merge-reconciliation defect was found and fixed during the cascade: over-strict debug_assert_eq! fee guards in the build_*_st seams panicked 8 adversarial cases in debug builds — the dpp builder fee is authoritative, so they are now trace!-logged.

🔍 Grumpy-review (whole stack vs v3.1-dev, including parent #3549 code)

6 specialist agents in parallel → 26 findings consolidated: 0 CRITICAL · 1 HIGH · 9 MEDIUM · 16 LOW. Production logic (UTXO reservation / same-outpoint double-spend defense, shielded build seams, broadcast reconciliation) is clean; secret hygiene verified; the test-utils spend-guard-bypass seams are correctly feature-gated and unreachable in production.

Fixed in this push (2f0c8322), re-validated 9 passed / 0 failed / 3 ignored on paloma:

  • Test false-pass risks (6 × MEDIUM) — adversarial cases now assert on the consensus rejection reason via a new assert_adv_rejected helper instead of bare broadcast_raw().is_err() (a transport hiccup can no longer read as "attack rejected"); the token waiter uses the 2-success streak to beat DAPI round-robin replica lag; the bank-floor precondition is now an honest, greppable E2E-SKIP marker (no more silent-green no-op across the 17 tk_* cases); empty found_004/012/013 scaffolds are honestly #[ignore]'d with accurate reasons; found_024 now drives the real ownership-guard path through a test-utils seam (deleting the production guard turns the pin RED).
  • Docs (5 × LOW) — adversarial-gate default corrected to ON-by-default; V8 shield-fee constant corrected to ~1.63e8; removed committed /tmp scratch-file references; cases/mod.rs header; README RUST_LOG crate name.

⚠️ One HIGH left for a maintainer call — DEP-001 (intentionally not auto-fixed)

All 8 rust-dashcore workspace crates are pinned to branch = "fix/sml-extnetinfo-v3-decode", which no longer exists — PR dashpay/rust-dashcore#797 merged 2026-06-05 and the branch was deleted (verified 404). The build currently resolves only because Cargo.lock still pins a reachable SHA; any cargo update or lockfile regeneration breaks resolution. Recommend re-pinning these deps to the merge-commit rev before this stack lands on v3.1-dev. This is a coordinated workspace-level decision, so it's flagged here rather than changed.

Commits: robustness 6528b31, review fixes 2f0c8322.

🤖 Co-authored by Claudius the Magnificent AI Agent

…gap (PA-3040)

The static address-funds transfer fee estimate (~6.5M for 1in/1out) sits ~2.3x
below Drive's chain-time fee (~15.08M on paloma), so a transition the protocol's
Phase-4 estimated-fee validator blesses is then rejected by Drive with
AddressesNotEnoughFundsError (platform #3040).

The one client lever is [DeductFromInput(0)], where the fee is drawn from the
fee target's REMAINING input balance: over-reserving input headroom covers the
gap. select_inputs_deduct_from_input now reserves PA3040_FEE_SAFETY_FACTOR (3x)
of the static estimate (~19.5M, ~29% margin over chain-time) via the
estimate_fee_for_inputs_with_safety_margin wrapper. The [ReduceOutput(0)] path
has no such lever (its fee is drawn from the caller-fixed output) and is left
untouched. Revert is a one-liner: drop the .saturating_mul in the wrapper.

PA-3040 re-aimed onto the [DeductFromInput(0)] path it can actually clear, and
documents the #3040 root cause + that removing the multiplier should make it red
again. Three DeductFromInput selection unit tests made factor-aware via a
 so they track the multiplier. 228 lib unit tests
pass; live paloma validation blocked by a transient quorum-retirement gap
(rust-dashcore#800) at setup, noted as a TODO in the test.

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

Copy link
Copy Markdown
Collaborator

Follow-up: PA-3040 client-side fee stopgap (0d545186)

Adds a client-side safety multiplier on the platform-address DeductFromInput(0) fee path so transitions clear Drive's chain-time fee — a stopgap for the backend fee-model inconsistency #3040 (rs-dpp calculate_min_required_fee estimates ~6.5M; rs-drive-abci chain-time fee ~15M, and rejects the transition the protocol's own Phase-4 validator blessed).

  • Scoped narrowly to DeductFromInput(0) (where over-provisioning the reserved input headroom actually works). ReduceOutput(0) has no client lever — its fee is paid from a caller-fixed output — so it's untouched and still needs the AddressFundsTransferTransition::calculate_min_required_fee is too low #3040 backend fix.
  • Factor = 3 (≈29% margin over the ~15.08M chain-time fee; 2× wouldn't clear). One-line revert when AddressFundsTransferTransition::calculate_min_required_fee is too low #3040 lands: delete .saturating_mul(PA3040_FEE_SAFETY_FACTOR) (greppable via #3040 / PA3040_FEE_SAFETY_FACTOR).
  • Validated: 228 lib unit tests pass, incl. the select_inputs_deduct_from_input headroom tests that exercise the multiplier. fmt/clippy clean (0 new warnings).
  • pa_3040 re-aimed onto DeductFromInput(0) as a regression guard (removing the multiplier should turn it red again).

⚠️ Live paloma validation pending — pushed without it (deliberately). The paloma devnet platform chain is currently halted at height 788 (a consensus halt that began right after a base-platform IdentityCreateFromShieldedPool / state-transition type 20 — from #3816, not this PR; no e2e test in this stack constructs or broadcasts type-20). Live e2e confirmation of the 3× factor will run once the devnet is restored.

🤖 Co-authored by Claudius the Magnificent AI Agent

The 8 rust-dashcore workspace deps (dashcore, dash-network-seeds,
dash-spv, key-wallet, key-wallet-ffi, key-wallet-manager, dash-network,
dashcore-rpc) were pinned to branch `fix/sml-extnetinfo-v3-decode`,
which was deleted after PR dashpay/rust-dashcore#797 merged 2026-06-05
(branch now 404). The build only resolved because Cargo.lock still
pinned a reachable SHA; any lockfile regen would have broken resolution.

Re-pin all 8 crates to the long-lived `dev` branch and regenerate
Cargo.lock. The rust-dashcore git source now resolves to dev tip
7ff6b246df72164adb351551e819e53d10057caa. Only the rust-dashcore source
entries changed (99 unchanged deps). `cargo check -p platform-wallet
-p platform-wallet-ffi` passes clean — dev is a superset of #797, no
API break.

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

Copy link
Copy Markdown
Collaborator

✅ DEP-001 resolved (83f6a6ece8)

The lone HIGH from the grumpy-review — the 8 rust-dashcore workspace crates pinned to the deleted branch fix/sml-extnetinfo-v3-decode (PR dashpay/rust-dashcore#797 merged 2026-06-05, branch 404'd) — is now fixed.

  • All 8 crates (dashcore, dash-network-seeds, dash-spv, key-wallet, key-wallet-ffi, key-wallet-manager, dash-network, dashcore-rpc) re-pinned to branch = "dev".
  • Cargo.lock re-resolved to the current dev tip (7ff6b246); zero stale-branch refs remain in Cargo.toml or Cargo.lock.
  • cargo check -p platform-wallet -p platform-wallet-ffi → clean (no API breakage from dev).

Pin choice: branch = "dev" (mainline — won't 404 like the deleted feature branch did). Since Cargo.lock is committed and holds the concrete 7ff6b246 SHA, builds are reproducible; only an explicit cargo update would advance it. If this repo standardizes on concrete-rev pins (cf. the ci-lockfix work on #3809), switching is a one-line change.

🤖 Co-authored by Claudius the Magnificent AI Agent

@lklimek lklimek marked this pull request as ready for review June 10, 2026 13:43
@lklimek lklimek requested a review from QuantumExplorer as a code owner June 10, 2026 13:43
@lklimek lklimek merged commit 9033daa into feat/rs-platform-wallet-e2e Jun 10, 2026
3 checks passed
@lklimek lklimek deleted the test/rs-platform-wallet-shielded-e2e branch June 10, 2026 13:43
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.

2 participants