Skip to content

feat(wasm-utxo): expose v6 (Ironwood) PSBT flow via wasm + TS - #343

Draft
veetragjain wants to merge 1 commit into
veetragjain/cshld-1294-wasm-utxo-wire-v6-ironwood-buildsigncombine-intofrom
veetragjain/cshld-1295-wasm-utxo-expose-v6-ironwood-methods-via-wasm-bindgen-ts
Draft

feat(wasm-utxo): expose v6 (Ironwood) PSBT flow via wasm + TS#343
veetragjain wants to merge 1 commit into
veetragjain/cshld-1294-wasm-utxo-wire-v6-ironwood-buildsigncombine-intofrom
veetragjain/cshld-1295-wasm-utxo-expose-v6-ironwood-methods-via-wasm-bindgen-ts

Conversation

@veetragjain

Copy link
Copy Markdown
Contributor

Surface the dedicated ZcashBitGoPsbt v6 methods through the wasm bindings and the TypeScript ZcashBitGoPsbt wrapper.

wasm (src/wasm/fixed_script_wallet): add create_empty_zcash_v6_at_height, add_ironwood_output, ironwood_v6_txid, ironwood_v6_transparent_sighash, add_ironwood_v6_signature, and combine_ironwood_proof, plus private zcash()/zcash_mut() helpers to reach the Zcash arm. Byte-length validation for recipient (43) / anchor (32) / memo (512) / ovk (32) happens at the boundary; randomness uses OsRng (getrandom js).

zcash_psbt: make the internal serialize()/deserialize() v6-aware so the standard from_bytes/serialize path round-trips a v6 PSBT (plain PSBT carrying the transparent skeleton + PCZT + ZecV6Params) without the v4 tx-replacement dance.

TS (ZcashBitGoPsbt.ts): add createIronwood, addIronwoodOutput, ironwoodTxid, ironwoodTransparentSighash, addIronwoodSignature, and combineIronwoodProof.

Test (test/fixedScript/zcashIronwoodPsbt.ts): drive the bindings from TypeScript — build a shield PSBT, assert the Ironwood version group id, compute a 32-byte v6 txid + per-input sighash that survive a serialize round-trip, and check the signature/recipient validation error paths. (Full sign→combine runtime behavior is covered by the native Rust test in PR4.) A small getrandom shim lets the bundler-target wasm source randomness under the Node/mocha harness; production bundler/browser and nodejs-target builds don't need it.

Surface the dedicated ZcashBitGoPsbt v6 methods through the wasm bindings and
the TypeScript ZcashBitGoPsbt wrapper.

wasm (src/wasm/fixed_script_wallet): add create_empty_zcash_v6_at_height,
add_ironwood_output, ironwood_v6_txid, ironwood_v6_transparent_sighash,
add_ironwood_v6_signature, and combine_ironwood_proof, plus private
zcash()/zcash_mut() helpers to reach the Zcash arm. Byte-length validation for
recipient (43) / anchor (32) / memo (512) / ovk (32) happens at the boundary;
randomness uses OsRng (getrandom js).

zcash_psbt: make the internal serialize()/deserialize() v6-aware so the standard
from_bytes/serialize path round-trips a v6 PSBT (plain PSBT carrying the
transparent skeleton + PCZT + ZecV6Params) without the v4 tx-replacement dance.

TS (ZcashBitGoPsbt.ts): add createIronwood, addIronwoodOutput, ironwoodTxid,
ironwoodTransparentSighash, addIronwoodSignature, and combineIronwoodProof.

Test (test/fixedScript/zcashIronwoodPsbt.ts): drive the bindings from TypeScript
— build a shield PSBT, assert the Ironwood version group id, compute a 32-byte
v6 txid + per-input sighash that survive a serialize round-trip, and check the
signature/recipient validation error paths. (Full sign→combine runtime behavior
is covered by the native Rust test in PR4.) A small getrandom shim lets the
bundler-target wasm source randomness under the Node/mocha harness; production
bundler/browser and nodejs-target builds don't need it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 28, 2026

Copy link
Copy Markdown

CSHLD-1295

return ZcashTransaction.fromWasm(this.wasm.extract_zcash_transaction(maxFeeRate));
}

// --- Zcash v6 (Ironwood / NU6.3) shielding ---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maybe we want to create a separate ZcashIronwoodBitGoPsbt.ts instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 — I'd do this in two steps:

Now (this PR): TS-level split. New ZcashIronwoodBitGoPsbt.ts extends ZcashBitGoPsbt, move the six v6 methods (addIronwoodOutput / ironwoodTxid / ironwoodTransparentSighash / addIronwoodSignature / combineIronwoodProof) onto it, createIronwood returns it, and fromBytes detects the ZecV6Params marker and returns the subclass. This gives the type-safety win with no wasm change — the underlying wasm object is the same flat BitGoPsbt. The one thing to get right is the fromBytes polymorphism (deserialize needs to return the subclass when the v6 marker is present).

Follow-up: wasm-level split. A genuinely separate WasmZcashIronwoodPsbt struct. Cheaper than I first thought — the generic PSBT ops come free via impl_wasm_psbt_ops!(WasmZcashIronwoodPsbt, psbt) (src/wasm/psbt.rs:782); only the wallet-construction surface (add_wallet_input/add_wallet_output ± _at_index, serialize) is hand-written on BitGoPsbt today and would need delegation — or a second macro arm to generate it. That step removes the zcash()/zcash_mut() runtime guards in favor of compile-time separation, so it's worth doing, just not blocking this PR.

Doing the TS split first keeps the public API shape the follow-up will want (a distinct Ironwood type) without holding up the stack.


Generated by Claude Code

@OttoAllmendinger OttoAllmendinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review

Overview

Surfaces the v6 flow through wasm-bindgen and the ZcashBitGoPsbt TS wrapper (createIronwood, addIronwoodOutput, ironwoodTxid, ironwoodTransparentSighash, addIronwoodSignature, combineIronwoodProof), plus v6-aware serialize/deserialize that auto-detects a v6 PSBT by the ZecV6Params proprietary key. Convention compliance is good: Uint8Array throughout (recipient/anchor/memo/ovk/sig/txid/proof), bigint for amount, number for non-monetary blockHeight/index/lockTime/expiryHeight, camelCase names, and length-validation at the wasm boundary.

Type separation (ZcashIronwoodBitGoPsbt)

Discussed in the ZcashBitGoPsbt.ts thread — agreed plan is a TS-level split in this PR (subclass returned by createIronwood, with fromBytes returning it when the ZecV6Params marker is present), and a wasm-level split as a follow-up (separate WasmZcashIronwoodPsbt, generic ops via impl_wasm_psbt_ops!, dropping the zcash()/zcash_mut() runtime guards). The TS split is the priority here because it gives compile-time separation — right now the six v6 methods live on ZcashBitGoPsbt and only work when the PSBT is actually v6, failing at runtime otherwise.

Issues

1. (API consistency) ironwoodTxid() returns bytes in display (reverse) order.
It returns a reversed Uint8Array — neither the internal-order txid nor a display hex string, an easy-to-misuse middle ground; the rest of the codebase exposes ids as getId(): string. Consider returning a hex string (display) to match getId, or internal-order bytes and letting the caller reverse+encode. Whatever you pick, document the order — a bare Uint8Array gives no hint it's reversed.

2. (Minor) combine_ironwood_proof clones the whole PSBT and leaves the JS object usable afterward.
The Rust combine_ironwood_proof consumes self, so the wasm binding takes &self and self.psbt.clone()s (PCZT + proof). Functionally fine, but the JS object stays "live" after combine, so a second combineIronwoodProof silently re-runs on stale state. Consider documenting it as terminal, or invalidating on the JS side.

Test coverage

Good smoke coverage (vgid, 32-byte txid/sighash + serialize round-trip, bad-sig reject, bad-recipient-length reject). One gap worth a line: combineIronwoodProof and the happy path of addIronwoodSignature are never exercised from TS — that wasm-bindgen glue is only covered by the native #342 test. A minimal TS combine (even against a placeholder proof) would exercise the binding end-to-end.

Note on the #338 finding (unshield digest)

This top layer is shield-onlyaddIronwoodOutput adds the shielded output and the transparent side is always the funding inputs; there's no shielded-input → transparent-output path exposed anywhere in the stack. So compute_v6_sig_digest's inputs.is_empty() fallback (my #338 finding #1) is unreachable in the current product surface — every real v6 tx here has ≥1 transparent input. I'd downgrade that finding to "latent, not currently reachable" — still worth fixing for API honesty, but not a shipping blocker.

Praise

Clean length-validation at the wasm boundary (recipient 43 / anchor 32 / memo 512 / ovk 32), the well-documented getrandom shim scoped to the mocha/Node-ESM harness (with a clear note production builds don't need it), and the plain-PSBT auto-detection via the marker key that keeps serialize/fromBytes transparent to callers.


Generated by Claude Code

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