Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 107 additions & 6 deletions packages/wasm-utxo/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 19 additions & 2 deletions packages/wasm-utxo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [

[features]
default = []
inspect = ["dep:num-bigint", "dep:serde", "dep:serde_json", "dep:hex"]
inspect = ["dep:num-bigint", "dep:serde_json", "dep:hex"]

[dependencies]
wasm-bindgen = "0.2"
Expand All @@ -37,10 +37,24 @@ musig2 = { version = "0.3.1", default-features = false, features = ["k256"] }
getrandom = { version = "0.2", features = ["js"] }
pastey = "0.1"
num-bigint = { version = "0.4", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
# serde is non-optional: the Ironwood PCZT serialization (always compiled) derives Serialize/
# Deserialize for its postcard mirror struct.
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true }
hex = { version = "0.4", optional = true }

# Orchard/Ironwood shielded-bundle construction (PCZT Constructor/Signer/Extractor).
# `default-features = false` DROPS the `circuit` feature, so halo2_proofs/halo2_gadgets
# (the heavy prover) are NOT pulled in — only Sinsemilla/pasta/note-encryption. Proving is
# delegated to an external service.
orchard = { version = "0.15", default-features = false, features = ["std"] }
rand = "0.8"
# Compact deterministic encoding of the Ironwood PCZT witness (proof-service payload / PSBT carry).
postcard = { version = "1.0", features = ["use-std"] }
# `ff::PrimeField::to_repr` for the one PCZT witness field (`alpha`, a Pallas scalar) that orchard
# exposes only as a curve scalar. Already in-tree via orchard/pasta_curves.
ff = "0.13"

# Pinned to avoid RUSTSEC-2026-0204 (invalid pointer dereference in fmt::Pointer)
crossbeam-epoch = ">=0.9.20"

Expand All @@ -58,6 +72,9 @@ pastey = "0.1"
# variant used as the txid oracle in the v6 codec tests. Native-only dev-dependency, so its
# pre-release zcash_*/orchard cohort stays out of the shipped wasm artifact.
zebra-chain = { version = "11.2", default-features = false }
# `NonEmpty` is required by `orchard::Bundle::from_parts`, used only in the ironwood_build golden
# test to reconstruct an orchard bundle from on-wire action data. Matches orchard's own version.
nonempty = "0.11"

[build-dependencies]
serde_json = "1.0"
Expand Down
86 changes: 86 additions & 0 deletions packages/wasm-utxo/js/fixedScriptWallet/ZcashBitGoPsbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,90 @@ export class ZcashBitGoPsbt extends BitGoPsbt {
override extractTransaction(maxFeeRate?: number): ZcashTransaction {
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


/**
* Create an empty Zcash **v6 (Ironwood)** shielding PSBT, with the consensus branch ID
* determined from block height.
*
* Add transparent inputs/outputs with the usual `addWalletInput` / `addWalletOutput`, the
* shielded output with {@link addIronwoodOutput}, then sign the transparent inputs over
* {@link ironwoodTransparentSighash} and finish with {@link combineIronwoodProof}.
*
* @param network - Zcash network name ("zcash", "zcashTest", "zec", "tzec")
* @param walletKeys - The wallet's root keys (sets global xpubs in the PSBT)
* @param options - Options including blockHeight (at/after NU6.3 activation)
*/
static createIronwood(
network: ZcashNetworkName,
walletKeys: WalletKeysArg,
options: { blockHeight: number; lockTime?: number; expiryHeight?: number },
): ZcashBitGoPsbt {
const keys = RootWalletKeys.from(walletKeys);
const wasm = WasmBitGoPsbt.create_empty_zcash_v6_at_height(
network,
keys.wasm,
options.blockHeight,
options.lockTime,
options.expiryHeight,
);
return new ZcashBitGoPsbt(wasm);
}

/**
* Add the shielded Ironwood output (Constructor role). Stores the orchard PCZT in the PSBT.
*
* @param recipient - 43-byte raw Orchard/Ironwood address
* @param amount - note value in zatoshi
* @param options.anchor - 32-byte Ironwood note-commitment-tree root
* @param options.memo - optional 512-byte memo (defaults to all zeros)
* @param options.ovk - optional 32-byte outgoing viewing key (omit for a keyless build)
*/
addIronwoodOutput(
recipient: Uint8Array,
amount: bigint,
options: { anchor: Uint8Array; memo?: Uint8Array; ovk?: Uint8Array },
): void {
const memo = options.memo ?? new Uint8Array(512);
this.wasm.add_ironwood_output(recipient, amount, options.ovk, options.anchor, memo);
}

/**
* The ZIP-244 v6 txid in display (reverse) byte order. Defined once the transparent
* inputs/outputs and the Ironwood output are in place; unchanged by signing or proving.
*/
ironwoodTxid(): Uint8Array {
return this.wasm.ironwood_v6_txid();
}

/**
* The ZIP-244 per-input transparent sighash (32 bytes) the key controlling transparent input
* `index` must sign.
*/
ironwoodTransparentSighash(index: number): Uint8Array {
return this.wasm.ironwood_v6_transparent_sighash(index);
}

/**
* Ingest a transparent-input signature returned by the client/HSM, after verifying it against
* {@link ironwoodTransparentSighash} for that input.
*
* @param index - transparent input index
* @param pubkey - the signing public key
* @param sig - DER ECDSA signature with the trailing SIGHASH_ALL byte (as in a scriptSig)
*/
addIronwoodSignature(index: number, pubkey: Uint8Array, sig: Uint8Array): void {
this.wasm.add_ironwood_v6_signature(index, pubkey, sig);
}

/**
* Transaction Extractor role: given the external prover's `proof` bytes, finalize the
* transparent inputs, apply the shielded binding signature, and return the broadcast-ready v6
* transaction bytes. Requires every transparent input to be signed via
* {@link addIronwoodSignature}.
*/
combineIronwoodProof(proof: Uint8Array): Uint8Array {
return this.wasm.combine_ironwood_proof(proof);
}
}
Loading