From 12473629fd5b2c8b043bf76c252aea627e6fbdf5 Mon Sep 17 00:00:00 2001 From: Veetrag Jain Date: Fri, 23 Oct 2026 10:43:42 +0530 Subject: [PATCH] feat(wasm-utxo): expose the Ironwood spend flow and KMS helpers to TS Ticket: CSHLD-2029 --- packages/wasm-utxo/Cargo.toml | 2 +- packages/wasm-utxo/cli/src/psbt/mod.rs | 2 +- .../ZcashIronwoodBitGoPsbt.ts | 280 ++++++++- .../fixedScriptWallet/ZcashIronwoodTypes.ts | 16 +- .../src/fixed_script_wallet/bitgo_psbt/mod.rs | 2 +- .../bitgo_psbt/zcash/error.rs | 20 +- .../bitgo_psbt/zcash/ironwood.rs | 4 +- .../bitgo_psbt/zcash/ironwood_spend.rs | 32 +- .../bitgo_psbt/zcash/tests/ironwood_spend.rs | 297 ++++++++- .../bitgo_psbt/zcash/tests/v6.rs | 6 +- .../bitgo_psbt/zcash/v6.rs | 10 +- packages/wasm-utxo/src/inspect/zcash.rs | 2 +- .../wasm/fixed_script_wallet/zcash_psbt.rs | 392 +++++++++--- .../wasm-utxo/src/wasm/try_from_js_value.rs | 19 + packages/wasm-utxo/src/wasm/zcash.rs | 9 +- .../wasm-utxo/src/zcash/ironwood_build.rs | 12 +- .../wasm-utxo/src/zcash/ironwood_construct.rs | 36 +- packages/wasm-utxo/src/zcash/ironwood_pczt.rs | 5 +- .../wasm-utxo/src/zcash/ironwood_verify.rs | 4 +- .../test/fixedScript/zcashIronwoodPsbt.ts | 2 +- .../test/fixedScript/zcashIronwoodSpend.ts | 577 ++++++++++++++++++ .../zcash/ironwood_spend_deshield.json | 83 +++ .../zcash/ironwood_spend_transfer.json | 156 +++++ 23 files changed, 1781 insertions(+), 187 deletions(-) create mode 100644 packages/wasm-utxo/test/fixedScript/zcashIronwoodSpend.ts create mode 100644 packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_deshield.json create mode 100644 packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_transfer.json diff --git a/packages/wasm-utxo/Cargo.toml b/packages/wasm-utxo/Cargo.toml index b770314373e..0f30d1d06a8 100644 --- a/packages/wasm-utxo/Cargo.toml +++ b/packages/wasm-utxo/Cargo.toml @@ -24,7 +24,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ default = [] inspect = ["dep:num-bigint", "dep:serde_json", "dep:hex"] # Pulls in orchard's halo2 circuit/proving-key machinery so this crate can produce its own -# Ironwood zk-proofs locally, instead of relying on an external proof service. Heavy (halo2 +# Ironwood zk-proofs locally, instead of relying on an external prover. Heavy (halo2 # circuit synthesis + proving-key generation), so it's opt-in and off by default — the shipped # WASM build (browser consumers) must not link the circuit. Standalone tooling (the CLI) enables # it explicitly. diff --git a/packages/wasm-utxo/cli/src/psbt/mod.rs b/packages/wasm-utxo/cli/src/psbt/mod.rs index caac4d1cb9f..a3bcc9d2346 100644 --- a/packages/wasm-utxo/cli/src/psbt/mod.rs +++ b/packages/wasm-utxo/cli/src/psbt/mod.rs @@ -167,7 +167,7 @@ pub enum PsbtCommand { /// Network (must be zec or tzec) #[arg(long, short, value_enum)] network: NetworkArg, - /// Halo2 proof bytes from the external proof service, hex-encoded + /// Halo2 proof bytes from the external prover, hex-encoded #[arg(long)] proof: Option, /// Produce the proof locally instead of supplying one via --proof (heavier: builds a diff --git a/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts index 8ceae6ccead..bffaebac406 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts @@ -14,7 +14,13 @@ import { type ParseOutputsOptions, type ParseTransactionOptions, } from "./BitGoPsbt.js"; -import { type ZcashShieldedSpend } from "./ZcashIronwoodTypes.js"; +import { + type IronwoodShieldedStatus, + type ZcashShieldedOutputRequest, + type ZcashShieldedSpend, + type ZcashShieldedSpendRequest, +} from "./ZcashIronwoodTypes.js"; +import { Zip317Dimensions } from "./Zip317Dimensions.js"; /** * Options for creating an empty Zcash v6 (Ironwood) shielding PSBT. @@ -93,19 +99,53 @@ type IronwoodParsedBase = ParsedTransaction 0, and + * there is no `ovk` — encryption belongs to the viewing-key holder in the spend flow. `memo` defaults per-entry to + * the ZIP-302 "no memo" encoding, as {@link addShieldedOutput}'s does + * @param options.anchor - the Ironwood note-commitment-tree root every spend's witness + * commits to (32 bytes) + * @returns the action index each requested spend/output landed at. The actions are shuffled, + * so request order does not equal action order: any caller correlating per-action data (a + * stored Unified Address, an out-ciphertext) must go through these indices. + */ + addShieldedSpends( + spends: ZcashShieldedSpendRequest[], + outputs: ZcashShieldedOutputRequest[], + options: { anchor: Uint8Array }, + ): { spendActionIndices: number[]; outputActionIndices: number[] } { + const map = this.wasm.add_ironwood_shielded_spends( + spends.map((spend) => ({ + recipient: spend.recipient, + value: spend.value, + rho: spend.rho, + rseed: spend.rseed, + cmx: spend.cmx, + nullifier: spend.nullifier, + witness: { position: spend.witness.position, authPath: spend.witness.authPath }, + })), + outputs.map((output) => ({ + recipient: output.recipient, + amount: output.amount, + memo: output.memo ?? zip302NoMemo(), + unifiedAddress: output.unifiedAddress, + })), + options.anchor, + ) as { spendActionIndices: number[]; outputActionIndices: number[] }; + return { + spendActionIndices: Array.from(map.spendActionIndices), + outputActionIndices: Array.from(map.outputActionIndices), + }; + } + + /** + * The out-ciphertext request: the raw PCZT bytes plus the real outputs still pending their + * `out_ciphertext`. The viewing-key holder re-encrypts each pending output under its FVK + * `ovk` and returns one 80-byte ciphertext per action index, applied with {@link addOutCiphertexts}. + * + * Throws when the PSBT is not a spend bundle (`IronwoodPsbtError.NotSpendBundle`) or + * nothing is pending (`IronwoodPsbtError.NothingPending`, e.g. a pure deshield, whose only + * output is transparent and carries no ciphertext). + */ + getOutCiphertextRequest(): { pczt: Uint8Array; actionIndices: number[] } { + const request = this.wasm.ironwood_out_ciphertext_request() as { + pczt: Uint8Array; + actionIndices: number[]; + }; + return { pczt: request.pczt, actionIndices: Array.from(request.actionIndices) }; + } + + /** + * Apply the out-ciphertext response: one `{ actionIndex, outCiphertext }` entry per + * pending real output. **Atomic** — every entry is validated before anything is written, so + * one bad entry stores nothing. Idempotent for identical bytes; different bytes for an + * already-set index throw (`IronwoodPsbtError.OutCiphertextConflict`). Frozen once signed: + * rejected once any signature exists (`IronwoodPsbtError.AlreadySigned`). + */ + addOutCiphertexts(entries: Array<{ actionIndex: number; outCiphertext: Uint8Array }>): void { + this.wasm.add_ironwood_out_ciphertexts(entries); + } + + /** Single-output counterpart to {@link addOutCiphertexts}. */ + addOutCiphertext(actionIndex: number, outCiphertext: Uint8Array): void { + this.addOutCiphertexts([{ actionIndex, outCiphertext }]); + } + + /** + * Install the signer's randomized `rk`s: one `{ actionIndex, alpha, rk }` entry per real + * spend, with `rk = ak.randomize(alpha)`. **Only `rk` is stored** — the PSBT never carries + * `alpha` or the wallet's `ak` (with `rk`, an `alpha` would reveal `ak`). + * + * `options.spendValidatingKey` (the wallet's spend-validating key `ak`, 32 bytes, optional) + * makes the installer check `rk == ak.randomize(alpha)` before storing; without it the pair + * is checked later, by the client's key-free verifier and by the keyed prover. + * + * Ciphertexts first: rejected while any real output's `out_ciphertext` is pending + * (`IronwoodPsbtError.OutCiphertextPending`). Frozen once signed + * (`IronwoodPsbtError.AlreadySigned`). **Atomic** — one bad entry stores nothing. + */ + addSpendRandomizers( + entries: Array<{ actionIndex: number; alpha: Uint8Array; rk: Uint8Array }>, + options?: { spendValidatingKey?: Uint8Array }, + ): void { + this.wasm.add_ironwood_spend_randomizers(entries, options?.spendValidatingKey); + } + + /** Single-spend counterpart to {@link addSpendRandomizers}. */ + setSpendRandomizer( + actionIndex: number, + alpha: Uint8Array, + rk: Uint8Array, + options?: { spendValidatingKey?: Uint8Array }, + ): void { + this.addSpendRandomizers([{ actionIndex, alpha, rk }], options); + } + + /** + * The ZIP-244 sig digest (32 bytes) the real spends' spend-auth signatures are computed + * over — the input to the FROST signing round. Every real output's `out_ciphertext` and + * every real spend's randomized `rk` must be installed first: the digest commits both + * (`IronwoodPsbtError.OutCiphertextPending` / `IronwoodPsbtError.RandomizerPending` + * otherwise). + */ + spendSighash(): Uint8Array { + return this.wasm.ironwood_spend_sighash(); + } + + /** + * Apply the signer's spend-auth signatures: one `{ actionIndex, signature }` entry per real + * spend, each a 64-byte RedPallas signature over {@link spendSighash}, verified against the + * spend's installed `rk` before it is stored. **Atomic.** Signing does not freeze this + * method: every spend-auth signature is over the same digest, so signatures may arrive in + * several batches. + */ + addSpendSignatures(entries: Array<{ actionIndex: number; signature: Uint8Array }>): void { + this.wasm.add_ironwood_spend_signatures(entries); + } + + /** Single-spend counterpart to {@link addSpendSignatures}. */ + addSpendSignature(actionIndex: number, signature: Uint8Array): void { + this.addSpendSignatures([{ actionIndex, signature }]); + } + + /** + * The proof request for the keyed prover: the raw PCZT bytes with each real spend's + * randomizer `alpha` patched in — `randomizers` is one `{ actionIndex, alpha }` per real + * spend. The alphas are written only to the returned bytes, never stored (with `rk`, an + * `alpha` would reveal the wallet's `ak`); they travel out of band from the FROST round to + * the prover with the proof request. + * + * Gated: every real output's `out_ciphertext`, every real spend's randomized `rk`, and + * every real spend's spend-auth signature must be installed first. This is the gated + * counterpart to the raw {@link getPczt} accessor, and replaces it for spend bundles. + */ + getProofRequest(randomizers: Array<{ actionIndex: number; alpha: Uint8Array }>): Uint8Array { + return this.wasm.ironwood_proof_request(randomizers); + } + + /** + * The orchestration view of the shielded workflow: the input kind, what is still pending at + * each stage, and whether the PSBT is ready for its proof — `readyForProof` means + * {@link getProofRequest} succeeds (given the alphas) and {@link combineProof} then needs + * only the proof. + */ + shieldedStatus(): IronwoodShieldedStatus { + return this.wasm.ironwood_shielded_status() as IronwoodShieldedStatus; + } + + /** + * The ZIP-317 conventional fee (zatoshi) for this PSBT's shape: the transparent terms plus + * the Ironwood action count (`max(real spends, real outputs)`) of the stored PCZT — shorthand + * for `Zip317Dimensions.fromPsbt(psbt).getConventionalFee()`. Before {@link addShieldedSpends} + * there is no PCZT, so the shielded term is zero; size a change output from the planned + * counts with {@link Zip317Dimensions} instead. + */ + zip317ConventionalFee(): bigint { + return Zip317Dimensions.fromPsbt(this).getConventionalFee(); + } /** * Client-managed `ovk`: re-encrypt the shielded output's `out_ciphertext` under **this wallet's** @@ -388,7 +627,7 @@ export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt< } /** - * Ingest a transparent-input signature returned by the client/HSM, after verifying it against + * Ingest a transparent-input signature returned by a signer, after verifying it against * {@link transparentSighash} for that input. * * @param index - transparent input index @@ -502,12 +741,21 @@ export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt< const wasmECPair = ECPair.from(key).wasm; return this.wasm.verify_ironwood_v6_signature_with_pub(inputIndex, wasmECPair); } - /** * 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 addTransparentSignature}. + * transaction bytes. + * + * What must be signed depends on the PSBT's input kind ({@link shieldedStatus}): + * + * - **Shielding** — every transparent input, via {@link addTransparentSignature} (2-of-3). + * - **Shielded spends** — every real spend's spend-auth signature, via + * {@link addSpendSignatures}; there are no transparent inputs to sign. The proof here is + * the one the prover returned for {@link getProofRequest}'s bytes — pass it straight in. + * + * The proof's length must match the bundle's action count (`ProofLength` otherwise), and the + * shielded binding signature is derived from the actions' value-commitment trapdoors, so the + * alphas never need to be present at combine time. * * Terminal: the wasm binding drops the stored PCZT on success, so any later v6 call on this PSBT * throws — including after a `serialize`/{@link fromBytes} round-trip, since the PCZT is gone @@ -546,9 +794,17 @@ export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt< * run — the Halo2 `zkproof` and binding signature, bridging this PSBT to the external proof * service and back. * - * Present only after {@link addShieldedOutput} has been called; `undefined` beforehand, and - * `undefined` again after {@link combineProof} succeeds, since that call drops the stored PCZT - * to make extraction terminal (see its doc comment). + * **Raw accessor.** It hands over whatever is stored, with no workflow gate. For a spend + * bundle, the gated counterpart is {@link getProofRequest}: it only returns bytes once the + * ciphertexts and randomizers are installed and every real spend is signed, with the alphas + * patched in — exactly what the keyed prover needs. Shielding keeps + * using `getPczt()`: the finalize step re-encrypts the dummy actions' ciphertexts itself, so + * the raw bytes are already final when the proof is requested. + * + * Present only after {@link addShieldedOutput} (shielding) or {@link addShieldedSpends} + * (spends) has been called; `undefined` beforehand, and `undefined` again after + * {@link combineProof} succeeds, since that call drops the stored PCZT to make extraction + * terminal (see its doc comment). */ getPczt(): Uint8Array | undefined { return this.wasm.ironwood_pczt_bytes(); diff --git a/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts b/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts index 9ec8dc77ffa..5cec43b28bc 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodTypes.ts @@ -1,11 +1,13 @@ /** * Shared Zcash Ironwood (v6) types for the shielded-spend flow. * - * These describe what the build service, the client and KMS each see as the PSBT moves - * through the build → KMS → sign → prove → combine flow. Parse-facing types live in + * These describe what each party sees as the PSBT moves through the + * build → ciphertexts → sign → prove → combine flow. Parse-facing types live in * `ZcashIronwoodBitGoPsbt.ts`; this module holds the shared vocabulary. */ +import { type ZcashIronwoodWitness } from "./ZcashIronwoodWitness.js"; + /** One shielded note to spend, as passed to `addShieldedSpends`. */ export type ZcashShieldedSpendRequest = { /** Raw 43-byte Orchard recipient (diversifier ‖ pk_d). */ @@ -18,9 +20,13 @@ export type ZcashShieldedSpendRequest = { rseed: Uint8Array; /** The note commitment the on-chain action carried. */ cmx: Uint8Array; - /** Merkle witness (position + auth path) proving `cmx` under the bundle's anchor. */ - witness: { position: number; authPath: Uint8Array[] }; - /** The spend's nullifier, from the note store or from KMS. */ + /** + * Merkle witness proving `cmx` under the bundle's anchor — a validated + * {@link ZcashIronwoodWitness}, built with `ZcashIronwoodWitness.build(cmx, position, + * authPath, anchor)`. + */ + witness: ZcashIronwoodWitness; + /** The spend's nullifier, from the note store or derived by the viewing-key holder. */ nullifier: Uint8Array; }; diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs index f7b98c59559..417146bbca9 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/mod.rs @@ -1493,7 +1493,7 @@ impl BitGoPsbt { /// /// Copies per input: partial_sigs, tap_key_sig, tap_script_sigs, proprietary. /// - /// Not supported for Zcash v6 (Ironwood) PSBTs: `deserialize_stripped` exists to accept an HSM + /// Not supported for Zcash v6 (Ironwood) PSBTs: `deserialize_stripped` exists to accept a signer /// response that has dropped everything but the signatures, but v6 bytes route to /// `deserialize_v6`, which requires the consensus branch id and the PCZT — so a stripped v6 /// response would fail with a "missing its Ironwood PCZT" error describing a corrupt PSBT diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/error.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/error.rs index c04a6d252a2..ab47ebf1cdc 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/error.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/error.rs @@ -28,8 +28,8 @@ pub enum IronwoodPsbtError { UnknownAction, /// The action index names a fabricated (dummy) half, not a real spend or output. DummyAction, - /// Ciphertexts first: these real outputs' `out_ciphertext`s are still pending — the KMS - /// ciphertext round must run before randomizers, sighashes, or proof requests. + /// Ciphertexts first: these real outputs' `out_ciphertext`s are still pending — the + /// ciphertexts must be installed before randomizers, sighashes, or proof requests. OutCiphertextPending { /// The pending real outputs' action indices. action_indices: Vec, @@ -76,6 +76,15 @@ pub enum IronwoodPsbtError { }, /// The operation has nothing to act on: there is no pending item, or the batch is empty. NothingPending, + /// A caller-supplied byte field has the wrong length. + BadLength { + /// The field's name as the caller spelled it (`outCiphertext`, `anchor`, …). + field: &'static str, + /// The field's fixed size. + expected: usize, + /// The supplied length. + actual: usize, + }, /// The Ironwood Constructor rejected the request; the wrapped /// [`IronwoodBuildError`](crate::zcash::ironwood_build::IronwoodBuildError) carries the /// reason (and its own `IronwoodBuildError.*` code). @@ -119,7 +128,7 @@ impl std::fmt::Display for IronwoodPsbtError { IronwoodPsbtError::OutCiphertextPending { action_indices } => write!( f, "real outputs {action_indices:?} are still pending their out_ciphertext; \ - run the KMS ciphertext round first" + install the out_ciphertexts first" ), IronwoodPsbtError::OutCiphertextConflict { action_index } => write!( f, @@ -158,6 +167,11 @@ impl std::fmt::Display for IronwoodPsbtError { IronwoodPsbtError::NothingPending => { write!(f, "there is nothing pending for this operation") } + IronwoodPsbtError::BadLength { + field, + expected, + actual, + } => write!(f, "{field} must be {expected} bytes, got {actual}"), IronwoodPsbtError::Build(e) => write!(f, "{e}"), IronwoodPsbtError::Pczt(e) => write!(f, "{e}"), IronwoodPsbtError::Verify(e) => write!(f, "{e}"), diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood.rs index df1e264eb58..9195c522136 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood.rs @@ -965,8 +965,8 @@ impl ZcashBitGoPsbt { /// Like [`Self::combine_ironwood_proof`], but produces the `zkproof` itself (via /// [`crate::zcash::ironwood_build::create_proof`]) rather than requiring it from an external - /// proof service. Behind the `orchard-proving` feature — standalone tooling (e.g. the CLI) - /// that has no external proof service to call can enable it; the shipped WASM build must not, + /// prover. Behind the `orchard-proving` feature — standalone tooling (e.g. the CLI) + /// that has no external prover to call can enable it; the shipped WASM build must not, /// since it would link the halo2 circuit into every consumer. Consumes `self`. #[cfg(feature = "orchard-proving")] pub fn combine_ironwood_proof_locally( diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood_spend.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood_spend.rs index e559bd27338..2421f1dae79 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood_spend.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/ironwood_spend.rs @@ -1,9 +1,9 @@ //! The Ironwood **spend** PSBT workflow: the keyless Constructor entry point //! ([`ZcashBitGoPsbt::add_shielded_spends`]) and the one-input-kind rule. //! -//! The build service (IMS) selects notes and calls [`ZcashBitGoPsbt::add_shielded_spends`] -//! with the notes' plaintext, their witnesses, the KMS-derived nullifiers and the requested -//! outputs — and no key at all. The constructed PCZT is stored in the PSBT together with the +//! The caller selects notes and calls [`ZcashBitGoPsbt::add_shielded_spends`] with the notes' +//! plaintext, their witnesses, their nullifiers and the requested outputs — and no key at +//! all. The constructed PCZT is stored in the PSBT together with the //! per-output Unified Addresses. //! //! The PSBT never carries the wallet's spend-validating key `ak`, nor the spends' randomizers @@ -25,20 +25,21 @@ use super::{IronwoodOutputRequest, ZcashBitGoPsbt}; impl ZcashBitGoPsbt { /// Constructor role: build the shielded spends (and optionally shielded outputs) as a /// single **keyless** orchard PCZT and store it in the PSBT — the deshield and - /// shielded→shielded Constructor for a build service that holds no viewing keys. + /// shielded→shielded Constructor for a caller that holds no viewing keys. /// /// Each entry of `spends` carries the spent note's plaintext (`recipient`/`value`/`rho`/ - /// `rseed`/`cmx`), its Merkle witness, and the nullifier (from the note store, or from - /// KMS's `generate-nullifiers`). Each entry of `outputs` is a requested shielded output - /// (its `ovk` must be `None`: KMS owns encryption in the spend flow). The bundle has + /// `rseed`/`cmx`), its Merkle witness, and the nullifier (from the note store, or derived + /// by the viewing-key holder). Each entry of `outputs` is a requested shielded output + /// (its `ovk` must be `None`: encryption belongs to the viewing-key holder in the spend + /// flow). The bundle has /// `max(spends, outputs)` actions; the actions are shuffled, so the returned /// [`IronwoodActionMap`](crate::zcash::ironwood_build::IronwoodActionMap) gives, for each /// `spends[i]`/`outputs[i]`, the action index it landed at. /// /// Every real spend carries [`PLACEHOLDER_RK`](crate::zcash::ironwood_build::PLACEHOLDER_RK) /// as its `rk` and no `alpha`; the signer installs the randomized `rk = ak.randomize(alpha)` - /// before any sighash is computed. Every output's `out_ciphertext` is a random placeholder — - /// KMS replaces it under its FVK `ovk`. No FVK ever enters the stored + /// before any sighash is computed. Every output's `out_ciphertext` is a random placeholder, + /// replaced under the FVK's `ovk`. No FVK ever enters the stored /// PCZT. /// /// Rules enforced here: @@ -89,7 +90,7 @@ impl ZcashBitGoPsbt { self.store_ironwood_pczt_and_unified_addresses(pczt, action_map, outputs)?; // Every real output starts pending its `out_ciphertext`: the // little-endian bitset over the bundle's actions starts all-clear, so - // `pending_out_ciphertexts` reports the full request until KMS's ciphertexts land. + // `pending_out_ciphertexts` reports the full request until the ciphertexts land. super::propkv::set_ironwood_out_ciphertexts_set( &mut self.psbt, vec![0u8; action_count.div_ceil(8)], @@ -124,7 +125,7 @@ impl ZcashBitGoPsbt { Ok(()) } - // ---- The KMS out_ciphertext / proof workflow ---- + // ---- The out_ciphertext / proof workflow ---- /// The stored PCZT, raw and deserialized, or [`IronwoodPsbtError::NotSpendBundle`] if /// absent. @@ -142,7 +143,7 @@ impl ZcashBitGoPsbt { /// /// The ciphertexts-first gate runs on bundles mid-way through the signer's randomizer writes, /// which the full verifier rejects, so it uses this classification alone; the paths that hand - /// data to KMS run the verifier first. + /// the PCZT out run the verifier first. fn real_output_indices(pczt: &orchard::pczt::Bundle) -> Vec { pczt.actions() .iter() @@ -188,9 +189,8 @@ impl ZcashBitGoPsbt { self.pending_out_ciphertexts_in(&pczt) } - /// The KMS out-ciphertext request: the raw PCZT bytes plus the real - /// outputs still pending their `out_ciphertext`. KMS decrypts nothing here — it - /// re-encrypts each pending output under its FVK `ovk` and returns one 80-byte + /// The out-ciphertext request: the raw PCZT bytes plus the real outputs still pending + /// their `out_ciphertext`. The viewing-key holder decrypts nothing here — it re-encrypts each pending output under its FVK `ovk` and returns one 80-byte /// ciphertext per action index, applied with [`Self::add_out_ciphertexts`]. /// /// Errors with [`IronwoodPsbtError::NotSpendBundle`] when no PCZT is stored or the @@ -284,7 +284,7 @@ impl ZcashBitGoPsbt { Ok(()) } - /// Apply the KMS out-ciphertext response: one `(action_index, + /// Apply the out-ciphertext response: one `(action_index, /// out_ciphertext)` pair per pending real output. **Atomic** — every entry is validated /// before anything is written, so one bad entry stores nothing. /// diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/ironwood_spend.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/ironwood_spend.rs index ad643e9b4bb..c951a281f5a 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/ironwood_spend.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/ironwood_spend.rs @@ -11,6 +11,8 @@ use crate::fixed_script_wallet::script_id::ScriptId; use crate::networks::Network; use crate::zcash::ironwood_build::{compute_out_ciphertext, IronwoodActionMap}; use crate::zcash::ironwood_construct::test_util::keyless_fixture; +#[cfg(feature = "orchard-proving")] +use crate::zcash::ironwood_construct::IronwoodKeylessSpendSpec; use crate::zcash::NetworkUpgrade; use ff::{Field, PrimeField}; use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey}; @@ -109,11 +111,11 @@ fn keyless_transfer_psbt( ) } -/// KMS's out-ciphertext round: re-encrypt every requested output's +/// The out-ciphertext round: re-encrypt every requested output's /// `out_ciphertext` under the FVK's external `ovk` and deliver it through /// [`ZcashBitGoPsbt::add_out_ciphertexts`] — the production entry point, so the pending /// marker tracks every install. -fn kms_out_ciphertexts(z: &mut ZcashBitGoPsbt, map: &IronwoodActionMap, fvk: &FullViewingKey) { +fn fvk_out_ciphertexts(z: &mut ZcashBitGoPsbt, map: &IronwoodActionMap, fvk: &FullViewingKey) { let ovk = *fvk.to_ovk(Scope::External).as_ref(); let pczt = z.ironwood_pczt().unwrap(); let entries: Vec<_> = map @@ -229,7 +231,7 @@ fn keyless_deshield_lifecycle_round_trips() { let bytes = z.serialize_v6(); let z = ZcashBitGoPsbt::deserialize_v6(&bytes, Network::ZcashTestnet).unwrap(); - // No shielded outputs ⇒ no KMS out-ciphertext round for this shape. + // No shielded outputs ⇒ no out-ciphertext round for this shape. let raw = z .combine_ironwood_proof(vec![0u8; Proof::expected_proof_size(1)], OsRng) .unwrap(); @@ -249,7 +251,7 @@ fn keyless_deshield_lifecycle_round_trips() { ); } -/// The shielded→shielded lifecycle: two spends into two outputs, with the KMS out-ciphertext +/// The shielded→shielded lifecycle: two spends into two outputs, with the out-ciphertext /// round — the patched ciphertexts are exactly what the FVK's `ovk` recovers, and they /// survive to the broadcast transaction. #[test] @@ -262,8 +264,8 @@ fn keyless_shielded_transfer_lifecycle_round_trips() { assert_eq!(z.ironwood_bundle_info().unwrap().value_balance, 0); - // KMS round: every output's out_ciphertext becomes recoverable under the FVK's ovk. - kms_out_ciphertexts(&mut z, &map, &fvk); + // Ciphertext round: every output's out_ciphertext becomes recoverable under the FVK's ovk. + fvk_out_ciphertexts(&mut z, &map, &fvk); let expected = z.ironwood_action_data().unwrap(); let pairs = install_randomizers(&mut z, &map, &ask); @@ -300,7 +302,7 @@ fn keyless_shielded_transfer_lifecycle_round_trips() { assert_eq!( action.out_ciphertext.to_vec(), expected.actions[i].out_ciphertext.to_vec(), - "action {i}'s KMS out_ciphertext survives to the broadcast transaction" + "action {i}'s real out_ciphertext survives to the broadcast transaction" ); } @@ -361,7 +363,7 @@ fn keyless_surplus_output_lifecycle_round_trips() { let (mut z, map, _ak, fvk, ask) = keyless_surplus_psbt(seed); assert_eq!(z.ironwood_bundle_info().unwrap().value_balance, 10_000_000); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let pairs = install_randomizers(&mut z, &map, &ask); install_spend_signatures(&mut z, &pairs, &ask); @@ -690,7 +692,7 @@ fn keyless_spends_prove_and_verify_locally() { ]; for (seed, build, n_actions) in shapes { let (mut z, map, _ak, fvk, ask) = build(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let pairs = install_randomizers(&mut z, &map, &ask); install_spend_signatures(&mut z, &pairs, &ask); prove_and_verify_locally(z, &pairs, &fvk, n_actions); @@ -771,10 +773,10 @@ fn prove_and_verify_locally( .expect("the keyless bundle's locally-produced proof verifies"); } -// ---- The KMS out_ciphertext / proof-request workflow ---- +// ---- The out_ciphertext / proof-request workflow ---- -/// KMS's ciphertext for one action, under the FVK's external `ovk`. -fn kms_out_ciphertext(z: &ZcashBitGoPsbt, fvk: &FullViewingKey, action_index: usize) -> [u8; 80] { +/// The real ciphertext for one action, under the FVK's external `ovk`. +fn fvk_out_ciphertext(z: &ZcashBitGoPsbt, fvk: &FullViewingKey, action_index: usize) -> [u8; 80] { let ovk = *fvk.to_ovk(Scope::External).as_ref(); compute_out_ciphertext(&z.ironwood_pczt().unwrap(), action_index, ovk, &mut OsRng).unwrap() } @@ -829,8 +831,8 @@ fn out_ciphertext_request_lists_real_outputs_and_errors_when_empty() { assert_eq!(pending, vec![map.output_action_index(0).unwrap()]); assert_eq!(z.pending_out_ciphertexts().unwrap(), pending); - // The KMS round resolves the request; a further request errors with NothingPending. - let ct = kms_out_ciphertext(&z, &fvk, pending[0]); + // The ciphertext round resolves the request; a further request errors with NothingPending. + let ct = fvk_out_ciphertext(&z, &fvk, pending[0]); z.add_out_ciphertexts(&[(pending[0], ct)]).unwrap(); assert!(z.pending_out_ciphertexts().unwrap().is_empty()); assert!(matches!( @@ -873,7 +875,7 @@ fn add_out_ciphertexts_is_atomic() { let (mut z, map, fvk) = keyless_two_to_one_psbt("ironwood_v6_keyless_atomic"); let before = z.raw_ironwood_pczt_bytes().unwrap(); let real_index = map.output_action_index(0).unwrap(); - let ct = kms_out_ciphertext(&z, &fvk, real_index); + let ct = fvk_out_ciphertext(&z, &fvk, real_index); let error = z .add_out_ciphertexts(&[(real_index, ct), (99, [0u8; 80])]) @@ -900,7 +902,7 @@ fn add_out_ciphertexts_is_atomic() { fn add_out_ciphertexts_is_idempotent_for_identical_bytes_only() { let (mut z, map, fvk) = keyless_two_to_one_psbt("ironwood_v6_keyless_idempotent"); let action_index = map.output_action_index(0).unwrap(); - let ct = kms_out_ciphertext(&z, &fvk, action_index); + let ct = fvk_out_ciphertext(&z, &fvk, action_index); z.add_out_ciphertexts(&[(action_index, ct)]).unwrap(); let after = z.raw_ironwood_pczt_bytes().unwrap(); @@ -927,7 +929,7 @@ fn add_out_ciphertexts_is_idempotent_for_identical_bytes_only() { fn add_out_ciphertexts_rejects_conflicting_duplicates_in_one_batch() { let (mut z, map, fvk) = keyless_two_to_one_psbt("ironwood_v6_keyless_batch_duplicates"); let action_index = map.output_action_index(0).unwrap(); - let ct = kms_out_ciphertext(&z, &fvk, action_index); + let ct = fvk_out_ciphertext(&z, &fvk, action_index); let before = z.raw_ironwood_pczt_bytes().unwrap(); let error = z @@ -953,12 +955,12 @@ fn add_out_ciphertexts_rejects_conflicting_duplicates_in_one_batch() { } /// A spend bundle whose bitset was stripped fails closed: every real output counts as -/// pending, the gates hold, and the KMS round still completes (re-creating the bitset). +/// pending, the gates hold, and the ciphertext round still completes (re-creating the bitset). #[test] fn missing_out_ciphertexts_marker_fails_closed() { let seed = "ironwood_v6_keyless_marker_stripped"; let (mut z, map, _ak, fvk, ask) = keyless_transfer_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); assert!(z.pending_out_ciphertexts().unwrap().is_empty()); strip_out_ciphertexts_marker(&mut z); @@ -975,11 +977,11 @@ fn missing_out_ciphertexts_marker_fails_closed() { let mut z = ZcashBitGoPsbt::deserialize_v6(&bytes, Network::ZcashTestnet).unwrap(); assert_eq!(z.pending_out_ciphertexts().unwrap(), expected); - // Re-delivering (the same KMS response would carry fresh randomness) resolves it. + // Re-delivering (a fresh response would carry fresh randomness) resolves it. let entries: Vec<_> = map .output_action_indices() .iter() - .map(|&action_index| (action_index, kms_out_ciphertext(&z, &fvk, action_index))) + .map(|&action_index| (action_index, fvk_out_ciphertext(&z, &fvk, action_index))) .collect(); z.add_out_ciphertexts(&entries).unwrap(); assert!(z.pending_out_ciphertexts().unwrap().is_empty()); @@ -995,7 +997,7 @@ fn add_out_ciphertexts_rejects_after_a_spend_signature() { let seed = "ironwood_v6_keyless_rule3_spend_sig"; let (mut z, map, _ak, fvk, ask) = keyless_transfer_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let pairs = install_randomizers(&mut z, &map, &ask); install_spend_signatures(&mut z, &pairs, &ask); @@ -1048,7 +1050,7 @@ fn proof_request_requires_ciphertexts_randomizers_then_signatures() { Err(IronwoodPsbtError::OutCiphertextPending { action_indices }) if action_indices == outputs )); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let mut spends = map.spend_action_indices().to_vec(); spends.sort_unstable(); assert!(matches!( @@ -1149,12 +1151,12 @@ fn sign_then_prove_lifecycle_combines_the_requested_proof() { let seed = "ironwood_v6_keyless_sign_then_prove"; let (mut z, map, _ak, fvk, ask) = keyless_transfer_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let pairs = install_randomizers(&mut z, &map, &ask); install_spend_signatures(&mut z, &pairs, &ask); let sighash = z.v6_sig_digest().unwrap(); - // Finalize, possibly in another request: the signed PSBT is all KMS needs. + // Finalize, possibly in another request: the signed PSBT is all the prover needs. let bytes = z.serialize_v6(); let z = ZcashBitGoPsbt::deserialize_v6(&bytes, Network::ZcashTestnet).unwrap(); let request = z.proof_request(&pairs).unwrap(); @@ -1201,7 +1203,7 @@ fn add_spend_randomizers_is_atomic_and_checks_the_pair_against_the_callers_ak() let seed = "ironwood_v6_keyless_randomizer_batch"; let (mut z, map, ak, fvk, ask) = keyless_transfer_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let [first, second] = [map.spend_action_indices()[0], map.spend_action_indices()[1]]; let pair = |action_index: usize| { let alpha = pasta_curves::pallas::Scalar::random(&mut OsRng); @@ -1274,7 +1276,7 @@ fn add_spend_randomizers_is_atomic_and_checks_the_pair_against_the_callers_ak() fn add_spend_randomizers_rejects_dummy_spends_and_shielding_bundles() { let seed = "ironwood_v6_keyless_randomizer_targets"; let (mut z, map, ak, fvk, _ask) = keyless_surplus_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let dummy = (0..2) .find(|i| !map.spend_action_indices().contains(i)) .unwrap(); @@ -1302,7 +1304,7 @@ fn add_spend_signatures_is_atomic() { let seed = "ironwood_v6_keyless_signature_batch"; let (mut z, map, _ak, fvk, ask) = keyless_transfer_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let pairs = install_randomizers(&mut z, &map, &ask); let sighash = z.spend_sighash().unwrap(); let sign = |&(action_index, alpha): &(usize, [u8; 32])| { @@ -1354,7 +1356,7 @@ fn add_spend_signatures_is_atomic() { fn combine_rejects_an_unsigned_spend_with_a_typed_error() { let seed = "ironwood_v6_keyless_combine_unsigned"; let (mut z, map, _ak, fvk, ask) = keyless_transfer_psbt(seed); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); install_randomizers(&mut z, &map, &ask); let mut spends = map.spend_action_indices().to_vec(); spends.sort_unstable(); @@ -1389,7 +1391,7 @@ fn shielded_status_tracks_every_stage() { assert_eq!(status.pending_spend_signatures, spends); assert!(!status.ready_for_proof); - kms_out_ciphertexts(&mut z, &map, &fvk); + fvk_out_ciphertexts(&mut z, &map, &fvk); let status = z.shielded_status().unwrap(); assert!(status.pending_out_ciphertexts.is_empty()); assert!(!status.ready_for_proof); @@ -1420,3 +1422,238 @@ fn shielded_status_tracks_every_stage() { assert_eq!(status.input_kind, IronwoodInputKind::Transparent); assert!(!status.ready_for_proof); } + +// ---- The TS fixture generator (test/fixedScript/zcashIronwoodSpend.ts) ---- +// +// The TS suite cannot produce the viewing-key holder's or prover's responses — the out-ciphertexts need the FVK and the proof +// needs Halo2 — so this `#[ignore]`d generator writes them into `test/fixtures/zcash/`. Run: +// +// cargo test --features orchard-proving generate_ts_spend_fixtures -- --ignored --nocapture +// +// The construction is seeded, so the post-construction PSBT bytes, the action map and the +// ZIP-317 fee are reproducible across runs. The ciphertexts, signatures and proof draw +// fresh `OsRng` entropy each run — they are internally consistent (same run produced the +// sighash chain and the proof), so regenerate every field together. + +#[cfg(feature = "orchard-proving")] +#[allow(clippy::too_many_arguments)] +fn spend_fixture_json( + name: &str, + mut z: ZcashBitGoPsbt, + map: IronwoodActionMap, + anchor: [u8; 32], + fvk: &FullViewingKey, + ak: [u8; 32], + ask: &SpendAuthorizingKey, + specs: &[IronwoodKeylessSpendSpec], + outputs: &[IronwoodOutputRequest], + transparent_output: Option<(Vec, u64)>, +) -> serde_json::Value { + use base64::Engine; + let to_hex = |bytes: &[u8]| hex::encode(bytes); + + // Deterministic construction: the shuffle (and therefore the action map) must be + // reproducible, since the TS lifecycle replays this exact PSBT. + let psbt_bytes = z.serialize_v6(); + let fee = z.zip317_conventional_fee().unwrap(); + + // The ciphertext round: one 80-byte ciphertext per real output, under the FVK's + // external `ovk`. A pure deshield has no real outputs and the request errors instead. + let out_ciphertexts: Vec<(usize, [u8; 80])> = match z.out_ciphertext_request() { + Ok((_, pending)) => { + let ovk = *fvk.to_ovk(Scope::External).as_ref(); + let pczt = z.ironwood_pczt().unwrap(); + let entries: Vec<(usize, [u8; 80])> = pending + .iter() + .map(|&action_index| { + ( + action_index, + compute_out_ciphertext(&pczt, action_index, ovk, &mut OsRng).unwrap(), + ) + }) + .collect(); + z.add_out_ciphertexts(&entries).unwrap(); + entries + } + Err(IronwoodPsbtError::NothingPending) => Vec::new(), + Err(error) => panic!("unexpected out-ciphertext request error: {error}"), + }; + + // The FROST stand-in: one (alpha, rk = ak.randomize(alpha)) pair per real spend. Only + // `rk` is stored; the alphas ride along out of band for the proof request. + let randomizers: Vec<(usize, [u8; 32], [u8; 32])> = map + .spend_action_indices() + .iter() + .map(|&action_index| { + let alpha = pasta_curves::pallas::Scalar::random(&mut OsRng); + let rk = <[u8; 32]>::from(VerificationKey::::from(&ask.randomize(&alpha))); + (action_index, alpha.to_repr(), rk) + }) + .collect(); + z.add_spend_randomizers(&randomizers, Some(&ak)).unwrap(); + + let sighash = z.spend_sighash().unwrap(); + let signatures: Vec<(usize, [u8; 64])> = randomizers + .iter() + .map(|&(action_index, alpha_repr, _)| { + let alpha = pasta_curves::pallas::Scalar::from_repr(alpha_repr).unwrap(); + ( + action_index, + <[u8; 64]>::from(&ask.randomize(&alpha).sign(OsRng, &sighash)), + ) + }) + .collect(); + z.add_spend_signatures(&signatures).unwrap(); + + let alphas: Vec<(usize, [u8; 32])> = randomizers + .iter() + .map(|&(action_index, alpha, _)| (action_index, alpha)) + .collect(); + let proof_request_bytes = z.proof_request(&alphas).unwrap(); + // The ZIP-244 txid commits the action data but neither signatures nor the proof, so it is + // the one stage value the TS run reproduces exactly. Recorded in display order, matching + // the `ironwood_v6_txid` wasm binding's `Txid::to_string()` convention (the TS test + // compares it against `getId()`). + let expected_txid = Txid::from_byte_array(z.v6_txid().unwrap()).to_string(); + + // The proof: what the keyed prover (here, the local prover) returns + // for the alpha-patched request bytes. Prove-and-combine locally, then slice the proof + // out of the transaction; the TS run re-does the combine itself via `combineProof`. + let raw = z + .clone() + .combine_ironwood_proof_locally_with_alphas(&alphas, Some(&fvk.to_bytes()), OsRng) + .unwrap(); + let tx = crate::zcash::v6::decode_v6_transaction(&raw).unwrap(); + let proof = tx.ironwood_bundle.as_ref().unwrap().proof.clone(); + + serde_json::json!({ + "name": name, + "network": "zcashTest", + "blockHeight": crate::zcash::NetworkUpgrade::Nu6_3.testnet_activation_height(), + "request": { + "anchor": to_hex(&anchor), + "spends": specs.iter().map(|spec| serde_json::json!({ + "recipient": to_hex(&spec.recipient), + "value": spec.value.to_string(), + "rho": to_hex(&spec.rho), + "rseed": to_hex(&spec.rseed), + "cmx": to_hex(&spec.cmx), + "witness": { + "position": spec.witness.position, + "authPath": spec.witness.auth_path.iter().map(|h| to_hex(h)).collect::>(), + }, + "nullifier": to_hex(&spec.nullifier), + })).collect::>(), + "outputs": outputs.iter().map(|output| serde_json::json!({ + "recipient": to_hex(&output.recipient), + "amount": output.amount.to_string(), + "memo": to_hex(&output.memo), + "unifiedAddress": output.unified_address, + })).collect::>(), + "transparentOutput": transparent_output.map(|(script, value)| serde_json::json!({ + "script": to_hex(&script), + "value": value.to_string(), + })), + }, + "psbt": base64::engine::general_purpose::STANDARD.encode(&psbt_bytes), + "spendActionIndices": map.spend_action_indices(), + "outputActionIndices": map.output_action_indices(), + "ak": to_hex(&ak), + "outCiphertexts": out_ciphertexts.iter().map(|&(action_index, out_ciphertext)| + serde_json::json!({ + "actionIndex": action_index, + "outCiphertext": to_hex(&out_ciphertext), + })).collect::>(), + "randomizers": randomizers.iter().map(|&(action_index, alpha, rk)| + serde_json::json!({ + "actionIndex": action_index, + "alpha": to_hex(&alpha), + "rk": to_hex(&rk), + })).collect::>(), + "sighash": to_hex(&sighash), + "signatures": signatures.iter().map(|&(action_index, signature)| + serde_json::json!({ + "actionIndex": action_index, + "signature": to_hex(&signature), + })).collect::>(), + "expectedTxid": expected_txid, + "proofRequest": to_hex(&proof_request_bytes), + "proof": to_hex(&proof), + "zip317ConventionalFee": fee.to_string(), + }) +} + +/// The transparent payout of the deshield fixture. +#[cfg(feature = "orchard-proving")] +const DESHIELD_PAYOUT_SCRIPT: [u8; 5] = [0x76, 0xa9, 0x14, 0x88, 0xac]; + +/// Generate both TS fixtures: the 1→0 deshield (transparent payout, no shielded outputs, so +/// no ciphertext round) and the 2→2 shielded→shielded transfer (ciphertexts). Both pay +/// exactly the ZIP-317 conventional fee for their shape. +#[test] +#[ignore] +#[cfg(feature = "orchard-proving")] +fn generate_ts_spend_fixtures() { + use rand::{rngs::StdRng, SeedableRng}; + + let dir = std::path::Path::new("test/fixtures/zcash"); + std::fs::create_dir_all(dir).unwrap(); + + for (seed, (name, is_deshield)) in [("deshield", true), ("transfer", false)] + .into_iter() + .enumerate() + { + let (specs, anchor, fvk, ak) = keyless_fixture(); + let ask = SpendAuthorizingKey::from(&test_spending_key()); + let mut z = empty_v6_psbt(&format!("ironwood_ts_spend_{name}")); + // Both shapes come to two logical actions (the deshield's Ironwood action plus its + // transparent output, the transfer's two Ironwood actions): the ZIP-317 grace floor. + let fee = 10_000; + let (specs_used, outputs, transparent_output) = if is_deshield { + let payout = specs[0].value - fee; + z.add_transparent_output( + miniscript::bitcoin::ScriptBuf::from(DESHIELD_PAYOUT_SCRIPT.to_vec()), + payout, + None, + ) + .unwrap(); + ( + &specs[..1], + Vec::new(), + Some((DESHIELD_PAYOUT_SCRIPT.to_vec(), payout)), + ) + } else { + let total: u64 = specs.iter().map(|spec| spec.value).sum::() - fee; + let outputs = vec![ + wallet_output(&fvk, 100, total / 2), + wallet_output(&fvk, 101, total - total / 2), + ]; + (&specs[..], outputs, None) + }; + // Seeded construction: the shuffle and the action map are reproducible. + let map = z + .add_shielded_spends( + specs_used, + &outputs, + &anchor, + StdRng::seed_from_u64(0xC511_0000 + seed as u64), + ) + .unwrap(); + assert_eq!(z.zip317_conventional_fee().unwrap(), fee); + let fixture = spend_fixture_json( + name, + z, + map, + anchor, + &fvk, + ak, + &ask, + specs_used, + &outputs, + transparent_output, + ); + let path = dir.join(format!("ironwood_spend_{name}.json")); + std::fs::write(&path, serde_json::to_string_pretty(&fixture).unwrap()).unwrap(); + println!("wrote {}", path.display()); + } +} diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/v6.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/v6.rs index 80a5aade3c8..dee9516dd70 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/v6.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/tests/v6.rs @@ -672,15 +672,15 @@ fn add_transparent_output_rejects_unified_address_on_v6_psbt() { /// `combine_inputs` refuses a v6 PSBT outright. /// -/// It parses the incoming bytes with `deserialize_stripped`, whose whole purpose is to accept an -/// HSM response that has dropped everything but the signatures. v6 bytes short-circuit to +/// It parses the incoming bytes with `deserialize_stripped`, whose whole purpose is to accept a +/// signer response that has dropped everything but the signatures. v6 bytes short-circuit to /// `deserialize_v6`, which requires the branch id and the PCZT, so a stripped v6 response would /// otherwise fail with "missing its Ironwood PCZT" — an error describing a corrupt PSBT rather /// than an unsupported code path. #[test] fn combine_inputs_rejects_a_v6_psbt() { let z = build_shield_psbt("v6_combine_inputs"); - // A stripped response: the same PSBT with the shielded state removed, as an HSM would return. + // A stripped response: the same PSBT with the shielded state removed, as a stripped signer response would carry. let mut stripped = z.clone(); stripped.mark_ironwood_extracted(); let stripped_bytes = stripped.serialize_v6(); diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/v6.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/v6.rs index 9dcf38caeeb..acac9624fcc 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/v6.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash/v6.rs @@ -11,11 +11,11 @@ // `VersionGroupId`/`ExpiryHeight` subtypes; `version_group_id`'s presence marks the PSBT as v6 so // it round-trips through a plain PSBT serialization without the v4 tx-replacement dance. // -// The lifecycle mirrors the microservice PSBT flow: the server builds (Constructor, no keys). For +// The lifecycle: the PSBT is built first (Constructor, no keys). For // real spends, the signer then installs each spend's randomizer pair (`add_spend_randomizers`) // before any sighash is fixed, and later its spend-auth signature (`add_spend_signatures`). The -// client + HSM sign the transparent inputs over the ZIP-244 sighash this module exposes, the -// signed PSBT goes to the external proof service for the Halo2 `zkproof`, and +// signers sign the transparent inputs over the ZIP-244 sighash this module exposes, the +// signed PSBT goes to an external prover for the Halo2 `zkproof`, and // `combine_ironwood_proof` finalizes the transparent inputs and splices in the proof + shielded // bundle to produce the broadcast-ready v6 transaction. See `crate::zcash::ironwood_build` for // the PCZT role bridge. @@ -482,7 +482,7 @@ impl ZcashBitGoPsbt { )) } - /// Ingest a transparent-input signature returned by the client/HSM into `partial_sigs`, after + /// Ingest a transparent-input signature returned by a signer into `partial_sigs`, after /// verifying it against [`Self::v6_transparent_sighash`] for that input. `sig` is a DER ECDSA /// signature with the trailing SIGHASH_ALL byte (as it appears in a scriptSig). pub fn add_v6_transparent_signature( @@ -766,7 +766,7 @@ impl ZcashBitGoPsbt { /// Deserialize a v6 PSBT that may not yet carry its Ironwood PCZT — for tooling (e.g. the /// CLI) that builds the transparent skeleton (inputs/outputs) across separate /// serialize/deserialize round-trips before calling [`Self::add_ironwood_output`], unlike the - /// in-process microservice flow [`Self::deserialize_v6`] otherwise assumes. Still validates + /// in-process flow [`Self::deserialize_v6`] otherwise assumes. Still validates /// the v6 params and consensus branch id, so a bare transparent skeleton round-trips exactly /// like a fully-shielded one. pub fn deserialize_v6_pre_shield( diff --git a/packages/wasm-utxo/src/inspect/zcash.rs b/packages/wasm-utxo/src/inspect/zcash.rs index 236484b0539..f8ba98e4eee 100644 --- a/packages/wasm-utxo/src/inspect/zcash.rs +++ b/packages/wasm-utxo/src/inspect/zcash.rs @@ -152,7 +152,7 @@ fn ironwood_v6_psbt_to_node(zcash_psbt: &ZcashBitGoPsbt, network: Network) -> No psbt_node.add_child(psbt_outputs_to_node(&psbt.outputs)); // Overall signing state of the transparent side, since a v6 PSBT is never "fully signed" on - // the shielded side until the external proof service and `combine_ironwood_proof` run (that + // the shielded side until the external prover and `combine_ironwood_proof` run (that // step produces a broadcast-ready transaction, not another PSBT). psbt_node.add_child(Node::new( "transparent_signing_state", diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet/zcash_psbt.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet/zcash_psbt.rs index 6d9c9358200..ad54609a83b 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/zcash_psbt.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/zcash_psbt.rs @@ -13,6 +13,10 @@ use super::fee_policy_from_js; use super::parse_network; use super::BitGoPsbt; use crate::error::WasmUtxoError; +use crate::fixed_script_wallet::bitgo_psbt::zcash::error::IronwoodPsbtError; +use crate::fixed_script_wallet::bitgo_psbt::zcash::ironwood_spend::{ + IronwoodInputKind, IronwoodShieldedStatus, +}; use crate::fixed_script_wallet::bitgo_psbt::zcash::parse::{ ZcashParsedInput, ZcashParsedInputs, ZcashParsedOutput, ZcashParsedTransaction, ZcashShieldedSpend, @@ -20,15 +24,17 @@ use crate::fixed_script_wallet::bitgo_psbt::zcash::parse::{ use crate::fixed_script_wallet::bitgo_psbt::zcash::IronwoodOutputRequest; use crate::wasm::bip32::WasmBIP32; use crate::wasm::ecpair::WasmECPair; -use crate::wasm::try_from_js_value::Bytes; -use crate::wasm::try_from_js_value::TryFromJsValue; +use crate::wasm::try_from_js_value::{ + get_field, get_required_raw_field, get_required_u64_field, TryFromJsValue, +}; use crate::wasm::try_into_js_value::js_obj; use crate::wasm::try_into_js_value::TryIntoJsValue; use crate::wasm::wallet_keys::WasmRootWalletKeys; use crate::zcash::ironwood_build::{ - AnchorBytes, MemoBytes, OrchardAddressBytes, OvkBytes, ANCHOR_SIZE, MEMO_SIZE, - ORCHARD_ADDRESS_SIZE, OVK_SIZE, + AnchorBytes, IronwoodActionMap, IronwoodWitness, MemoBytes, OrchardAddressBytes, OvkBytes, + WitnessAuthPath, IRONWOOD_MERKLE_DEPTH, }; +use crate::zcash::ironwood_construct::IronwoodKeylessSpendSpec; impl BitGoPsbt { /// Borrow the inner `ZcashBitGoPsbt`, erroring if this is not a Zcash PSBT. @@ -261,17 +267,10 @@ impl BitGoPsbt { memo: &[u8], unified_address: Option, ) -> Result<(), WasmUtxoError> { - /// Length-check a boundary byte string, naming the field and its expected size in the error. - fn fixed(bytes: &[u8], field: &str) -> Result<[u8; N], WasmUtxoError> { - bytes.try_into().map_err(|_| { - WasmUtxoError::new(&format!("{field} must be {N} bytes, got {}", bytes.len())) - }) - } - - let recipient: OrchardAddressBytes = fixed::(recipient, "recipient")?; - let anchor: AnchorBytes = fixed::(anchor, "anchor")?; - let memo: MemoBytes = fixed::(memo, "memo")?; - let ovk: Option = ovk.map(|v| fixed::(&v, "ovk")).transpose()?; + let recipient: OrchardAddressBytes = fixed_bytes(recipient, "recipient")?; + let anchor: AnchorBytes = fixed_bytes(anchor, "anchor")?; + let memo: MemoBytes = fixed_bytes(memo, "memo")?; + let ovk: Option = ovk.map(|v| fixed_bytes(&v, "ovk")).transpose()?; self.zcash_mut()? .add_ironwood_output( &recipient, @@ -304,12 +303,7 @@ impl BitGoPsbt { outputs: JsValue, anchor: &[u8], ) -> Result, WasmUtxoError> { - let anchor: AnchorBytes = anchor.try_into().map_err(|_| { - WasmUtxoError::new(&format!( - "anchor must be {ANCHOR_SIZE} bytes, got {}", - anchor.len() - )) - })?; + let anchor: AnchorBytes = fixed_bytes(anchor, "anchor")?; let arr = js_sys::Array::from(&outputs); let requests = arr @@ -456,7 +450,7 @@ impl BitGoPsbt { /// A PCZT is the shielded-bundle counterpart to a PSBT: it accumulates the orchard action /// (spend + output), the ZIP-244-committed `out_ciphertext`, and — once /// [`Self::combine_ironwood_proof`] has run — the Halo2 `zkproof` and binding signature, - /// bridging this PSBT to the external proof service and back (see + /// bridging this PSBT to an external prover and back (see /// [`crate::zcash::ironwood_pczt`] / [`crate::zcash::ironwood_build`]). /// /// Present only after [`Self::add_ironwood_output`] has been called on a v6 PSBT; `undefined` @@ -632,66 +626,320 @@ impl BitGoPsbt { )) } } - // ============================================================================= -// IronwoodOutputRequest: one requested multi-recipient Ironwood shielded output +// Zcash v6 (Ironwood) shielded spends: the build → ciphertexts → sign → prove flow // ============================================================================= -impl TryFromJsValue for IronwoodOutputRequest { +#[wasm_bindgen] +impl BitGoPsbt { + /// Constructor: add the shielded spends (and optionally shielded outputs) as a single + /// **keyless** orchard PCZT stored in the PSBT — the deshield and shielded→shielded + /// Constructor for a caller that holds no viewing keys. + /// + /// * `spends` - one entry per spent note: `{ recipient: Uint8Array, value: bigint, + /// rho: Uint8Array, rseed: Uint8Array, cmx: Uint8Array, witness: { position: number, + /// authPath: Uint8Array }, nullifier: Uint8Array }` — the note's plaintext, its Merkle + /// witness (`authPath` is the 32 sibling hashes flattened, as `IronwoodWitness.auth_path` + /// returns it; validated against the anchor at construction), and the nullifier (from + /// the note store or derived by the viewing-key holder) + /// * `outputs` - zero or more requested shielded outputs, shaped exactly like + /// `add_ironwood_outputs`' entries; every output's `ovk` must be omitted (encryption + /// belongs to the viewing-key holder in the spend flow) and its amount must be > 0 + /// * `anchor` - the Ironwood note-commitment-tree root every spend's witness commits to + /// (32 bytes) + /// + /// One input kind: rejected when the PSBT already carries transparent inputs, and one + /// call per PSBT. Every real spend carries the placeholder `rk` and no `alpha`; the + /// signer installs the randomized `rk` (`add_ironwood_spend_randomizers`) before any + /// sighash is computed. Every output's `out_ciphertext` is a random placeholder, + /// replaced under the FVK's `ovk`. No key material is accepted or stored. + /// + /// Returns `{ spendActionIndices: number[], outputActionIndices: number[] }` — the action + /// index each requested spend/output landed at. The actions are shuffled, so request + /// order does not equal action order: any caller correlating per-action data (a stored + /// Unified Address, an out-ciphertext) must go through these indices. + pub fn add_ironwood_shielded_spends( + &mut self, + spends: JsValue, + outputs: JsValue, + anchor: &[u8], + ) -> Result { + let anchor: AnchorBytes = fixed_bytes(anchor, "anchor")?; + let spends = js_sys::Array::from(&spends) + .iter() + .map(|item| IronwoodKeylessSpendSpec::try_from_js_value(&item)) + .collect::, _>>()?; + let outputs = js_sys::Array::from(&outputs) + .iter() + .map(|item| IronwoodOutputRequest::try_from_js_value(&item)) + .collect::, _>>()?; + self.zcash_mut()? + .add_shielded_spends(&spends, &outputs, &anchor, rand::rngs::OsRng) + .map_err(WasmUtxoError::from)? + .try_to_js_value() + } + + /// The out-ciphertext request: `{ pczt: Uint8Array, actionIndices: number[] }` — the raw + /// PCZT bytes plus the real outputs still pending their `out_ciphertext`. The viewing-key + /// holder re-encrypts each pending output under its FVK `ovk` and returns one 80-byte + /// ciphertext per action index, applied with `add_ironwood_out_ciphertexts`. + /// + /// Throws when the PSBT carries no spend bundle (`IronwoodPsbtError.NotSpendBundle`) or + /// nothing is pending (`IronwoodPsbtError.NothingPending`, e.g. a pure deshield). + pub fn ironwood_out_ciphertext_request(&self) -> Result { + let (pczt, action_indices) = self + .zcash()? + .out_ciphertext_request() + .map_err(WasmUtxoError::from)?; + js_obj!( + "pczt" => pczt, + "actionIndices" => action_indices, + ) + } + + /// Apply the out-ciphertext response: an array of + /// `{ actionIndex: number, outCiphertext: Uint8Array }` — one per pending real output. + /// **Atomic**: every entry is validated before anything is written, so one bad entry + /// stores nothing. Idempotent for identical bytes; different bytes for an already-set + /// index throw (`IronwoodPsbtError.OutCiphertextConflict`). Rejected once any signature + /// exists (`IronwoodPsbtError.AlreadySigned`). + pub fn add_ironwood_out_ciphertexts(&mut self, entries: JsValue) -> Result<(), WasmUtxoError> { + let entries: Vec = serde_wasm_bindgen::from_value(entries) + .map_err(|e| WasmUtxoError::new(&format!("invalid out-ciphertext entries: {e}")))?; + let entries = entries + .into_iter() + .map(|entry| { + Ok(( + entry.action_index, + fixed_bytes(&entry.out_ciphertext, "outCiphertext")?, + )) + }) + .collect::, IronwoodPsbtError>>()?; + self.zcash_mut()? + .add_out_ciphertexts(&entries) + .map_err(WasmUtxoError::from) + } + + /// Install the signer's randomized `rk`s: an array of + /// `{ actionIndex: number, alpha: Uint8Array, rk: Uint8Array }` — one per real spend, + /// with `rk = ak.randomize(alpha)`. **Only `rk` is stored**: the PSBT never carries + /// `alpha` or the wallet's `ak` (with `rk`, an `alpha` would reveal `ak`). + /// + /// `spendValidatingKey` (the wallet's 32-byte `ak`, optional) makes the installer check + /// `rk == ak.randomize(alpha)` before storing; without it the pair is checked later, by + /// the client's key-free verifier and by the keyed prover. **Atomic**, and rejected once + /// any signature exists (`IronwoodPsbtError.AlreadySigned`). + pub fn add_ironwood_spend_randomizers( + &mut self, + entries: JsValue, + spend_validating_key: Option>, + ) -> Result<(), WasmUtxoError> { + let entries: Vec = serde_wasm_bindgen::from_value(entries) + .map_err(|e| WasmUtxoError::new(&format!("invalid spend randomizer entries: {e}")))?; + let ak: Option<[u8; 32]> = spend_validating_key + .map(|bytes| fixed_bytes(&bytes, "spendValidatingKey")) + .transpose()?; + let entries = entries + .into_iter() + .map(|entry| { + Ok(( + entry.action_index, + fixed_bytes(&entry.alpha, "alpha")?, + fixed_bytes(&entry.rk, "rk")?, + )) + }) + .collect::, IronwoodPsbtError>>()?; + self.zcash_mut()? + .add_spend_randomizers(&entries, ak.as_ref()) + .map_err(WasmUtxoError::from) + } + + /// The ZIP-244 sig digest (32 bytes) the real spends' spend-auth signatures are computed + /// over. Every real output's `out_ciphertext` and every real spend's randomized `rk` + /// must be installed first — the digest commits both. + pub fn ironwood_spend_sighash(&self) -> Result, WasmUtxoError> { + self.zcash()? + .spend_sighash() + .map(|digest| digest.to_vec()) + .map_err(WasmUtxoError::from) + } + + /// Apply the signer's spend-auth signatures: an array of + /// `{ actionIndex: number, signature: Uint8Array }` — one per real spend, each a 64-byte + /// RedPallas signature over `ironwood_spend_sighash`, verified against the spend's + /// installed `rk` before it is stored. **Atomic.** + pub fn add_ironwood_spend_signatures(&mut self, entries: JsValue) -> Result<(), WasmUtxoError> { + let entries: Vec = serde_wasm_bindgen::from_value(entries) + .map_err(|e| WasmUtxoError::new(&format!("invalid spend signature entries: {e}")))?; + let entries = entries + .into_iter() + .map(|entry| { + Ok(( + entry.action_index, + fixed_bytes(&entry.signature, "signature")?, + )) + }) + .collect::, IronwoodPsbtError>>()?; + self.zcash_mut()? + .add_spend_signatures(&entries) + .map_err(WasmUtxoError::from) + } + + /// The proof request for the keyed prover: the raw PCZT bytes with each real spend's + /// randomizer `alpha` patched in — `randomizers` is an array of + /// `{ actionIndex: number, alpha: Uint8Array }`, exactly one per real spend. The alphas + /// are written only to the returned bytes, never stored (with `rk`, an `alpha` would + /// reveal the wallet's `ak`). The proof the prover returns goes + /// straight to `combine_ironwood_proof`. + /// + /// Gated: every real output's `out_ciphertext`, every real spend's randomized `rk`, and + /// every real spend's spend-auth signature must be installed first. A shielding bundle + /// (dummy spends only) takes an empty array and is served as-is — but the shielding flow + /// hands the prover `getPczt()` bytes instead. + pub fn ironwood_proof_request(&self, randomizers: JsValue) -> Result, WasmUtxoError> { + let randomizers: Vec = serde_wasm_bindgen::from_value(randomizers) + .map_err(|e| WasmUtxoError::new(&format!("invalid proof request randomizers: {e}")))?; + let alphas = randomizers + .into_iter() + .map(|entry| Ok((entry.action_index, fixed_bytes(&entry.alpha, "alpha")?))) + .collect::, IronwoodPsbtError>>()?; + self.zcash()? + .proof_request(&alphas) + .map_err(WasmUtxoError::from) + } + + /// The orchestration view of the shielded workflow, as + /// `{ inputKind: "transparent" | "shielded", pendingOutCiphertexts: number[], + /// pendingRandomizers: number[], pendingSpendSignatures: number[], readyForProof: + /// boolean }` — `readyForProof` means `ironwood_proof_request(randomizers)` succeeds and + /// `combine_ironwood_proof` then needs only the proof. + pub fn ironwood_shielded_status(&self) -> Result { + self.zcash()? + .shielded_status() + .map_err(WasmUtxoError::from)? + .try_to_js_value() + } +} + +/// Length-check a boundary byte string, naming the field and its expected size in the error. +fn fixed_bytes( + bytes: &[u8], + field: &'static str, +) -> Result<[u8; N], IronwoodPsbtError> { + bytes.try_into().map_err(|_| IronwoodPsbtError::BadLength { + field, + expected: N, + actual: bytes.len(), + }) +} + +/// Read a required fixed-size byte field off a JS object. +fn required_fixed_bytes( + item: &JsValue, + field: &'static str, +) -> Result<[u8; N], WasmUtxoError> { + let bytes = Vec::::try_from_js_value(&get_required_raw_field(item, field)?)?; + Ok(fixed_bytes(&bytes, field)?) +} + +/// One `add_ironwood_out_ciphertexts` entry from JS: `{ actionIndex, outCiphertext }`. +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct OutCiphertextEntry { + action_index: usize, + out_ciphertext: serde_bytes::ByteBuf, +} + +/// One `add_ironwood_spend_randomizers` entry from JS: `{ actionIndex, alpha, rk }`. +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SpendRandomizerEntry { + action_index: usize, + alpha: serde_bytes::ByteBuf, + rk: serde_bytes::ByteBuf, +} + +/// One `add_ironwood_spend_signatures` entry from JS: `{ actionIndex, signature }`. +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct SpendSignatureEntry { + action_index: usize, + signature: serde_bytes::ByteBuf, +} + +/// One `ironwood_proof_request` randomizer from JS: `{ actionIndex, alpha }`. +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct ProofRequestAlpha { + action_index: usize, + alpha: serde_bytes::ByteBuf, +} + +/// One shielded note to spend, as passed to `add_ironwood_shielded_spends`: the note's +/// plaintext, its Merkle witness (`{ position, authPath }`, re-validated against the bundle's +/// anchor at construction), and the nullifier. +impl TryFromJsValue for IronwoodKeylessSpendSpec { fn try_from_js_value(item: &JsValue) -> Result { - let recipient_val = js_sys::Reflect::get(item, &"recipient".into()) - .map_err(|_| WasmUtxoError::new("Missing 'recipient' field on Ironwood output"))?; - if recipient_val.is_undefined() { - return Err(WasmUtxoError::new( - "Missing 'recipient' field on Ironwood output", - )); - } - let recipient: OrchardAddressBytes = Bytes::<43>::try_from_js_value(&recipient_val)?.into(); - - let amount_val = js_sys::Reflect::get(item, &"amount".into()) - .map_err(|_| WasmUtxoError::new("Missing 'amount' field on Ironwood output"))?; - if amount_val.is_undefined() { - return Err(WasmUtxoError::new( - "Missing 'amount' field on Ironwood output", - )); + let witness = get_required_raw_field(item, "witness")?; + let auth_path: [u8; IRONWOOD_MERKLE_DEPTH * 32] = + required_fixed_bytes(&witness, "authPath")?; + let mut path: WitnessAuthPath = [[0u8; 32]; IRONWOOD_MERKLE_DEPTH]; + for (node, chunk) in path.iter_mut().zip(auth_path.chunks_exact(32)) { + node.copy_from_slice(chunk); } - if !amount_val.is_bigint() { - return Err(WasmUtxoError::new("'amount' must be a bigint")); - } - let amount = u64::try_from(js_sys::BigInt::unchecked_from_js(amount_val)) - .map_err(|_| WasmUtxoError::new("'amount' must be a bigint convertible to u64"))?; - - let memo_val = js_sys::Reflect::get(item, &"memo".into()) - .map_err(|_| WasmUtxoError::new("Missing 'memo' field on Ironwood output"))?; - if memo_val.is_undefined() { - return Err(WasmUtxoError::new( - "Missing 'memo' field on Ironwood output", - )); - } - let memo: MemoBytes = Bytes::<512>::try_from_js_value(&memo_val)?.into(); + Ok(Self { + recipient: required_fixed_bytes(item, "recipient")?, + value: get_required_u64_field(item, "value")?, + rho: required_fixed_bytes(item, "rho")?, + rseed: required_fixed_bytes(item, "rseed")?, + cmx: required_fixed_bytes(item, "cmx")?, + witness: IronwoodWitness { + position: get_field(&witness, "position")?, + auth_path: path, + }, + nullifier: required_fixed_bytes(item, "nullifier")?, + }) + } +} - let ovk_val = js_sys::Reflect::get(item, &"ovk".into()).unwrap_or(JsValue::UNDEFINED); - let ovk: Option = if ovk_val.is_undefined() || ovk_val.is_null() { - None - } else { - Some(Bytes::<32>::try_from_js_value(&ovk_val)?.into()) - }; +impl TryIntoJsValue for IronwoodActionMap { + fn try_to_js_value(&self) -> Result { + js_obj!( + "spendActionIndices" => self.spend_action_indices().to_vec(), + "outputActionIndices" => self.output_action_indices().to_vec(), + ) + } +} - let unified_address_val = - js_sys::Reflect::get(item, &"unifiedAddress".into()).unwrap_or(JsValue::UNDEFINED); - let unified_address = if unified_address_val.is_undefined() || unified_address_val.is_null() - { - None - } else { - Some(String::try_from_js_value(&unified_address_val)?) +impl TryIntoJsValue for IronwoodShieldedStatus { + fn try_to_js_value(&self) -> Result { + let input_kind = match self.input_kind { + IronwoodInputKind::Transparent => "transparent", + IronwoodInputKind::Shielded => "shielded", }; + js_obj!( + "inputKind" => input_kind.to_string(), + "pendingOutCiphertexts" => self.pending_out_ciphertexts.clone(), + "pendingRandomizers" => self.pending_randomizers.clone(), + "pendingSpendSignatures" => self.pending_spend_signatures.clone(), + "readyForProof" => self.ready_for_proof, + ) + } +} +// ============================================================================= +// IronwoodOutputRequest: one requested multi-recipient Ironwood shielded output +// ============================================================================= + +impl TryFromJsValue for IronwoodOutputRequest { + fn try_from_js_value(item: &JsValue) -> Result { + let ovk: Option> = get_field(item, "ovk")?; Ok(IronwoodOutputRequest { - recipient, - amount, - ovk, - memo, - unified_address, + recipient: required_fixed_bytes(item, "recipient")?, + amount: get_required_u64_field(item, "amount")?, + ovk: ovk.map(|ovk| fixed_bytes(&ovk, "ovk")).transpose()?, + memo: required_fixed_bytes(item, "memo")?, + unified_address: get_field(item, "unifiedAddress")?, }) } } diff --git a/packages/wasm-utxo/src/wasm/try_from_js_value.rs b/packages/wasm-utxo/src/wasm/try_from_js_value.rs index 067343fd4d5..bec95e15852 100644 --- a/packages/wasm-utxo/src/wasm/try_from_js_value.rs +++ b/packages/wasm-utxo/src/wasm/try_from_js_value.rs @@ -149,6 +149,25 @@ pub(crate) fn get_nested_field( .map_err(|e| WasmUtxoError::new(&format!("{} (path: {})", e, path))) } +/// Get a required raw field: present and not `undefined` +pub(crate) fn get_required_raw_field(obj: &JsValue, key: &str) -> Result { + let field_value = get_raw_field(obj, key)?; + if field_value.is_undefined() { + return Err(WasmUtxoError::new(&format!("Missing '{}' field", key))); + } + Ok(field_value) +} + +/// Get a required `bigint` field as a `u64` +pub(crate) fn get_required_u64_field(obj: &JsValue, key: &str) -> Result { + let field_value = get_required_raw_field(obj, key)?; + if !field_value.is_bigint() { + return Err(WasmUtxoError::new(&format!("'{}' must be a bigint", key))); + } + u64::try_from(js_sys::BigInt::unchecked_from_js(field_value)) + .map_err(|_| WasmUtxoError::new(&format!("'{}' must be a bigint convertible to u64", key))) +} + // ============================================================================= // TryFromJsValue implementations for domain types // ============================================================================= diff --git a/packages/wasm-utxo/src/wasm/zcash.rs b/packages/wasm-utxo/src/wasm/zcash.rs index 95b379810d7..ab65f17a0f9 100644 --- a/packages/wasm-utxo/src/wasm/zcash.rs +++ b/packages/wasm-utxo/src/wasm/zcash.rs @@ -44,9 +44,8 @@ pub fn zcash_ironwood_version_group_id() -> u32 { } /// A validated Merkle witness for an Ironwood/Orchard note commitment, from -/// [`ironwood_build_witness`]. -/// -/// Not yet installed anywhere — see [`ironwood_build_witness`]'s docs. +/// [`ironwood_build_witness`]. Its `position` and `auth_path` are what +/// `BitGoPsbt::add_ironwood_shielded_spends` takes as a spend's witness. #[wasm_bindgen] pub struct IronwoodWitness { inner: crate::zcash::ironwood_build::IronwoodWitness, @@ -75,8 +74,8 @@ impl IronwoodWitness { /// any input isn't a canonical field element, or if the witness doesn't recompute to `anchor`. /// /// This crate has no chain state, so the raw sibling-hash path must be supplied by the caller -/// (typically BitGo's backend, querying a Zcash-aware service). The returned witness is not yet -/// installed anywhere — hook it into a spend once real-spend construction exists. +/// (typically BitGo's backend, querying a Zcash-aware service). The returned witness's +/// `position` and `auth_path` feed a spend in `BitGoPsbt::add_ironwood_shielded_spends`. #[wasm_bindgen] pub fn ironwood_build_witness( cmx: &[u8], diff --git a/packages/wasm-utxo/src/zcash/ironwood_build.rs b/packages/wasm-utxo/src/zcash/ironwood_build.rs index 1ffd8562316..d0ecafa7a6f 100644 --- a/packages/wasm-utxo/src/zcash/ironwood_build.rs +++ b/packages/wasm-utxo/src/zcash/ironwood_build.rs @@ -256,9 +256,9 @@ pub enum IronwoodBuildError { /// for it. NoSpends, /// An output passed to the keyless spend constructor carried an explicit `ovk`: in the - /// spend flow KMS owns encryption, so the PSBT side may not choose - /// one. The placeholder `out_ciphertext` the constructor installs is replaced by KMS's - /// `out_ciphertext` later. + /// spend flow encryption belongs to the viewing-key holder, so the PSBT side may not + /// choose one. The placeholder `out_ciphertext` the constructor installs is replaced by + /// the real `out_ciphertext` later. OvkNotAllowed { /// The offending output's position in the `outputs` slice. output_index: usize, @@ -524,7 +524,7 @@ impl core::fmt::Display for IronwoodBuildError { Self::OvkNotAllowed { output_index } => write!( f, "ironwood-build: output {output_index} carries an explicit ovk; the keyless \ - spend constructor leaves encryption to the KMS" + spend constructor leaves encryption to the viewing-key holder" ), Self::BadSpendValidatingKey => write!( f, @@ -598,7 +598,7 @@ pub struct IronwoodOutputSpec { /// /// The keyed (FVK-carrying) spend spec — the **test oracle** for the keyless constructor /// (`ironwood_construct::construct_spend_pczt_keyless`). Production builds never see the FVK: -/// the build service is keyless, and the shipped WASM must not carry this path. +/// the PSBT is built keyless, and the shipped WASM must not carry this path. #[cfg(any(test, feature = "orchard-proving"))] #[derive(Clone, Copy, Debug)] pub struct IronwoodSpendSpec { @@ -2839,7 +2839,7 @@ mod tests { )); } - /// Server-side (HSM) validation: ECDH agreement is symmetric, so the server — holding + /// Server-side validation: ECDH agreement is symmetric, so the server — holding /// `bitgo_privkey` — can derive the *same* `ovk` from the other side of the key pair /// (`ECDH(bitgo_privkey, user_pubkey)`, vs. the client's `ECDH(bitgo_pubkey, user_privkey)`) /// without ever learning `user_privkey` or the `ovk` itself over the wire. It then uses that diff --git a/packages/wasm-utxo/src/zcash/ironwood_construct.rs b/packages/wasm-utxo/src/zcash/ironwood_construct.rs index 16d0afc6a99..463a2cd3bb3 100644 --- a/packages/wasm-utxo/src/zcash/ironwood_construct.rs +++ b/packages/wasm-utxo/src/zcash/ironwood_construct.rs @@ -3,11 +3,11 @@ //! //! The keyed constructor ([`construct_spend_pczt_multi`]) needs each spend's FVK — the orchard //! [`Builder`](orchard::builder::Builder) derives the nullifier and checks note ownership with -//! it — but the build service holds no keys. This module assembles the same actions field by +//! it — but the builder holds no keys. This module assembles the same actions field by //! field instead: //! //! - each real spend carries its **note plaintext** (`recipient`/`value`/`rho`/`rseed`/`cmx`), a -//! **witness** and a caller-supplied **nullifier** (from the note store, or from KMS); +//! **witness** and a caller-supplied **nullifier** (from the note store, or derived by the viewing-key holder); //! - each real output carries its recipient, value and memo; its note (and therefore //! `cmx`/`enc_ciphertext`) is fabricated here, keyed to the paired spend's nullifier; //! - every action's `rcv`/`cv_net` pair is sampled here, and the bundle's committed @@ -17,7 +17,7 @@ //! - no FVK ever enters the constructed bundle: real spends carry `fvk = None`, so the wire //! form stays keyless by construction; //! - each output's `out_ciphertext` is a random **placeholder** (encrypted under `ovk = ⊥`): -//! KMS re-encrypts it under its FVK `ovk` and splices the real one in via +//! the viewing-key holder re-encrypts it under its FVK `ovk` and splices the real one in via //! [`with_out_ciphertext`](super::ironwood_pczt::with_out_ciphertext). //! //! Surplus outputs (more outputs than spends) are fabricated as **dummy-spend** actions by an @@ -60,12 +60,12 @@ const MAX_MONEY: u64 = 21_000_000 * 100_000_000; /// One Ironwood note to spend, as passed to [`construct_spend_pczt_keyless`]. /// -/// Everything here is the note's plaintext from the wallet's own note record (or a KMS -/// nullifier response): the reconstructed note must commit to `cmx`, and the witness must +/// Everything here is the note's plaintext from the wallet's own note record (or a +/// derived nullifier): the reconstructed note must commit to `cmx`, and the witness must /// recompute to the bundle's anchor. The **nullifier is not verified here** — that needs the -/// FVK, which the build service does not hold. A wrong nullifier only produces an invalid -/// transaction: the proof fails, and the KMS worker (which re-derives every nullifier) rejects -/// the bundle before proving. +/// FVK, which the builder does not hold. A wrong nullifier only produces an invalid +/// transaction: the proof fails, and a prover that re-derives every nullifier rejects the +/// bundle before proving. #[derive(Clone, Copy, Debug)] pub struct IronwoodKeylessSpendSpec { /// The spent note's recipient: 43-byte raw Orchard/Ironwood address. @@ -81,7 +81,7 @@ pub struct IronwoodKeylessSpendSpec { pub cmx: [u8; 32], /// Merkle witness for `cmx` at the bundle's anchor. pub witness: IronwoodWitness, - /// The nullifier this spend reveals, from the note store or from KMS. Installed verbatim: + /// The nullifier this spend reveals, from the note store or the viewing-key holder. Installed verbatim: /// a wrong value fails the proof, never misdirects value. pub nullifier: [u8; 32], } @@ -112,8 +112,8 @@ struct ActionOrigin { /// (replaced by the signer's randomized `rk` later), and real spends carry no FVK, `ak` or /// `alpha`. /// -/// Every output must have `ovk: None` and a positive amount: KMS owns encryption in the spend -/// flow, and a zero-valued output is how a bundle marks fabricated dummies. +/// Every output must have `ovk: None` and a positive amount: encryption belongs to the +/// viewing-key holder in the spend flow, and a zero-valued output is how a bundle marks fabricated dummies. /// /// The returned [`IronwoodActionMap`] gives, for each `spends[i]`/`outputs[i]`, the action /// index it landed at in the bundle — the actions are shuffled, so request order does not @@ -140,7 +140,7 @@ pub fn construct_spend_pczt_keyless( } let mut recipients = Vec::with_capacity(outputs.len()); for (output_index, spec) in outputs.iter().enumerate() { - // KMS owns encryption in the spend flow — no caller-chosen `ovk`. + // Encryption belongs to the viewing-key holder in the spend flow — no caller-chosen `ovk`. if spec.ovk.is_some() { return Err(IronwoodBuildError::OvkNotAllowed { output_index }); } @@ -401,7 +401,7 @@ fn real_output_wire( /// Encrypt an output note: the real `enc_ciphertext` (recoverable by whoever holds the /// recipient's `ivk`) and the ephemeral key it rode in on, plus a placeholder `out_ciphertext` -/// under `ovk = ⊥` — random bytes, replaced by KMS's real one later. +/// under `ovk = ⊥` — random bytes, replaced by the real one later. fn encrypt_output( note: Note, memo: &MemoBytes, @@ -609,9 +609,9 @@ pub(crate) mod test_util { use crate::zcash::ironwood_build::test_util::{real_two_leaf_tree, RealLeaf}; /// Two real notes (100M and 80M zatoshi) of one wallet under a real two-leaf tree, as the - /// keyless Constructor receives them (nullifiers as the note store / KMS would report + /// keyless Constructor receives them (nullifiers as the note store would report /// them), plus the anchor, the wallet's FVK (the test oracle's key holder, standing in for - /// KMS) and its spend-validating key `ak` (the client's key, never passed to the + /// the viewing-key holder) and its spend-validating key `ak` (the client's key, never passed to the /// Constructor). pub(crate) fn keyless_fixture() -> ( Vec, @@ -683,7 +683,7 @@ mod tests { } /// Patch every requested output's `out_ciphertext` to the real encryption under `ovk` — - /// what KMS does in the production flow. + /// what the viewing-key holder does in the production flow. fn patch_out_ciphertexts( bundle: &PcztBundle, output_indices: &[usize], @@ -698,7 +698,7 @@ mod tests { } /// The oracle round trip: a bundle built by the keyless Constructor - /// behaves exactly like one built by the keyed oracle — KMS's `out_ciphertext`s splice in, + /// behaves exactly like one built by the keyed oracle — the real `out_ciphertext`s splice in, /// the FROST randomizers install, the keyed verifier and the key-free verifier both pass, /// and the FROST stand-in signatures verify. fn oracle_round_trip(spends: usize, outputs: usize) -> (PcztBundle, IronwoodActionMap) { @@ -712,7 +712,7 @@ mod tests { // Shape: UNPADDED means exactly max(S, O) actions. assert_eq!(bundle.actions().len(), spends.max(outputs)); - // KMS's out_ciphertext round: every requested output gets its real ciphertext. + // The out_ciphertext round: every requested output gets its real ciphertext. let ovk = *fvk.to_ovk(Scope::External).as_ref(); let bundle = patch_out_ciphertexts(&bundle, map.output_action_indices(), ovk); diff --git a/packages/wasm-utxo/src/zcash/ironwood_pczt.rs b/packages/wasm-utxo/src/zcash/ironwood_pczt.rs index 21b0f3b905e..167aab496d0 100644 --- a/packages/wasm-utxo/src/zcash/ironwood_pczt.rs +++ b/packages/wasm-utxo/src/zcash/ironwood_pczt.rs @@ -1,13 +1,12 @@ //! Serialization of an `orchard` **PCZT** (Partially Constructed Zcash Transaction) Ironwood -//! bundle — the witness payload exchanged with the external Ironwood proof service and carried +//! bundle — the witness payload exchanged with an external Ironwood prover and carried //! through the PSBT. //! //! `orchard::pczt::Bundle` is not itself serde-serializable, so this module bridges it to a //! compact wire form: a 1-byte [`FORMAT_VERSION`] followed by a [`postcard`]-encoded *mirror* //! struct whose fields are exactly what `orchard`'s `Bundle/Action/Spend/Output::parse(...)` //! consume. Serialize reads the bundle's public getters; deserialize reconstructs it via -//! `parse(...)`. Both `wasm-utxo` (build/combine) and the proof service (prove) use this format; -//! see `docs/ironwood-proof-service-contract.md`. +//! `parse(...)`. Both `wasm-utxo` (build/combine) and the external prover (prove) use this format. //! //! Fields orchard exposes only as `>32`-byte arrays are stored as `Vec` (serde derives arrays //! only up to length 32) and length-checked on the way back in. `zip32_derivation` is not a diff --git a/packages/wasm-utxo/src/zcash/ironwood_verify.rs b/packages/wasm-utxo/src/zcash/ironwood_verify.rs index d412c406191..573d4527cb9 100644 --- a/packages/wasm-utxo/src/zcash/ironwood_verify.rs +++ b/packages/wasm-utxo/src/zcash/ironwood_verify.rs @@ -1,6 +1,6 @@ //! The key-free verifier for Ironwood (Zcash v6) PCZT bundles. //! -//! A party without viewing keys — the client co-signing, or a build service — cannot check +//! A party without viewing keys — the client co-signing, or a keyless builder — cannot check //! nullifier ownership or decrypt anything, but it CAN check that the transaction it is //! asked to sign moves exactly the values it was shown: //! @@ -153,7 +153,7 @@ pub fn verify_bundle_without_keys( let output = action.output(); let is_real_spend = spend.dummy_sk().is_none(); - // Every action keeps its full plaintext — the prover (KMS) needs it to build the + // Every action keeps its full plaintext — the prover needs it to build the // proof, and it is what lets anyone reading the PSBT see exactly what moves where. // A missing field could otherwise hide a value from the checks below. let (spend_value, output_value, output_recipient) = action_plaintext(action_index, action)?; diff --git a/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts b/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts index 9524f9a86c3..dba4ea11b36 100644 --- a/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts +++ b/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts @@ -754,7 +754,7 @@ describe("ZcashIronwoodBitGoPsbt v6 (Ironwood)", function () { it("re-encrypts out_ciphertext, changing the sighash and txid, without touching anything else", function () { // Server: build the keyless PSBT (`addShieldedOutput` with no `ovk`) and hand off the bytes, - // matching the microservice build/serve flow — round-tripping through bytes rather than + // matching a build-then-hand-off flow — round-tripping through bytes rather than // reusing the in-memory object, so this exercises exactly what a real client receives. const server = buildShieldPsbt(); const txidBefore = server.getId(); diff --git a/packages/wasm-utxo/test/fixedScript/zcashIronwoodSpend.ts b/packages/wasm-utxo/test/fixedScript/zcashIronwoodSpend.ts new file mode 100644 index 00000000000..70472a8d7c1 --- /dev/null +++ b/packages/wasm-utxo/test/fixedScript/zcashIronwoodSpend.ts @@ -0,0 +1,577 @@ +import assert from "node:assert"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, it } from "mocha"; + +import { + ZcashIronwoodBitGoPsbt, + type CreateEmptyIronwoodOptions, +} from "../../js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.js"; +import { ZcashIronwoodWitness } from "../../js/fixedScriptWallet/ZcashIronwoodWitness.js"; +import { ZcashV6Transaction } from "../../js/fixedScriptWallet/ZcashV6Transaction.js"; +import { getWalletKeysForSeed } from "../../js/testutils/index.js"; +import type { + IronwoodShieldedStatus, + ZcashShieldedOutputRequest, + ZcashShieldedSpendRequest, +} from "../../js/fixedScriptWallet/ZcashIronwoodTypes.js"; + +/** + * One `test/fixtures/zcash/ironwood_spend_*.json` fixture, generated in Rust by the + * `#[ignore]`d `generate_ts_spend_fixtures` test (under `orchard-proving`): + * + * - `request` is what the builder replays through `addShieldedSpends`; + * - `psbt` is the seeded post-construction PSBT the lifecycle drives; + * - `outCiphertexts` / `proof` are the viewing-key holder's and prover's responses (they need + * the FVK / Halo2, which the TS + * side does not hold); + * - `randomizers` / `signatures` are the FROST stand-in round, keyed to the same run's + * ciphertexts (the sighash chain depends on them); + * - `proofRequest` is the exact alpha-patched PCZT `getProofRequest` must return. + */ +type IronwoodSpendFixture = { + name: string; + network: "zcashTest"; + blockHeight: number; + request: { + anchor: string; + spends: Array<{ + recipient: string; + value: string; + rho: string; + rseed: string; + cmx: string; + witness: { position: number; authPath: string[] }; + nullifier: string; + }>; + outputs: Array<{ + recipient: string; + amount: string; + memo: string; + unifiedAddress: string | null; + }>; + transparentOutput: { script: string; value: string } | null; + }; + psbt: string; + spendActionIndices: number[]; + outputActionIndices: number[]; + ak: string; + outCiphertexts: Array<{ actionIndex: number; outCiphertext: string }>; + randomizers: Array<{ actionIndex: number; alpha: string; rk: string }>; + sighash: string; + signatures: Array<{ actionIndex: number; signature: string }>; + proofRequest: string; + proof: string; + expectedTxid: string; + zip317ConventionalFee: string; +}; + +const hex = (value: string): Uint8Array => Buffer.from(value, "hex"); +const base64 = (value: string): Uint8Array => Buffer.from(value, "base64"); + +const fixtureDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../fixtures/zcash"); + +function loadFixture(name: string): IronwoodSpendFixture { + return JSON.parse( + fs.readFileSync(path.join(fixtureDir, `ironwood_spend_${name}.json`), "utf8"), + ) as IronwoodSpendFixture; +} + +/** The fixture's spend request, with witnesses rebuilt (and re-validated) from the raw path. */ +function spendRequests(fixture: IronwoodSpendFixture): ZcashShieldedSpendRequest[] { + return fixture.request.spends.map((spend) => ({ + recipient: hex(spend.recipient), + value: BigInt(spend.value), + rho: hex(spend.rho), + rseed: hex(spend.rseed), + cmx: hex(spend.cmx), + witness: ZcashIronwoodWitness.build( + hex(spend.cmx), + spend.witness.position, + spend.witness.authPath.map(hex), + hex(fixture.request.anchor), + ), + nullifier: hex(spend.nullifier), + })); +} + +function outputRequests(fixture: IronwoodSpendFixture): ZcashShieldedOutputRequest[] { + return fixture.request.outputs.map((output) => ({ + recipient: hex(output.recipient), + amount: BigInt(output.amount), + memo: hex(output.memo), + ...(output.unifiedAddress ? { unifiedAddress: output.unifiedAddress } : {}), + })); +} + +/** A fresh PSBT at the fixture's post-construction state (the deterministic seeded build). */ +function freshPsbt(fixture: IronwoodSpendFixture): ZcashIronwoodBitGoPsbt { + return ZcashIronwoodBitGoPsbt.fromBytes(base64(fixture.psbt), "zcashTest"); +} + +/** `serialize` → `fromBytes`: the wire round-trip every lifecycle step must survive. */ +function roundtrip(psbt: ZcashIronwoodBitGoPsbt): ZcashIronwoodBitGoPsbt { + return ZcashIronwoodBitGoPsbt.fromBytes(psbt.serialize(), "zcashTest"); +} + +/** Asserts `call` throws with the given typed error code. */ +function assertErrorCode(call: () => unknown, expectedCode: string): void { + try { + call(); + } catch (err) { + assert.strictEqual((err as Error & { code?: string }).code, expectedCode); + return; + } + assert.fail(`expected ${expectedCode}, but the call succeeded`); +} + +/** Whether `needle`'s byte sequence appears anywhere in `haystack`. */ +function containsSequence(haystack: Uint8Array, needle: Uint8Array): boolean { + outer: for (let start = 0; start + needle.length <= haystack.length; start++) { + for (let offset = 0; offset < needle.length; offset++) { + if (haystack[start + offset] !== needle[offset]) { + continue outer; + } + } + return true; + } + return false; +} + +/** Status view with index lists sorted, for order-independent comparison. */ +function normalizedStatus(status: IronwoodShieldedStatus): IronwoodShieldedStatus { + const sort = (indices: number[]) => indices.slice().sort((a, b) => a - b); + return { + ...status, + pendingOutCiphertexts: sort(status.pendingOutCiphertexts), + pendingRandomizers: sort(status.pendingRandomizers), + pendingSpendSignatures: sort(status.pendingSpendSignatures), + }; +} + +/** The expected pre-work status of the fixture's post-construction PSBT. */ +function expectedPending(fixture: IronwoodSpendFixture): { + inputKind: "shielded"; + pendingOutCiphertexts: number[]; + pendingRandomizers: number[]; + pendingSpendSignatures: number[]; + readyForProof: boolean; +} { + const spendIndices = fixture.spendActionIndices.slice().sort((a, b) => a - b); + const outputIndices = fixture.outputActionIndices.slice().sort((a, b) => a - b); + return { + inputKind: "shielded", + pendingOutCiphertexts: outputIndices, + pendingRandomizers: spendIndices, + pendingSpendSignatures: spendIndices, + readyForProof: false, + }; +} + +describe("ZcashIronwoodBitGoPsbt shielded spends", function () { + const walletKeys = getWalletKeysForSeed("ironwood-ts"); + const createOptions = (blockHeight: number): CreateEmptyIronwoodOptions => ({ blockHeight }); + const scriptId = { chain: 0, index: 0 } as const; + const signPath = { signer: "user", cosigner: "bitgo" } as const; + + describe("keyless construction (addShieldedSpends)", function () { + it("constructs a spend bundle from the fixture request and maps the actions", function () { + for (const fixture of [loadFixture("deshield"), loadFixture("transfer")]) { + const psbt = ZcashIronwoodBitGoPsbt.createEmpty( + fixture.network, + walletKeys, + createOptions(fixture.blockHeight), + ); + if (fixture.request.transparentOutput) { + psbt.addTransparentOutput( + hex(fixture.request.transparentOutput.script), + BigInt(fixture.request.transparentOutput.value), + ); + } + const map = psbt.addShieldedSpends(spendRequests(fixture), outputRequests(fixture), { + anchor: hex(fixture.request.anchor), + }); + + // Every request landed at an action, all distinct, and the bundle has exactly + // max(spends, outputs) actions (deshield: 1 spend + a fabricated dummy output). + assert.strictEqual(map.spendActionIndices.length, fixture.request.spends.length); + assert.strictEqual(map.outputActionIndices.length, fixture.request.outputs.length); + const actions = new Set([...map.spendActionIndices, ...map.outputActionIndices]); + assert.strictEqual( + actions.size, + Math.max(fixture.request.spends.length, fixture.request.outputs.length), + ); + + // Freshly constructed: everything pending, nothing signed, not ready for proof. + assert.deepStrictEqual(normalizedStatus(psbt.shieldedStatus()), expectedPending(fixture)); + + // The ZIP-317 fee matches the seeded fixture value recorded at the same stage. + assert.strictEqual(psbt.zip317ConventionalFee(), BigInt(fixture.zip317ConventionalFee)); + + // One input kind: no transparent input may join a spend bundle. + assert.throws( + () => + psbt.addWalletInput({ txid: "11".repeat(32), vout: 0, value: 100_000n }, walletKeys, { + scriptId, + signPath, + }), + /one input kind per transaction/, + ); + } + }); + + it("rejects shielded spends on a PSBT that already carries transparent inputs", function () { + const fixture = loadFixture("deshield"); + const psbt = ZcashIronwoodBitGoPsbt.createEmpty( + fixture.network, + walletKeys, + createOptions(fixture.blockHeight), + ); + psbt.addWalletInput({ txid: "11".repeat(32), vout: 0, value: 200_000_000n }, walletKeys, { + scriptId, + signPath, + }); + assertErrorCode( + () => + psbt.addShieldedSpends(spendRequests(fixture), outputRequests(fixture), { + anchor: hex(fixture.request.anchor), + }), + "IronwoodPsbtError.MixedInputKinds", + ); + }); + + it("leaves the caller's witnesses usable, so a failed build can be retried", function () { + const fixture = loadFixture("transfer"); + const spends = spendRequests(fixture); + const outputs = outputRequests(fixture); + const options = { anchor: hex(fixture.request.anchor) }; + const rejected = ZcashIronwoodBitGoPsbt.createEmpty( + fixture.network, + walletKeys, + createOptions(fixture.blockHeight), + ); + rejected.addWalletInput({ txid: "11".repeat(32), vout: 0, value: 100_000n }, walletKeys, { + scriptId, + signPath, + }); + assertErrorCode( + () => rejected.addShieldedSpends(spends, outputs, options), + "IronwoodPsbtError.MixedInputKinds", + ); + + const psbt = ZcashIronwoodBitGoPsbt.createEmpty( + fixture.network, + walletKeys, + createOptions(fixture.blockHeight), + ); + psbt.addShieldedSpends(spends, outputs, options); + for (const [i, spend] of spends.entries()) { + assert.strictEqual(spend.witness.position, fixture.request.spends[i].witness.position); + } + }); + + it("rejects wrongly sized byte fields with a typed code", function () { + const fixture = loadFixture("deshield"); + const psbt = ZcashIronwoodBitGoPsbt.createEmpty( + fixture.network, + walletKeys, + createOptions(fixture.blockHeight), + ); + const [spend] = spendRequests(fixture); + assertErrorCode( + () => + psbt.addShieldedSpends([{ ...spend, rho: spend.rho.slice(1) }], [], { + anchor: hex(fixture.request.anchor), + }), + "IronwoodPsbtError.BadLength", + ); + assertErrorCode( + () => psbt.addShieldedSpends([spend], [], { anchor: new Uint8Array(31) }), + "IronwoodPsbtError.BadLength", + ); + + const built = freshPsbt(fixture); + const [randomizer] = fixture.randomizers; + assertErrorCode( + () => + built.addSpendRandomizers([ + { + actionIndex: randomizer.actionIndex, + alpha: new Uint8Array(31), + rk: hex(randomizer.rk), + }, + ]), + "IronwoodPsbtError.BadLength", + ); + assertErrorCode( + () => built.addSpendSignature(randomizer.actionIndex, new Uint8Array(63)), + "IronwoodPsbtError.BadLength", + ); + }); + }); + + for (const name of ["deshield", "transfer"]) { + describe(`lifecycle: ${name}`, function () { + const fixture = loadFixture(name); + const spendIndices = fixture.spendActionIndices.slice().sort((a, b) => a - b); + const ciphertextEntries = fixture.outCiphertexts.map((entry) => ({ + actionIndex: entry.actionIndex, + outCiphertext: hex(entry.outCiphertext), + })); + const randomizerEntries = fixture.randomizers.map((entry) => ({ + actionIndex: entry.actionIndex, + alpha: hex(entry.alpha), + rk: hex(entry.rk), + })); + const signatureEntries = fixture.signatures.map((entry) => ({ + actionIndex: entry.actionIndex, + signature: hex(entry.signature), + })); + const alphaEntries = fixture.randomizers.map((entry) => ({ + actionIndex: entry.actionIndex, + alpha: hex(entry.alpha), + })); + const ak = hex(fixture.ak); + + /** Drives a fresh PSBT to the signed state (ciphertexts → randomizers → signatures). */ + function psbtReadyForProof(): ZcashIronwoodBitGoPsbt { + const psbt = freshPsbt(fixture); + if (ciphertextEntries.length > 0) { + psbt.addOutCiphertexts(ciphertextEntries); + } + psbt.addSpendRandomizers(randomizerEntries, { spendValidatingKey: ak }); + psbt.addSpendSignatures(signatureEntries); + return psbt; + } + + it("runs the full build → ciphertexts → sign → prove flow with wire round-trips", function () { + let psbt = freshPsbt(fixture); + + // Ciphertexts first: randomizers are rejected while any out_ciphertext is pending. + if (ciphertextEntries.length > 0) { + assertErrorCode( + () => psbt.addSpendRandomizers(randomizerEntries), + "IronwoodPsbtError.OutCiphertextPending", + ); + + // The request names exactly the pending real outputs. + const request = psbt.getOutCiphertextRequest(); + assert.deepStrictEqual( + request.actionIndices.slice().sort((a, b) => a - b), + ciphertextEntries.map((entry) => entry.actionIndex).sort((a, b) => a - b), + ); + assert.ok(request.pczt instanceof Uint8Array && request.pczt.length > 0); + + psbt.addOutCiphertexts(ciphertextEntries); + assert.deepStrictEqual(psbt.shieldedStatus().pendingOutCiphertexts, []); + psbt = roundtrip(psbt); + } else { + // A pure deshield has no shielded outputs, so there is nothing to request. + assertErrorCode(() => psbt.getOutCiphertextRequest(), "IronwoodPsbtError.NothingPending"); + } + + // Randomizers next: no sighash while any real spend still carries the placeholder rk. + assertErrorCode(() => psbt.spendSighash(), "IronwoodPsbtError.RandomizerPending"); + assertErrorCode( + () => psbt.getProofRequest(alphaEntries), + "IronwoodPsbtError.RandomizerPending", + ); + psbt.addSpendRandomizers(randomizerEntries, { spendValidatingKey: ak }); + psbt = roundtrip(psbt); + + // The randomized rks changed the digest: it matches the fixture's, computed over the + // same ciphertexts and rks. + assert.strictEqual(Buffer.from(psbt.spendSighash()).toString("hex"), fixture.sighash); + assert.deepStrictEqual(psbt.shieldedStatus().pendingRandomizers, []); + assert.strictEqual(psbt.getId(), fixture.expectedTxid); + + // Signed before proven: the proof is requested only once the wallet has signed. + assertErrorCode( + () => psbt.getProofRequest(alphaEntries), + "IronwoodPsbtError.SpendSignaturePending", + ); + psbt.addSpendSignatures(signatureEntries); + psbt = roundtrip(psbt); + + const signedStatus = psbt.shieldedStatus(); + assert.deepStrictEqual(signedStatus.pendingSpendSignatures, []); + assert.strictEqual(signedStatus.readyForProof, true); + + // The gated proof request returns exactly the bytes the prover proved over — alphas patched + // in, never stored. + assert.strictEqual( + Buffer.from(psbt.getProofRequest(alphaEntries)).toString("hex"), + fixture.proofRequest, + ); + }); + + it("keeps the alphas out of the PSBT", function () { + const psbt = psbtReadyForProof(); + psbt.getProofRequest(alphaEntries); + const bytes = psbt.serialize(); + for (const entry of fixture.randomizers) { + assert.ok( + !containsSequence(bytes, hex(entry.alpha)), + "the proof request must never write an alpha back into the PSBT", + ); + } + assert.ok(!containsSequence(bytes, ak), "the wallet ak must never appear in the PSBT"); + }); + + it("rejects ciphertexts for unknown and dummy actions, and conflicting bytes", function () { + if (ciphertextEntries.length === 0) { + // The deshield's single action pairs the real spend with a fabricated dummy + // output: an out_ciphertext for it is rejected as a dummy, not stored. + const psbt = freshPsbt(fixture); + assertErrorCode( + () => psbt.addOutCiphertext(0, new Uint8Array(80)), + "IronwoodPsbtError.DummyAction", + ); + return; + } + const psbt = freshPsbt(fixture); + assertErrorCode( + () => psbt.addOutCiphertexts([{ actionIndex: 99, outCiphertext: new Uint8Array(80) }]), + "IronwoodPsbtError.UnknownAction", + ); + // Re-delivering identical bytes is fine; different bytes for a set index are not. + psbt.addOutCiphertexts(ciphertextEntries); + psbt.addOutCiphertexts(ciphertextEntries); + const [first, ...rest] = ciphertextEntries; + const conflict = Uint8Array.from(first.outCiphertext); + conflict[0] ^= 0xff; + assertErrorCode( + () => + psbt.addOutCiphertexts([ + { actionIndex: first.actionIndex, outCiphertext: conflict }, + ...rest, + ]), + "IronwoodPsbtError.OutCiphertextConflict", + ); + }); + + it("rejects a randomizer pair that does not match the spend-validating key", function () { + const psbt = freshPsbt(fixture); + if (ciphertextEntries.length > 0) { + // Ciphertexts first: the randomizer gate runs before the pair check. + psbt.addOutCiphertexts(ciphertextEntries); + } + // A perturbed rk no longer satisfies rk == ak.randomize(alpha). + const broken = randomizerEntries.map((entry, i) => + i === 0 ? { ...entry, rk: entry.rk.map((b, j) => (j === 0 ? b ^ 1 : b)) } : entry, + ); + assertErrorCode( + () => psbt.addSpendRandomizers(broken, { spendValidatingKey: ak }), + "IronwoodBuildError.RandomizerMismatch", + ); + // A perturbed alpha is rejected the same way: ak no longer randomizes under it to the + // recorded rk. + const brokenAlpha = randomizerEntries.map((entry, i) => + i === 0 ? { ...entry, alpha: entry.alpha.map((b, j) => (j === 0 ? b ^ 1 : b)) } : entry, + ); + assertErrorCode( + () => psbt.addSpendRandomizers(brokenAlpha, { spendValidatingKey: ak }), + "IronwoodBuildError.RandomizerMismatch", + ); + // A non-canonical spend-validating key is rejected before any pair check. + const malformedAk = ak.map((b, i) => (i === 0 ? b ^ 1 : b)); + assertErrorCode( + () => psbt.addSpendRandomizers(randomizerEntries, { spendValidatingKey: malformedAk }), + "IronwoodBuildError.BadSpendValidatingKey", + ); + // Without a spend-validating key the (unchecked) pair is accepted — the client + // verifier and the keyed prover re-check it later. + const unchecked = freshPsbt(fixture); + if (ciphertextEntries.length > 0) { + unchecked.addOutCiphertexts(ciphertextEntries); + } + unchecked.addSpendRandomizers(randomizerEntries); + assert.deepStrictEqual(unchecked.shieldedStatus().pendingRandomizers, []); + }); + + it("rejects a signature that does not verify against the spend's rk", function () { + const psbt = freshPsbt(fixture); + if (ciphertextEntries.length > 0) { + psbt.addOutCiphertexts(ciphertextEntries); + } + psbt.addSpendRandomizers(randomizerEntries, { spendValidatingKey: ak }); + // A signature with flipped bytes fails verification, and the batch stores nothing. + const tampered = signatureEntries.map((entry, i) => + i === 0 + ? { ...entry, signature: entry.signature.map((b, j) => (j === 0 ? b ^ 1 : b)) } + : entry, + ); + assertErrorCode( + () => psbt.addSpendSignatures(tampered), + "IronwoodBuildError.InvalidExternalSignature", + ); + assert.deepStrictEqual(psbt.shieldedStatus().pendingSpendSignatures, spendIndices); + }); + it("freezes ciphertexts and randomizers once signed", function () { + const psbt = psbtReadyForProof(); + assertErrorCode( + () => psbt.addSpendRandomizers(randomizerEntries), + "IronwoodPsbtError.AlreadySigned", + ); + if (ciphertextEntries.length > 0) { + assertErrorCode( + () => psbt.addOutCiphertexts(ciphertextEntries), + "IronwoodPsbtError.AlreadySigned", + ); + } + }); + + it("gates the proof request on one alpha per real spend", function () { + const psbt = freshPsbt(fixture); + if (ciphertextEntries.length > 0) { + psbt.addOutCiphertexts(ciphertextEntries); + } + psbt.addSpendRandomizers(randomizerEntries, { spendValidatingKey: ak }); + + // Signed before proven: the signature gate runs before the alpha checks. + assertErrorCode(() => psbt.getProofRequest([]), "IronwoodPsbtError.SpendSignaturePending"); + psbt.addSpendSignatures(signatureEntries); + + // No alphas at all: every real spend is missing one. + assertErrorCode(() => psbt.getProofRequest([]), "IronwoodPsbtError.RandomizerMissing"); + // One alpha twice: the batch names an action more than once. + const [first] = alphaEntries; + assertErrorCode( + () => psbt.getProofRequest([first, ...alphaEntries.slice(0, 1)]), + "IronwoodPsbtError.DuplicateAction", + ); + }); + + it("combines the external proof into a broadcast-ready v6 transaction", function () { + const psbt = psbtReadyForProof(); + const proof = hex(fixture.proof); + + // The proof must be sized for the bundle's action count. + assertErrorCode( + () => psbt.combineProof(proof.slice(0, proof.length - 1)), + "IronwoodPsbtError.ProofLength", + ); + assertErrorCode( + () => psbt.combineProof(new Uint8Array(proof.length + 1)), + "IronwoodPsbtError.ProofLength", + ); + + const rawTx = psbt.combineProof(proof); + const tx = ZcashV6Transaction.fromBytes(rawTx); + assert.strictEqual( + tx.ironwoodActionCount, + Math.max(fixture.request.spends.length, fixture.request.outputs.length), + ); + assert.strictEqual( + tx.ironwoodValueBalance, + fixture.request.spends.reduce((sum, spend) => sum + BigInt(spend.value), 0n) - + fixture.request.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n), + ); + // Terminal: the PCZT is gone, including from the serialized bytes. + assert.strictEqual(psbt.getPczt(), undefined); + assert.throws(() => psbt.combineProof(proof), /no Ironwood PCZT stored/); + }); + }); + } +}); diff --git a/packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_deshield.json b/packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_deshield.json new file mode 100644 index 00000000000..30062b77d8a --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_deshield.json @@ -0,0 +1,83 @@ +{ + "ak": "6ebb833c1d2f8433080abceabe47906097f90678d603f577d0486c9111737b07", + "blockHeight": 4134000, + "expectedTxid": "51df7b135bc61af67b00be5cfca8974a31dffdcc57c9e3b2b7a62b6c1d40fc7b", + "name": "deshield", + "network": "zcashTest", + "outCiphertexts": [], + "outputActionIndices": [], + "proof": "d0aee040179049da4852613115a2ab16f863ee10d88e18165a2bd47cc98a91175c41d9e266cc6ee85877310ae431abe563f257c67abad464c584bf8c1aec6c87b4237067924eae3ed4f7830ee3cdb3ef1c1bd65d2a90a0f056a89d29bf7bc2967f7580f4a3ca418347bb28628dacbdcb0d639728fe9c46082da03f786494ec3f0f231e56fd2f76ef012aabc4284d6b7612a4409a242c3522629f58de492fde8ee5a3c40a8136b3bb9c082deeece66ad98180815df7db02790659bf4b102bd185389b5be40a59f70c51ff548574ce41430fd7bdb84901344e3f242ee89d53e31a944357dca9e214c306e3ad081c47cfb2796462d21bdb02c89e27fc3937331d251ee98684e494cd37bc53e19bfe4fe3207f6c35262a981d003a08b6c076776895f56fe0dc2ab203f366e7c9fa4dc9b4d6ebc7a7f7a9b366023fce01ffde6bbe35c7be475afe8efb64c3957681395882ed719d6ec95c2e8d5dcad7469a2015040b1235a35424e1a6595d3306fc5412bf665432f2c399402ca3dd081bad520a2085edf93e702d64404b6c1e9e19edeabc354374548f63a5c0797f19f5f4cc80ef0e02f74be06dd8505b2b4ebba54da5d5cc9f1a06e1eca81b4430cdc0ee11415c950d599aa3859106654ce62b25d6f8ba31e8f8841d0da49606f6a6da8bf422d22441adf9bb5c41a94462d6c8047b92fd385efba02dacfb0a83075870ce48d5a7bf0660bb3f560cdc5a8732cdf75afb4f0931fb4e4c66bc40bfd176d01ecc88de39b375e6a81e8cbebd0f80229aa55c40f13f89288c79de369993b484320ea9991c58f0439bd874296fb67d3b046478d0ed2525113d4221bcdd526a5b66895e4baa360fb24332217a9c920cc7397a1db92d265abe36a3ea3d1e2c8cc0e617228390772e704aa011b6b148e68d5518e46760a32996638d220efbbae50682b6ea9ba9fc955262a789ccf5e2844070aa30a399c66df596fe5db295e18707790842c93f37710e27992976b214dfeb67f2712d88ccdede8bcfebc3cc69bf6f4e4c13b0a35e1ed9c6c2e0a123026676cf80b26d7aa5dc72bc26e94478729170530b009781e7e714f4a29a05339b8aee027bfb513a8947b4251c6d96d76a4fc0f1d7fd0bae8282681826be79eeca0b2fde3bfa759551d0b8291f846ddac02e57a7958df6907cce16c0f1e254d1943e9a3d48a419231464f567ec0077ebec96f8ab0c3c9018f7c89c57be881f21f5b05f7031de0376fba4429978afaede720719bf85073438a4409a76cd5cabf123d1bfd4a2af42188e5d232610de1102a970d5d231f8ba14d3d7bdf5a01f01235b8e577fb9505c747a59d6d5f816756e1e3fdf44a7c85b32b9ef3d0e73d0e0457cac85c6c806f876388313c28bfcb5d477c6bf08d28b76b533d8e62ec6a577c6a49ef2fc74cdb1f049222cb3b807ad8fd7f5f418b104bf265a0da25656ca7714d6ab255928c8b6bb291a1497218e1d05055cffb76c57a1354d70203d188b167067a27da225ee963b96d7a7c1bc31523dd8e76df1b91a0b343baf4499bf4098bd57cbd60e8a452742afde2194aee314d91d928980bfb68f0a6f45b4fee7cbd7d15fe62ddb6f6d5716b92ede3920cfc48262b1fa4a939aa32b65d0ff306f3fcaa13e7a11b5ad3da1de7a33501e34612d76120bf81b3f084b0f12daa9a4b8f3ac45a2b13a23f1ea816364a5ae67ce10813879c8ac27abda4304256fa591fddc71903cf2d031c186070d4917a9f87c61b62a7f7c16fc8867451cf93a10019bdbf15fac3b2b5455f3399f8e9b92d678701f9447a41e7af7797335b601aaa7c7a02e5258adb3abaf35c3689a2a270137b80533fe0e5fd668582738cb2b6a9c8a0d75925f3ffc37e221060cded3a738f5d05869fa857790a2761b1e3fc72fe42d0a72bc162c9c18c4a23c889f9d0da463dfbcc5ee094830479f482ee168b8ad8d3007ed126bcd1849053ce071c94ee1af4e7c88c4eb36c0fc28950072c024b113e107d5fc1911aebfc3027010fc36cf22227b1fdd157635734c332b5ed11eb686b0932b27061510a6332f7983bdb87bd3bbcd6971928e66ff32dc3dfa2aff9c761393f28ee296c8ca5293243090ddc67d5706a50a17bebe87beb817e1a7697c2930bffc554fc803bc8a86202a7019479bd96595e3f2476984161c2b5f0d5602af889cfcb3b067c49e62345cecbd7ad1cae9242d90549ade6a6fde1be20e1e73d665c5441f8a4a41afaf316f1ff33e548359d13fcb96f907c443d51664a963d63dd2b2d0883e0f283cc71ca3c65b1c9546a42ce224b4c172c1546f03836737753df67375eddc31883e0be855bcd44ebc015fb770f6e6fa9210d11a2aaae6ba59bbe527080c751d6f8bccf8b3e41a4b9543fec87765a04d35f2503010d9957ca82c820a19240e94f7ced26b515b42ca47c44336fc325246e5a8eb5916cf5c30e7e3fe34b3d195888f3e72c06bd44c645c74589a9d655a0ed05a2464014c87f617d76fb3476a431e2e5c7315f7bad710fad4799267327c31b74cabdf067438d2606c869b12e26e77e7efdc3903ab0b73386d2adde18a0f5e72c98ee518408f9d7ae6d299a6eef27b47e7da65ff71d04d8c9b928f4ef5d2b168f1b8c515763c64a8fd18df448663edbdebb51aebf0dbb043ad950faea7a76af672517a285af57c80a47412e7f73988144fd67a730f35aa9e9fbd8cfe33d6020d95181339dd9a010c67942e43e0cd3d1b506201136f7955f834bed22dd6198220ff13590687dde388739458a2c6034e3911ed2e13b26ea5e61d0ecfab70217aee86f3b3284355550fa68f4b07b2cdf8aa304c0b3520ea316bc518d0ef6cb543d8e5de551970df62ec1e9b7a8cfc8e32b365fed5509bef7f7f69a167d8061322ae2d2702130a348a3ab2df94380d1e71c7f9e3c4e3df276a64cfc4890be49a5c4a4d3cfa397225377cb9cdd94a58822045493020209d68a3fc15fdedae624d66225e1e5414758e24f5da97d5a1fb5b9a3ce65fbebb3c19dd5f8b131cc8444160e08fa85a278a5541b62c860dc09871c4bdffa33e6055969323d93c080b0cb9e1d154950c0fd6d1aef3bd6ac66df433c5e9acf263d60b431529db6ca8357cf322241e709800c1bdd122327f7b39cb7d7547d4744ae712d1128eac2126a4f99f1a4f8f27a837016066be6e3ada2bb55f3446707bfdfea120ca8a183d255198d7f314b1a9330ccdb801d11d0e2706554eef6a76e36d79b35c91f277eb21ddf9045e82137afb22bc8b905eda7fdf9d928987c441da084cc2fd6b9ed49f27dde6c42aa4bcec240b6b0660d38238dffdeddc67b4ddbebe316f9b3522677c0c5a594dab5aafa0433f314e120dd46c5f82f1e03ff8e00ab5796e34ba29588617c974c3b4c70142e9122d0f873f6706a48864b88c58b27a65b3e4c30dae92699f169b049e91220ae41657c6436436408feb6d1b16314bc76f934c55d539b70636c3bacd292f1623d8002abb88d7e159d6ce3f32291c8fcbd74c757e54d5ad413f8d377aa2c72d65051bccf922386f1d21a0d9892f4944ab8c69860766df5dcb4c311b335835c1f1640d95ea8a79e59661aab5b29c18f0d6d2fd0f2bca722dfa2a8b14c4bf9717e64333a82c10bef31185ab8fcc7baf610610ac159d3f8327a6c59c5215a24454f35704801bf0bb01ebfbbb011c90e594ddc72266598ac60e23bf1c652d38bd043e2510a48b18762a5153a50921f8a74588cc6e9c3ad0d373ab76f27bc600df50f73d2d35d1278b96acbf1064a8fcbd7d5289945db88a2d92334a9b47862e6ec624a706276edc86c829059e7fe86813c2be8456d10b97acdfb60f2f909254a31f246c0fa52eada955e6267a086aff5adcf9ae6168ab1e78fb1e52c1f2b73e6b260bc517df22c5e7c00104066c5821b05f560818242af4538277afcf70a83d4ffcb06038ea5a706febf8890023b69da7a2f40433b5e1ea63206677446428d0469a822f305a23e534363165d6f7a1dc2694e543e60898b1957660675c3ef645a7c2f40a0aa321ddee52aa8d59a3b5972fbe36f73367457057e7ecda3b36181cb92a9092221f4296817401f822649ec0fb5200cce497cc8102444acfc704c42162d8b12436129d19558d0cfa3a757d4056d75a3f2f9993a7b124b16239010d6a217bbde809d25eb4804d8158942ff10c7ba259f4cf014f63be8334ec0d090d505944133a3dd0343f89235a6c71a1f2f1dba44596726d33d1cfd942776f148c730e6bfe982f6fdc1e522720c449b819975a2fda0b018ae717baefffec23be9982aea8c6602e2ab03c37596a8f20ebb276d10d0aef5756982b732a4722de1b64867a54adcb0bc433979100bb7b067e384d3b629299751584d2b063d0451e55aaf101629e1822ce337c9c67667eb8320dec2ad4311d7e9a6df848c8a124f5690282e6a6d4fb305efcd43bbaafd34f56f780d33892ce243c43082db2204d9920886e09671e5c1c6d41d98753b67a3ca69ad0042276cf974fb91da391e3125d5a0bef77cadfd73dc4e530d8a77b6199dd56fda5ce228c0d6c163b0027e6528fb3d029e11fdc103a7a7289ad2dd66a79d326342c39f94a9cdbba8bf3767478447faeaab41a9faf178fc36d6dc4132bcf9035c1a94a519f3b0e476aa8b67d7ea4a7029a7a2ccfd33cca00a184c9990391e0683a2e9780d99d70de181b452108e22fc13720b183b8080471238028f5084b98d03c85f36f6505214420abb2ffeaf8e625c4ddd7c2403a55d633a7c38fdb6dc42514e0f4ab4774e0cf8ec159f9df44dc31ee60336dc90e8b06ef7b060c2d9ec41170c80998daf003ba4419830fa00b60bafd7b33d6d819338843d9bfa39e940bc425f38f9e402807df3e1faf9ded4f55b25b72c184be3a7e5199b8083fa59ed21cd6df5b91740e504ca9b0395024dd0ad3f7a1062d6d33ec6141e2f76cd701d8f71c196f9d8e4c307ff02921d50cc785d3afdc8014603f05f67d947ea9d1d418e77fd242b24f3d5f20656164f78b05ca9dd90cd141e5010be205fcb0394d4c5c5c778487764318a3e7cdf9a2c327ec6c180d913c2f6d2fedf6d5235a6f7b8971c40ce2d25d31f79d76269f8deeaf3dc4a93f852ad2d31d5e6f7f08f3dcbb4af7f62c6e0ef540d1d2868e2816648628347c6172ece00827a56a828e3dbf7e32bd35810ab3f8e1d3179e85bbb4f7e34386d219dad3c2501cad31ef775c2925c6270e3b70f59d8f0839c4eb38985934611269d932eacfa332af602f9e0b12d60512e1e2c3e27037c17eec03b496a9ec711e7998c088df63116d149c46ba89b71115a1624c6e9b3d94c7b502ceb4d4c8e701d83b789244f02a8e443e1292c5c468534ad0579825bc466afddfcacb5bdfa99409988de4c951197d43aff2d91b0eaba844fa3513c8a6b3f384474e88c1d1690092786c52c76639dd8ac1448a46abe88d42d4b9e8fad99c80263748f1ff454d41dbdbdd216d1c0f5f3384ad19c1aa33343463913c4549427025f370cb986633f86d5ca4a7320036ca833f23a7ab40f3aa6323925ab1df3560f45fe3ca51f3a18ea9a0804d6dd032bfb2f97442b7a6da94c32196bbea37c83c64a394a516a82628d249f5bd43cc3c0bc94e04620c2bb13d0717b47b4f3d82d8bd3276252ca419d6d491f7aadf181411b2dcd5b3d50a640580eafcecd11c0c53c3c4187c3917a29738b784888b122f75b6aa90bd861d83687adc7815b59f5006ab35018bf7aa625185768bd8341123c50c1f2438a07047f98dd4c1e46197314a5b3b30630341a0ac321b28417cfc3c0da7e9c87496fa9bde9ffa1dc004e53e4f40ffeedfef8f905028dff09df1722b6d023b5981861ef3604ad7fe9b6e63a9200821af2b50f583f3e61783637fcd010dad5e5c1c6b84ef3b31b514d69012e4e314db0796dcae2ae27abbd60c997b10795df9b5a6797fbcf625506e04fd089a6ba7a46a00725ed878ccf9f713ac131c12a0fe2221fe1541f7c4e1264521d18a8a9fc4da203217e833874835ee616b0e9ed733441402772fcc1bdf29e71208a4395491d4c14be5fc7e8e7af38fe087863a110ffdc2aa950c5ccf595f192d02541d2f9926b055fe6f3299834dcafdf121bae3995e632543edf18197b117bf1d02ea377902bb29a17b4a09ee71a144439eb465c767a38130949b7ee38af95154fb94b2b79f6b24ea76f806f93a1c617108bd9bf3deac1f2f75414527a7fbc6966c5482d5d9dfa9951b7daedaeea2aa353ad3e44ea481154dacc5183d0ddd7b45f413a11d72741d8b67948c81ba5ba089bb096032fd080998fb7db43f7b5453887262afea4e92599c4b489adbd7d80b7e3b8fa3b202b5d4de9cc3bd5c399862bbc069b709e2edcea3b9191313cabbd9f6b409543e7c567320491842b53aac5314168f9ac8d17f3780ba2860986204174ebf1c1680d1872afc5ebe447f7b8a96dc55c22c2fd3ae0ffd0ef56f3387170d5312263fa398f154b8715af41a6b636f4281f8fee6c8d8e4dab90b9fb88b74ee70231ed4d38cd0a5d824af99012b01586e02a7a8a5c84a16485336ac1d05dd4948b4c7a9bf04025ebdfff0771fc568fd1622bb293d66ed24a209e093c2754f8d52050da41b0f978c91175102369bbab7eefc4b402dbbc42178741e95ceae3417b7ae16b012ed865b30a64ab5829071d9b528dbcdf2f1868b84162fd3c56f4aa5e4094c89d662bf7b68e18e81e5a355625f52c568aa7b0c605007a3dfc74c5697b53cb96b744e2acb4dc0f492fc35352fbcfe3c6eca6a3d23a9016515521d8500eba1f426ed08c4bc2b3f817b5b29266bccfb18e63a40df6cca9cfd331f4a3791b4b5ca2fabaef2e75f769cf1ad9afca35423635285d954c0bd01a6437e3a80b2d6a7fcc349b02e64decb6b71e26286e920fc0aa579e81d7ac63accbacd1f1ea408bdaf26489496fa4b0b61a55dc98c40a84386efe57e53398e3016497ec6031eb3158858eeade1b7d56ba3462b8cad320afdde38f36caa3e5c6c9799d39fd7d8c110e672af249206bd65892c9e6feb756f70fff48cadd3f5361d969b4aa70411003e", + "proofRequest": "0207010280c2d72f00277302f645a4c6b7ea56fc199ba39f31721d42d2dd08813bddb57bcfeb5e2c2d00000168e39d0541994980d79b8c84d64cf1f9bb1f0f3216b69187055bac214b27ae24018459eb1e67434b5809d95d3803f5efafdd11095ea5f7cdaee176c7e259cdda31621c8910e2a41c7959bd4f2e685a9a917b94fb0ed734ba906c1af5ad75abfe0ce38293336c6d2a106793a2542e6638080a973471224e5bd83ca7b9025441c19e01406d0f3ec99e881e292f0c697c6f2fda28062f7d81621b82a4df6917fa6ed91d146139d215faff299f68c82d19c87c6fcace681243d15f1d43adac460f07b29908012b4559029c0b5dbf941c5ad181a5fe8f45b34630f29d0c8dd8dc1cc3573386f416cb324133156d723df5e62d0180c2d72f01c8000000000000000000000000000000000000000000000000000000000000000134343434343434343434343434343434343434343434343434343434343434340001002033190bdef16c2df1b0f6d41903142f52660fde345f45b9c94d87e61a9f78851602000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000102155e64c5f669fc519dfd352928ebb830ee1f28a0a171510de4ac85d68de239000103636d782038fe5156c36c78da72290b6d81ccade9932a4a83500aa8962f04a6fd593bd30c09ddea739d31c50fac4e3dd513fd8373b6ec331e0336f2083b7dc1e177f5a1296dc429a6526aa55c363046e2ab79ce094b12755536462e7c197717d13217840fc404fbe2652c49eebe461dbf9730ae5a5e677adef0e31e51fc6f341d8e6316a1ee2233bbaf7b876fb196a1b65a652b74fe476fa0cfaf63e5491afed2b2be25787e412f110fdf477ed771ab29d2474ce14e050619b1d9418d1329961c085e29be7728f60f752595c238055291bccd0bc317addcc4e9be713c71757b5ead8bc8fccd9e9f5e9f3e3329499907213bc9033f68a6d34581e537625948975aad5d206973b83af128ff8f7932a7a9c95e5e72068364f769724de47c74d8805523ed8765ceee4f0e222074ec19c4b86afb185bd01b10832a858175b3720ba8b11bd1dd46e0c4fb77e544f521779e04140b2420926d44ade37e1a2964d55dddbbfffe8f94957e6678fe4bdca2e6595dbb7861c29fabb5d7b6d718bea3165c7d7e6743056c792ecfecd6255fecdfef80030e57fccab24705cb4bcc63ea6adf0f93c57d4f7d21f2dde126a117f24c6c4b666082c293e7a037b4660a4a721217c399277b6737dad209e5d78f51820c45b1befa9fd69cba99b6041af3b6509f0575f017adbc2f6ccce4ec144560c84784cc51e64428683cd1fd4e82bfed0cfc7e20275825da17fe8b6372af32e25a945c5d356c1f9eddaa9b86921b192d1d61f03fe50a026345d6e4e4ac32a60cfc703037ba41e08d7a6f8cd54f2ce5617fb29e403d448e60933b2bd0ef5364cee1e41b26129570aebb17443c7d4d2dd94fc94071186d05b983f14135fc0c65fa99c56ca2d8b0ca41c4375988e8c90424251b4be15d522e8e0d7797d333337b12696502c7d3b74ad31504d7a23f34011419515267a1298e6668fd8b6e2242dc50fb77afec31a23f407abb6856ba24b6315b0ba369b463b794d8a82591e7ef6dbc691a5b3ab4191cd3735d2d81084c82ae378fd040c81cc5955205a3f46f1ae6de443711e39a28b1d52a54262e22cff670012bda898b3c55aca6f9c941f20efb5fe0c9642d2626af5a84d36cacc2ccdf2b189b54817aca800c2a80653ab0010001aeb543d0e65d68a26a449389f07c4afca6943a79960b6aee840eb2595d07a6d2000000", + "psbt": "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", + "randomizers": [ + { + "actionIndex": 0, + "alpha": "02155e64c5f669fc519dfd352928ebb830ee1f28a0a171510de4ac85d68de239", + "rk": "e38293336c6d2a106793a2542e6638080a973471224e5bd83ca7b9025441c19e" + } + ], + "request": { + "anchor": "277302f645a4c6b7ea56fc199ba39f31721d42d2dd08813bddb57bcfeb5e2c2d", + "outputs": [], + "spends": [ + { + "cmx": "38fe5156c36c78da72290b6d81ccade9932a4a83500aa8962f04a6fd593bd30c", + "nullifier": "621c8910e2a41c7959bd4f2e685a9a917b94fb0ed734ba906c1af5ad75abfe0c", + "recipient": "4559029c0b5dbf941c5ad181a5fe8f45b34630f29d0c8dd8dc1cc3573386f416cb324133156d723df5e62d", + "rho": "c800000000000000000000000000000000000000000000000000000000000000", + "rseed": "3434343434343434343434343434343434343434343434343434343434343434", + "value": "100000000", + "witness": { + "authPath": [ + "33190bdef16c2df1b0f6d41903142f52660fde345f45b9c94d87e61a9f788516", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000" + ], + "position": 0 + } + } + ], + "transparentOutput": { + "script": "76a91488ac", + "value": "99990000" + } + }, + "sighash": "7bfc401d6c2ba6b7b2e3c957ccfddf314a97a8fc5cbe007bf61ac65b137bdf51", + "signatures": [ + { + "actionIndex": 0, + "signature": "6d0f3ec99e881e292f0c697c6f2fda28062f7d81621b82a4df6917fa6ed91d146139d215faff299f68c82d19c87c6fcace681243d15f1d43adac460f07b29908" + } + ], + "spendActionIndices": [0], + "zip317ConventionalFee": "10000" +} diff --git a/packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_transfer.json b/packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_transfer.json new file mode 100644 index 00000000000..8413a8026c0 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/ironwood_spend_transfer.json @@ -0,0 +1,156 @@ +{ + "ak": "6ebb833c1d2f8433080abceabe47906097f90678d603f577d0486c9111737b07", + "blockHeight": 4134000, + "expectedTxid": "b8798172f78840a67a1670d8160843efbd9f00ca5e517f16dd699c5168a90b16", + "name": "transfer", + "network": "zcashTest", + "outCiphertexts": [ + { + "actionIndex": 0, + "outCiphertext": "21bf03ff6d04f84ed3f2ea3713f09dac4db712643e0b929315fc49711e79f880ec92fc3b002941cc0e10afc7f55f512a3d8041db69d5f7aaee6d5a8db962b7decff349190cb2dddac1e17f9598ab5f5f" + }, + { + "actionIndex": 1, + "outCiphertext": "74cb7768c899b2991a0659bd775eb2bf45954e98856cc3659f9ce39abe8dd329f67a088141c44451dd599692d9fda74ccada5a47fd66f49cb74122e756de6600a08c99810a747848b1d9cd1fea91c7a6" + } + ], + "outputActionIndices": [1, 0], + "proof": "06beba2a6c628f9b5946b6a253e64264ffb0f010e0eeec4a8950bf1b0d9fd604494fbe481e1e1def8d1e018ec5cba45e9342394a203af44256e4e1e01a55242cce1ed5cac3dd9d67b1f3569e640e544f76ce2c7fab2661c4a5c51dcfce03d827faa1d1759480fe4283088578c400c94ae86eafb0b1e470118d13d46d037fa109d89c403753de8a1b34c5360ecd910ab62266ecd80e39cb19c5933b5518b50b053f952883be4b15b93e8fb3b25ce30ca34158a37a27e5020f6aa28a47e299170d9f6f284ed0efda0e23b163191ba1717705b9146191fabb05c436f372dde7f109c916ee19169915f81eaf2295267cac02dafb433b08a644a66779427d194f5a885d7703eca58e6ef8fd82fa24de74f45da6079c64834987b1407f7dfb63fdbe3a08cd106c753863909b4e16ee084874a1c229de64f85f6f679b7fd9cb705e7dbdbeec02d1221ba294ae7b22bf18100468ce82a2826130eea4b36173b16615590823cb39bccdd8b92d6de8fb463af52dd5a1416fca7d130e31d1516290ccaf4b0dc55749e3c47e0b070d5a9135c682901ca6e647b94b3de5ef9b095cae9c6f822dfdb0b1518ad728da4f079f1fcbadd35c4f7f2ed53d2bea747d23055fb6127fbefaaeb78eaea0cf9376608d19cecaf70fdd09246cc8a6c7370c1b750c1c9a9206d1033c0e4631fd2efadac76392df6b020423439995b79d8f1ca808896d86e12d4e886a9ad8db9618f29e41b67f4fd13b2c1762a1869a35c1e45a25d9c545e1b1b7b92f7d2de533c2cb2a72e0c747254e408fc01f2d764df965b7fbb6339f5037dfd47e2a09d9cdcae5782d8c9fb6708eff4c17bf9554f743e8006558cfa6521faf20679bfdeb9c711a026f09c2ec7ffb6ad28a6df8c0028c894f493c53833e18e1897079f145a0f5accf71dee404d9a7ea5acce7c046abfa5956107acaa284928ebb898d08912b87f36cd9ade5ed814fb8302f6cc3f256cd9ed6b8236d279fb911b521f5e11dc4db1ad7943e19af1d97af9d8141848ba0093630f1f78487779472ffa2d3191903fe45ca8479fd9f480efcf6b5e91dc771155cf194d298a5a00d3e8a68630c6763181bfca044ec20ad2ad772bab797cbf70e5906e363a854fd9fe9a60a4a2949ef2e60173c3edd9fcd0a9ae6d70565abae269026ac8c59e8a9bb5205a0cb7565bc7a002f714feb2af99e7cc80a3046f42dca21cf9e2878a3eca863df34f5963ecdd00cbc9b87a70654b0c967869e8aaaf114806e2c3a602fffa19cc6423a6bb3fea40d90443796d27090959a277bf5750946db426c6cab967da5139d28ed9800ed3207ef1a73e324bf48f4a6c2e23c5178285d781abd6aa55b068031ae5b9e79470bb70d8ab1d0eeeccd7b8145202be310e1852fd28f4eb36e12c0c19de1b3b51c033be530223440dc00167c1d75c284fa029d6f3bcc6c77c4b3890063dc1efa270e63dbd97f2b4a18f0e4a48e7ad5d15b52698b273101def42a77ec27873de8d161e9659ab5731f70dac8bbef2d522bd7e2a78d77618152d786176d499abb82887ce64ab57f3042688a4c80abc75a33952399b62a2264e52ca771b440c8ecda0a214a8385a6f7c58e77f5b794bdf3f448bf97c02acd3699fe313d01afc8f4cedb55dadec7f6bac62a67cdb5efd1fb56e8f8e96bc144367eef2a5cc925c98871496df08b84069495dca0898b3af0d7c25bff161f4f38fc5fba07ed6bf290852494dbcb31c4b9f517ea9d3afd360940dda8eb8316ed81048edfa69e4bd51fab27f60c941a3b21aa69dfde491c9b5b17b86ec1f55b52b10b02d09426d0ab3592f39c5a24e95859a076a77f706c4dfd18391a96ae3751c7376d0dae62615dd3b49a908dcfb99d330021b93ac77a48a904acbaca538e6e636c7aaeb8e2fa57aca0b96b0556f09bb0aa26d8728063b44e1c8ed68540c0b3ac0e68f408b27ea9f9e262850a48baee8e8f41159f58010768bdad9706074690f748dfbab1fd66332046021af2a717d8001c0075df71d11574b05fb59f9b27b6688d97c713ac0c5cf0c8c92095bcba72ea2fca9a8776fb62426946a3a71d5a56604f251631a0d6035fd831b02d99695e0c0be645582615c6cc89260702485860ddab0f90b1e83a3289492a69133379cb5fb882d4212706d40dede42f962c5f0e8f7ef3a7a328f5b32f65a13889c804b34f4e411fe95bd5bf9bbf035e0744160bad76d091b97ec52b10cd8eeb0bbc1ab4135fc720940cee43c9ba2b50e0405646da7b57e4b9777b7be6cf1972c3c6aee51d64fd212cb9a6c5207d4f1a5a96e25c5c9f9bc20ebf5ac53805551bd208dd4f02ac707f031c9799672295786270e46b41cbda7429bf2db3b6890d891c1d75a9a9de2fe86bceda0c1dd9c1b8e9ad25deed70af4f30a7c4db3137b6d35711595395757f917024536c7e79f8253be005dad18e10363027bd4d74e78c5266301b45dbe3b695bf0024d6b8f452916e5a43fb6ea2b79d2933681e73ed0fa2ef59c1ea5e20f8a883065972ee81eeb4448f317dda3343d009d12bdadcd0456333488d8f9be13e25d7d098ea37dd11733b7885b6e49fa4482ec1ecdb5a44ff5e65e91bbb63b4be0890f88d8972df2765684393b7cad2fdc0294c74bf9847bc2a4273ede4c09fd839d82753fe18316b994c63390e4ce36fbf0eb0d812b9ec18126cf4bcb6ae0249efd3da86fa9bed21b299fa97bd69fd20f518d53c405e4a03f0da4ebf466a0fbc08cb7294c98f6e3980dcbfe9c9403f34881f156474e3962e88ae8f8333b19e19ec262832693257db01ed3e160c2140bf8b237aee07c18f23a7340e3fd517a6f4a9ab7d6c727383a2445ef1b28db480353f32d2f09b2bd79e43e8819ee0fcac5c8415074f97b15cdad6611a15db6739719621b2e7fa86a34c79a92dca08342037509cd76ab983795076f8df1d60950e8f05219253fa4f172128cc372333a6aefdaa27bd444c1d180d6cbd8f9fed3fb35cc02aa2d071d0fb59605708c31f844918497555de652d4d8d1663c49d96ef2e9cc12d2abc2b0cafd278c2fc478c3bd8ec3c2862bf6ddda6ee8452e15fe025669c1836f53edb6c8888d55bf0c5c6bf090695a687b6c5f7d03e0babb61c54811103a30826ae7a71917e59bcce1ddca1d74e4eca677c530069cf34ad398de1504d96a731f74a01e1cc287e050c7f4e4c595599d97d19a0a25328b5601c52a3e3ef5342091610f52a339290c5eddd277aba68511260700d4d0f1c310c739e4bdddaaf7c3a5108fe8781521a43e39b7cfb82f148970597f2e667ef0f159301292f6caf5136d32a185161930fe40309dea10ad10deeb79a9eb25f9407c3e1ac0c26eb86da3ddad6aaeff48e8bb2963b75b9dbe9333d9e1b6dd6e704364672b6705596b490250a4cd8a409032c56330c3d9f067906cca7b2cce2ac16f293794b0c690030c6158ff7f8c771d4b6ee2dd7b97519518dd4938f5059cbc6d3753bd307dbd4a2861f1298191e50d563a874796c2c23cf36c0d90ffd63c8903fdf41a02cf17e147f1c651590e0e81816cfd601b08af319dfcfb7e721b5a8fd25bee0bff072a8972c045a50658654557e680aae11c155e1ffeda94c2e991544fd6a3c927fc877d8470cd6d5e3ee76c6b3b58f6cd25aac5324070a65f2bab2b052ad97fd3201e6d004337e1e39589fa1ec0f1dbee4735a4139c1284b85ca55ba68ac4b2583810b39552ae3e88123b3540e09e1ab4f89cf606193786dc0399ee4a4158acb8c9afa05711e65a57a7d380ecf98f6dbd8c64b910891f1b4bbf8d4e619cedd5299ac7a66cf06741c6217c5294087a6952f3afbd20349df5fb81476a3b802d0ca1ff0941a1305b4f4dc66dd0e5f3942e9998cdbc0ac36df2faea1016773192f29f88412a6c806486d9ccf83ce01e50afd02a4acab94c3cdcb6e19f8e2f8350b9d872268d7de14bc5806ee9c1ecb5fbc478728a8599f68c8ac1df66f2cfcd2e3ee9df747b4702f88644db8b5cb7eba5605a6659c70a68b8e6391c9ae797b78ca325a6930f50d0f14823ce0e0198bc05d38c4af693ee6261c7633d9a8da794a4e3978f5577118181442c0d5498cb8a69136aef369d1d958133a76bcdb19dadbc86da653d0f00d3bf5126f360285155390421f65d6b1da0b63a190652f4a800bc3086196cf003627efba8f979acd43ab79f728931b9ab86c66c460dcf1f174f570b829db2a2e392ebf28c7dae829366b32fe6f32408a038579fd8125fd99cad4d29475f45f225d242d4c3cd81a95b54d510a9013462502805b3dbd0832fdb2ce0f4e9a1106d1731f5062535bb660fcb4022d7586d30fe43c359e3fb4a3458f08d08e72193aa5a10d2d525b264b948e300f64921fd681e973165f64a968e0ca28d92f36290093f1174661b35600ce1f2d22cae6ee0e7784f93a84fa094f272b7d3283bccf7b7b99313842523f66f7933fe8578d7553cab6d0761e767cc5b3ef996c1adfd9ee46600643613f16bc87d1ca9a465b8d633f2b002701d46764dff1c829a409dce28fe727a8565e69941895a22ca19ae01e718ebe38b1fa9402d6dc6108368eab812c41207790bad795c15b587911907fed16b3f3dac36484ba35755e49ce50063164952dd2dafb12a380d7203f396e9d4793eb352e9837f050d5bfb7ce4794de68b76b2bf264f13b662ebec82f1c3baf834cc02f703bf17f4e9c4ec32fb61abe1328073a0db5a68d0a533ef05e4c9b3c8277fe9ebe8f9b3a342871a21f8918a9540fbe2ceeb09cb830754944398bfcf6b14ed0c8a6b2b74e68826559b6ade3496327a638fce28ce8e54ffb6ff9b4e415181d102abbb0ecc4d3e69052e809aa08c65c9b22b4bb01a22961d6fa0a28fb9677ab2d17ca2528d84d3d190b036bbfa548414115561a25ca2049e325c919e8e452fd68a57cd3a7ca1129a631cadc431f4c0cc4392aff9f43a03c8b65473ee860bcc88b3846956eaa490d68818e06920bf9eea705e0d4f5a3b6e1bae628864e26a852eb459a034aebef72ef2a5a7c45bbc24aba28950a2652e02d2c0952d5004bbb579d764f07d7f841243d255c0fec0a7c96a8255f4e15c2f0b225ac9fab4a003238f8bae701b8692f2f8a402856bb27a83efb0c6eefb414349fcfc60b6a402d94ef0589fff89c5cb9147ef0ddc4c06c1fcbc022808cc322657240f748d245ca1a20ee11c39c993499bc7af91e4582e3efb0d7296716aa217ef8d7b7b8f56ad9b75c9d6ce6bbacd44fdea57cb03e9aa99dc8562f58d478b98d1a8389afe2fb00686c5a1ccb88d4158d73ac81238e8fafd5762c2db2895d206eddab9035da8223a98448896fb3d13362029a4a98c3da7fbcaa683fea781d5484f1fc24ca2be32d7a679f1b11e46923e6c1c31bbbc21ecd5c8e082be4a866cb3242de3178066fd1b69a3adcc5b6b59f0f975a2a0811473ad4c7fd06542a092841252c95d3924bb152b38c59abcaaf4c018eaa81dbef9bd7e24b132ed67602d623810ef1fd46e728908b094af3a17c04a8fd156d021b7cfae3c6a8264949439090500ce7abe1c5bb2bda8c85a383f994f8c37284bb5b6c0ea0029c2c611826aeb809f1723c0aa2e88e9fc348f3723b003ef7f7a530dec30a0e666a15acb0ac458aaf3297781a402180013cbd3741c73f110219a6837237c56359573657757022308499df58fa9be9700e48895a7a62b3df89a9f194eada3181604e0eb2586efcb675aa501234c03ebd949ffa8e77984a45328a5e7f30c46ebeb98d0a63e268ec64ef79e934f313e76c1e370a694815f4f832ddc33d1f73ab1b5c971ac5a05c5e5c10fdffcf763bede61657b61633b1169696b511caa8e6ecdb8e8300cc4186278fa9558fc28de613d834fbb4a11edb5f772d8fc11e54900e8c773c1fca0424860682cd88878ce814b8f821153e0f3f43378a7e6abae00aa536f056107cd5ed44af8e55f3cc44791d2e4465a07068737c57b96781a9ab6c6f0dd0402e69cabd8a06de6a8445870316b82a6230395b6eac2af3cae71303e74c26a43f2334585a374e80421c708db391a22555f7b03943192d0e65eabe8e87767a1307328c187fb2ef7a5a8930a569d6db5863b68ba2a2279ac77e403ebce5bedcbc7d1b0cf672ab7e7b0e19d7d73b157b172b156d0e7ad71c479dd06153bf92144b3a31a447fa3c53b8b0d33ea436da2211be05c7663bced6c85b40859b3e4d5a2039070a41ef9bbac1343f464f35d802df3b75f2093708f9d833a0ff2a8f39c2e03a0e41dc4900831ec9540d1fbdbeff675b6ffb6e97c89371eebdef228c2542f1fb03e0bf89e1c5b99ed7b2462c4585763039094faaa1aba1caa3cf7d92ae4b90361be685aea5c3c035115a988772880398c7fe81b2a133a6f4dc293e54115ddbd627a508fa8aa17aca30ee5295ba8871bf1fdc580caa6d779620109a654f9f7ded0803dfb922f7849caa907f25024ecaca7e0c890ce8a4af43eb0a0727da6b9502208f6f35e95f8fc92539cc061b1880b46661314b84ead53b75c30a75138ef3bf3bd4e0efcf8296a09d5d9f05760471d44ff982538173b4a07d28aa64121a12691b28bed339f73836f46a2b1488e7938dec6299aaef8f72cec7ddeacfcc69790b081f615076e164979dd71d845a40746dfe61c964db997b0857e7e1db76d7d7601486b840866c550a7091113913e090b6aff1f7ae42ebd9eb3574307c01a1a87412180f3ad3a06b8e34114d4b581b18a67fbf579e963a80dd0d9e1cf6c74705b5333a6e0eb042c778202c8fd0fd6b5a8c311e8ffa01f9b6d9f07a9bb2823bf6cd194a10cecde832405a933328fb05f9ee783af69271fdc184cd5e259eb502824823a4335f8142e8be685ef062d16c34eb29f779ee222bcf00719dd89e439ce34c0a2c1e58208c42ae921941c488a2c133f695e94dd55b180a4d9170a90442828d2f417058bdbe8eb7e47f17071eafa2a0c2cf012427cf5e5d62827d86e83858891e5ac53006453c8ade60bffa75273a92312f09fab20cd010a342f5abdc5fdcda1bd9c998e430be17ac2990220757fe2e96fe4adcaa62c7e46744b55ee5bd6ef22f53f6b3c11410c3d8e9624dca3d4cca4209dbd0b9af3dc55731f405217067e8274228c9bfb64f3509401929236bad85262e18c1abdfca6fe6255c0954c27c503aea2b425ecff1a721e54663d56274f17c9fa4de7d90e1168a9e4ff7544d8ba911ab4a168363e576ee0ddcc9a1f74671651e595783ca6c6baf720b7e2823ec210093d0e1eb6ea9d7d57d72f75df6d0e96a4692111088be48b80b01680da3515a077a5e2281a9ba60d263a10f82b89a82b1e7309e4b161e9d6b9600de34710bd2096dbad4fdaa774b2759114d61de5fc1b5f79669cc8ab6ac47106fa81b855633013eaa04c7534ea3037533117c1f8dc62b69043a0009e3e66ae383cb058a4d4a1d9cb049b837c1b44be5d907b2faf446a11df53859bd3081ad5ac9be01d1985620d619916d460a109b8079d242f02779d22ca74d4b92614e111a5f36522765e415878d22a62a063904e2bf1191bb85d5ba55e209d153c17da5dcb2b8755f74141ea4f628d060f814910d4e36452cb39572a02a069c53c51bf343b25eb142f400357ecce6460ac9389d2291e46feed354ae24812572d93b4039f29a4fe3c8799b3107247687a3c6e3a552aa6b7a236d21cf3db1d9d4fcdcefc5ba98ac4fda0959008fa6c49cd3e110c971c1c79d1c4e9e33a943546d817b96b342beeab428a9363c13a94602cfc58435ff30b2aa81c3ddcc4642102280507f3f143977353b3e9213f209b35ca932a4c1cd0b02d5ae0d9513a4c5de65efbd70d14d1d0bfc596a6009797169cff250e17bc4556a3621a5ec5e281f578d3810e9e67ad02352dfd45822440afb6acd75d023c193bf215fbc034f4d5aab5ef2924ff5a7047efe33bbe1273682750d88c669330661eb98ee55dd08d9c9e80e11aa48c5cf89180ad8fa813c1c2bc4b8df7d28261e00f5aab3f8aa77323c0fb26f258a531f93f991295d910690bb658648c59b1bad26b57a8ce620584c0b3a99b4ebdb37ff1d919ee1929a019b11aff1f782a6081fcc4689ae592ef6f1c0b08835802a12fc34f4aaebd58e29690e396275bfa511088408642a5e5ee2ddba2c371d69888d4e04f0c5d7531717d8751770c54421c16a042fed300a68d3b9c87d708da0e35027830fde41f1f503209c860c234e5cb33145bcae6813ff9f596bf98587430fc1c489397c7d3b851a535c5c51412f4133e834f68734bf76ed70613b3385687c8dea3157a9810bac2136e37eba5ced820c9b62cfc3c5f12dd68cfb74fb106f61b1cfa4d67917c5582820353ec0a34c87b6fb0c29ebef564e31f15bbfd7ae9eee6a67039e09b20e571f58ee4abfdcd7fb34bef79d7df8d82d981b60587511b87173ec6053ecbc5bbf34c2b9939a97f90a9f17309f221f540843fb875a10cd2507e74f5648e4c119733591bc45fa59f7526a7d591f27f3e5737b74f382568a5f69ec8f5c99aae5cfa207a3361aa29620692ee02bda75e39de9cd0b0ce97c3fe01d67853a9d4384620a0126555e886cb853541ab49521004b21575930d151ae8afa159b8adca942fb3f106d08a24cc158b4a3cfbf37dcbdde3d7d89363655b086777a30057fa8c152d60415c07f3058661647c4f4a38b6a2f70c909a6f50f101f9e6568074b8776c8092ed2823d9155973a536a419dbff5c30c5230cfce965fdc50159e1f96e450a3b3328f73c3e8b46c4c551f3cffafc25f0fca518b099a8dd8dd512379d54dcebce009d1cb607f9ccfb45a4cc9e1dbbff4c9088eb34efb974076b7902759f3a14ae11d324d981ed99f48d9d0cee945a5f9493b44a2f94c7b159350bbb1566213cc292e27fcb3c22488906fd240b2ba042f2dd7e4b93ea29f160af9e5acbcde62ed0a1940bd7bf909fd239389f123e7b7c31f559c5f1fb425d314fa5299ba149d58e911d80672699a24891bd9e8b3621b0cf25ab11c217524d63e71cb7f2eba5a84ab3303b668bac05d66fdaedf9b09f32107b3bc5fac854bd1e595b08b3157315ae50df85867dc6d976f7531f48f4a08713f338f46cd7e9eb7d9a13487efe6763f3e021b8ac317225ccea189b6e337935ccb231c67a7ae73b9131a78486480057e8f26563135c1be39d9fde90e0b5e2fe6ac29b1cb623ecf06136566622f1a4c48e22d6ceee8f46fbb3c86b9faee78b1f7355ea676fbc6b98db75334720b85906c9f8a3e12907e1e989407005105e1873ebb5a10f8d195b25fcbfb5f7c3128c85ca636b57f0d1297f2649d0679b71fb8508323af1a32aeaf1bddb95448e979d5e93f9e49fe22be6dc95dd23b3b4f703bbfeae2a8f0054b4b3ca5d062c556c8ea50411e78851e29bb7bbb925bcd290a1dc60d6cf57040586a1eb0f7624c286cb5e490add0ddbd35302b2c4bce07015578be17a699374a7fc3da21d65bfc0979fce95e06999f21633251b30a989614cdaaeda5fa486f4373898536c3db6930457b04a1014df99afddea176dfff029681853ea8515b3ca4a6da7c13e9804da2f43c27b2a95b1978ec66fd6fb72747791863fe1c1e51b0f958f2cf9cd363691d14afe974a66e3f6717d91b82452d952f690b5c2a256532ce23759f49d8482af56c5cd2161d8e2b71d5fed2496f9c9545ff4e2d9ba641f26fe8031133215e1bfaeafcad030de8f263ba128e29012c03d0cf54680e4df14803d43ece7bd42c117a4fa6de0c2bc266a619c7795b783ba00e1f6d168ed3bad7e6b62a9e9707dc22f2bcc7e2feba84b93ace7873473f5073e6a8d2e13ac8f88e137276454ac540c466f4dc5faa3aa3779308d3a66f532270b008330734f19f13bdb0d8be7ab37599b895361a14a47ca70a3e60f0f220571888f9ad684360ceb5036ea37d4e91512217c9d3d04cbfe963763cf251e2c8db55863d5caabc8b52ca4fccc202b832ffec6a0d6f5bc6a71256b1ac59e8f00a56a74b966d5f2806323d5290ef8d2e2a270d36f0863e1e869155edaf3911a582c7af41a2413a04ae28b708f931588f604e0e431fa5ebb33419469c186278bb6065620a3642469ea6c7af1b2d533d5fb8515f074703e629ab5fea059c46c4f828a03bd87e75c5c1c74b580a9383560d293fce95bc2a5be137b41eea0e0f6944504eac12730267d44121ed475da7d4321406067a365d4ad5bd689613a8f32d82b7856cba0424011548f7e15c23f5802c408ed99f665a0cf60789a73980f96dd7b9da25acf1cfb678ec993600051fe4eb133000eeb5f9e0852a", + "proofRequest": "02070102904e00277302f645a4c6b7ea56fc199ba39f31721d42d2dd08813bddb57bcfeb5e2c2d000002b3adb77e7e9cb97aefc80ac7bfa4d175b797b7a8f1717a3930c0c6bc42408fb901afa7403387c236561d4cf309aa562ee4e0c16a6604745ed09974db8a3946672136784e21bf4ffd676c1906835206c5f6e399141a0905802a9b7a18486cf0b53947edce3385360af8a3985609cebfeab72265b6334f75e725b95e6d13b0a4a836014015a1685ae0c1d414ba3728e0ddada6b2b2e96bffe7578b2c50d3c6c9c578c901b7058428a8ed7aa1430bb3eee54759f4582e1a3ba98ad58414e9aa8c25a69739012b37f1c1f4c922ecb98f3cf2dbe6f12f65a73fc93316a1c0ad4a23a8a3b9b4eadbb7687b6e82161d031698bf0180e8922601c9000000000000000000000000000000000000000000000000000000000000000135353535353535353535353535353535353535353535353535353535353535350001012038fe5156c36c78da72290b6d81ccade9932a4a83500aa8962f04a6fd593bd30c020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001d00375e8766803493c9ce953e0ab68b545e169223fe697c19adde56e0c86173e000103636d782033190bdef16c2df1b0f6d41903142f52660fde345f45b9c94d87e61a9f788516b3509d4b6f458ab08e2ddb24ba9bb188f43d4492167b727ca6322383de1d26060bfc1743d4556a9702277d2b3989446fff656b61830d1504f4f5ceafb0f0370dc404b5b066d80f695f611db079e1db68b2de1cf3148b18bc89bb00516c833ab30dff821e7e1af1eb5f1ce6741fbf3079761666540e025ccbb4f339352890a3b7f0858b3c4e3b3bf71e4591f77fb7731ca58a10ac880bf7acc776896c52160022be8fc72212b19ea7fe3ed1535b88ebb8e906f05564c66406c5f5c28257686a936ae54370f194772894a59dadd37283fd848b69b6b26ecb22a6b6c1f6d0c49bd782bb9ff467478643e73983af90d4620de3e2fc8447f3e251c42e3770d6ed9b57d51deac2810e648a5016a6ead566e49c1828a43167ce041b3a2719022336a947d1fa3eda81dfa75b3b8d753d031bbe04f929d7080d8f549830f3b616abea43d342b4f017528c68aef95278f18eda744cae46bb53553d0089e4030cea6158aebd1e31b6e7145f7b79fa704d7f556e0e1f6a9588208c376ed9ee8a193e79316f98977a2971b0dc0b6b175c6d567647bf9770703d6bee6714777f9542e8e31cac4b5c1b384a6af5b511291e606d7e8699e6114a2d953bcb5a4526e2b5942a108f9d10de854d33dbdbb87dc57fee8aae6f2362da808d004d1af99bef5177a9f3f9f3502c4764eba80deb3147a13c922d213003b388c49810aa7cc44f100f227161f6c76276d43e4ec861a1c2296e964df533c5821025e782e5484204eef0cc3dbfa36a58ba2ea8076e283af2d4adbaa7a5037f18ed796cb0d3412567a48b7ce201bdc475a543c9826d8a1bb25b666eef03d0c1fe822ace54b9e6d2a2dcad920e621685bb31280fff93f3fea83a674dc3d555d62a163ae4e908b1bd4f1459b1628812a95395bc9bf55021bf03ff6d04f84ed3f2ea3713f09dac4db712643e0b929315fc49711e79f880ec92fc3b002941cc0e10afc7f55f512a3d8041db69d5f7aaee6d5a8db962b7decff349190cb2dddac1e17f9598ab5f5f012bb22b122d2d03d30be6665844ac2fa20e5f177cef86a49ec93c195e6597af49d3b01b958617fce8170ce0a701f8edf42a014828e01738b0a8d784860040b6908a76ca6f17581c1788462a9195a48cc1e3ac0000000f864af500bca593d9b13fca0080029af59fb41a10987433c9996f208611b830015850d6cefdf244c13d203eef21973ea6f2e43411d8199e81447b91d89d43760c621c8910e2a41c7959bd4f2e685a9a917b94fb0ed734ba906c1af5ad75abfe0c7f53338dba978315293b7907496aa7872b472a6f01b5001af6a752b9b17975a20140a93920735ebeff8b694d6d62a26542ae182c8b11fa1038e4b6f8365c7b28c3ab6c37d1fcf809834b83bd4306593f64b7242ce4c879ff9b05564a4c1a69510523012b4559029c0b5dbf941c5ad181a5fe8f45b34630f29d0c8dd8dc1cc3573386f416cb324133156d723df5e62d0180c2d72f01c8000000000000000000000000000000000000000000000000000000000000000134343434343434343434343434343434343434343434343434343434343434340001002033190bdef16c2df1b0f6d41903142f52660fde345f45b9c94d87e61a9f788516020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001bbc54e0f2fcf43feed0a535886f82e3bc7d99ad7a07268b0d1199ef4279b9931000103636d782038fe5156c36c78da72290b6d81ccade9932a4a83500aa8962f04a6fd593bd30c28ddec0af49be6052349934b32afa84b18d8031cc035e7550a120a4954016e3d43f7e27fb426b524fd48757e03768bfe237361641ab602101cd457d95799e492c404bb7e1484e4860594894a05906c1b17ff1e758d22d5a3ebdc96690703db34b977fef5255236ffb468c5036fbac83b15e4c0fe85152192a1b383be5c809b2c348c48488186e49264e3a0e89801d9dd943adb9ec36cacf795c1529ba164d5260224634e8744146ab9e49bb60d0222f17bddfa8aad8df1ac1ba608acc1f5247d4e33c28c17c1dcbd76d6e5101911da5dc7ae75ebfbc47ee71cbf440f156843d845deb788e4cc9d1913964f4cd4fc6a0a2c9e242a49d66d7bb6b387cea5ccb3727fca1aa6dd79153df930cf54f5513582d5c28fb9b312fd6ac6ea790679bedc2ecdf6b28970cff57ca59e18fe3ee397c686d184a42d8e0188a356d9791a99921b332ade2e6cbfb5fc838407d8727e6ab66acd83b9288d0f86744d008aac094cf2b98a0d2226c5c4938850f71dd960b356cd412a0189b2361ae4fc51980d9e4c930aa935addf265560d0a99d300b86a2219beef3938ab04466b9e26e9eba5946b4142721fb97f6b9fbdcc520700b85811464db19daa6c97e6c4b87a4664195d9a6e8a9b1fb0e420b817e324a4d05887e093705428c554b90caf3e45c1b8c1ac475c0c3ffb39667fa63d6fb6374e0310a1e30255295d6c492bb260042c520b6fbdeec4d6faec13bb23cfe7b521b59b829c0342db81c2f07cc5a2bf115baea84a60459af44d40cca78a3cad130fbe5468e8e9bb1a1b5a5787c3bf3fd07707260e174f57c41f2a8936578248313b5e29f632ba5345c5cc0c02ad281b179a9132dd4912a64b4c3464da95b4026a820295597842b0e5e0c597d4429d9c39062ddfbdc7fc46a7fef681b5074cb7768c899b2991a0659bd775eb2bf45954e98856cc3659f9ce39abe8dd329f67a088141c44451dd599692d9fda74ccada5a47fd66f49cb74122e756de6600a08c99810a747848b1d9cd1fea91c7a6012b38b8c9ef7b9ad7d18319bff63ee4eee248c43176fbb19364bb81c8b5e65789f900bc05ec219b4e6c1979ae01f8edf42a0139a31c3e6ad71544f767a20334a7ace90376584b10ed8328d939a6f4c825cc25000000", + "psbt": "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AAAAAAAAAAAAATQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0AAEAIDMZC97xbC3xsPbUGQMUL1JmD940X0W5yU2H5hqfeIUWAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEDY214IDj+UVbDbHjacikLbYHMremTKkqDUAqoli8Epv1ZO9MMKN3sCvSb5gUjSZNLMq+oSxjYAxzANedVChIKSVQBbj1D9+J/tCa1JP1IdX4Ddov+I3NhZBq2AhAc1FfZV5nkksQEu34UhOSGBZSJSgWQbBsX/x51jSLVo+vclmkHA9s0uXf+9SVSNv+0aMUDb7rIOxXkwP6FFSGSobODvlyAmyw0jEhIgYbkkmTjoOiYAdndlDrbnsNsrPeVwVKboWTVJgIkY06HRBRqueSbtg0CIvF73fqKrY3xrBumCKzB9SR9TjPCjBfB3L121uUQGRHaXceudev7xH7nHL9EDxVoQ9hF3reI5MydGROWT0zU/GoKLJ4kKknWbXu2s4fOpcyzcn/KGqbdeRU9+TDPVPVRNYLVwo+5sxL9asbqeQZ5vtwuzfayiXDP9Xylnhj+PuOXxobRhKQtjgGIo1bZeRqZkhszKt4ubL+1/IOEB9hyfmq2as2DuSiND4Z0TQCKrAlM8rmKDSImxcSTiFD3Hdlgs1bNQSoBibI2GuT8UZgNnkyTCqk1rd8mVWDQqZ0wC4aiIZvu85OKsERmueJunrpZRrQUJyH7l/a5+9zFIHALhYEUZNsZ2qbJfmxLh6RmQZXZpuipsfsOQguBfjJKTQWIfgk3BUKMVUuQyvPkXBuMGsR1wMP/s5Zn+mPW+2N04DEKHjAlUpXWxJK7JgBCxSC2+97sTW+uwTuyPP57UhtZuCnANC24HC8HzFor8RW66oSmBFmvRNQMynijytEw++VGjo6bsaG1pXh8O/P9B3ByYOF09XxB8qiTZXgkgxO14p9jK6U0XFzAwCrSgbF5qRMt1JEqZLTDRk2pW0AmqCApVZeEKw5eDFl9RCnZw5Bi3fvcf8Rqf+9oG1Ahdr9r7S9FAU6bS38ZWTCXK7QUge9OxFc8SUpudP1sp6BQrFjIGQpjKe5+zqcV07iZmS02pNX4Zm9BIzzVmdhJP0fIu/YW/3oVSDmaW6m0DAErOLjJ73ua19GDGb/2PuTu4kjEMXb7sZNku4HIteZXifkAvAXsIZtObBl5rgH47fQqATmjHD5q1xVE92eiAzSnrOkDdlhLEO2DKNk5pvTIJcwlAAAAD/wMQklUR08vWkVDL1Y2AgSYtoTYD/wMQklUR08vWkVDL1Y2AwQAAAAAD/wMQklUR08vWkVDL1Y2CQEAAA==", + "randomizers": [ + { + "actionIndex": 1, + "alpha": "bbc54e0f2fcf43feed0a535886f82e3bc7d99ad7a07268b0d1199ef4279b9931", + "rk": "7f53338dba978315293b7907496aa7872b472a6f01b5001af6a752b9b17975a2" + }, + { + "actionIndex": 0, + "alpha": "d00375e8766803493c9ce953e0ab68b545e169223fe697c19adde56e0c86173e", + "rk": "47edce3385360af8a3985609cebfeab72265b6334f75e725b95e6d13b0a4a836" + } + ], + "request": { + "anchor": "277302f645a4c6b7ea56fc199ba39f31721d42d2dd08813bddb57bcfeb5e2c2d", + "outputs": [ + { + "amount": "89995000", + "memo": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "recipient": "38b8c9ef7b9ad7d18319bff63ee4eee248c43176fbb19364bb81c8b5e65789f900bc05ec219b4e6c1979ae", + "unifiedAddress": null + }, + { + "amount": "89995000", + "memo": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "recipient": "b22b122d2d03d30be6665844ac2fa20e5f177cef86a49ec93c195e6597af49d3b01b958617fce8170ce0a7", + "unifiedAddress": null + } + ], + "spends": [ + { + "cmx": "38fe5156c36c78da72290b6d81ccade9932a4a83500aa8962f04a6fd593bd30c", + "nullifier": "621c8910e2a41c7959bd4f2e685a9a917b94fb0ed734ba906c1af5ad75abfe0c", + "recipient": "4559029c0b5dbf941c5ad181a5fe8f45b34630f29d0c8dd8dc1cc3573386f416cb324133156d723df5e62d", + "rho": "c800000000000000000000000000000000000000000000000000000000000000", + "rseed": "3434343434343434343434343434343434343434343434343434343434343434", + "value": "100000000", + "witness": { + "authPath": [ + "33190bdef16c2df1b0f6d41903142f52660fde345f45b9c94d87e61a9f788516", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000" + ], + "position": 0 + } + }, + { + "cmx": "33190bdef16c2df1b0f6d41903142f52660fde345f45b9c94d87e61a9f788516", + "nullifier": "36784e21bf4ffd676c1906835206c5f6e399141a0905802a9b7a18486cf0b539", + "recipient": "37f1c1f4c922ecb98f3cf2dbe6f12f65a73fc93316a1c0ad4a23a8a3b9b4eadbb7687b6e82161d031698bf", + "rho": "c900000000000000000000000000000000000000000000000000000000000000", + "rseed": "3535353535353535353535353535353535353535353535353535353535353535", + "value": "80000000", + "witness": { + "authPath": [ + "38fe5156c36c78da72290b6d81ccade9932a4a83500aa8962f04a6fd593bd30c", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000", + "0200000000000000000000000000000000000000000000000000000000000000" + ], + "position": 1 + } + } + ], + "transparentOutput": null + }, + "sighash": "160ba968519c69dd167f515eca009fbdef430816d870167aa64088f7728179b8", + "signatures": [ + { + "actionIndex": 1, + "signature": "a93920735ebeff8b694d6d62a26542ae182c8b11fa1038e4b6f8365c7b28c3ab6c37d1fcf809834b83bd4306593f64b7242ce4c879ff9b05564a4c1a69510523" + }, + { + "actionIndex": 0, + "signature": "15a1685ae0c1d414ba3728e0ddada6b2b2e96bffe7578b2c50d3c6c9c578c901b7058428a8ed7aa1430bb3eee54759f4582e1a3ba98ad58414e9aa8c25a69739" + } + ], + "spendActionIndices": [1, 0], + "zip317ConventionalFee": "10000" +}