Skip to content

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

Draft
lklimek wants to merge 9 commits into
feat/rs-platform-wallet-e2efrom
test/rs-platform-wallet-shielded-e2e
Draft

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

Conversation

@lklimek
Copy link
Copy Markdown
Contributor

@lklimek lklimek commented May 22, 2026

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 against funded porter devnet. A RED here for an adversarial (SH-020..SH-035) case is a backend finding, not a defect in the test.

ID Test description Expected outcome Actual outcome Comment
No live run executed yet (porter bank unfunded). Table populates after the first run.

🤖 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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Important

Review skipped

Draft detected.

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31feebb4-ec29-413d-b952-ce5dc3e70155

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

1 participant