diff --git a/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts b/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts index d9856a4b344..5b1f4763789 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/ZcashIronwoodBitGoPsbt.ts @@ -352,4 +352,21 @@ export class ZcashIronwoodBitGoPsbt extends ZcashBitGoPsbt { combineProof(proof: Uint8Array): Uint8Array { return this.wasm.combine_ironwood_proof(proof); } + + /** + * The raw serialized orchard PCZT (Partially Created Zcash Transaction) bundle stored in this + * PSBT, or `undefined` if none is present. + * + * 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 {@link combineProof} has + * 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). + */ + getPczt(): Uint8Array | undefined { + return this.wasm.ironwood_pczt_bytes(); + } } diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs index da862a67d61..49bae9d0b9f 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs @@ -719,6 +719,12 @@ impl ZcashBitGoPsbt { crate::zcash::ironwood_pczt::deserialize_pczt(&bytes).map_err(|e| e.to_string()) } + /// The raw serialized orchard PCZT bytes stored in the proprietary map, or `None` if absent + /// (before [`Self::add_ironwood_output`], or after [`Self::combine_ironwood_proof`] drops it). + pub fn raw_ironwood_pczt_bytes(&self) -> Option> { + super::propkv::get_ironwood_pczt(&self.psbt) + } + /// Client-managed `ovk`, raw-key form: re-encrypt the Ironwood output's `out_ciphertext` under /// an `ovk` derived as the ECDH agreement of `bitgo_pubkey` and `user_privkey`. The `ovk` never /// leaves this call — it is not returned and not persisted anywhere. diff --git a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs index 72b0c4dee2a..b536580d657 100644 --- a/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs +++ b/packages/wasm-utxo/src/wasm/fixed_script_wallet/mod.rs @@ -673,6 +673,22 @@ impl BitGoPsbt { Ok(tx) } + /// The raw serialized orchard PCZT (Partially Created Zcash Transaction) bundle stored in + /// this v6 (Ironwood) PSBT's proprietary key-value map, or `undefined` if none is present. + /// + /// 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 + /// [`crate::zcash::ironwood_pczt`] / [`crate::zcash::ironwood_build`]). + /// + /// Present only after [`Self::add_ironwood_output`] has been called on a v6 PSBT; `undefined` + /// beforehand, and `undefined` again after [`Self::combine_ironwood_proof`] succeeds, since + /// that call drops the stored PCZT to make extraction terminal (see its doc comment). + pub fn ironwood_pczt_bytes(&self) -> Option> { + self.zcash().ok().and_then(|z| z.raw_ironwood_pczt_bytes()) + } + /// Convert a half-signed legacy transaction to a psbt-lite. /// /// # Arguments diff --git a/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts b/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts index de7dbbbf91d..fe38a28a7db 100644 --- a/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts +++ b/packages/wasm-utxo/test/fixedScript/zcashIronwoodPsbt.ts @@ -185,6 +185,32 @@ describe("ZcashIronwoodBitGoPsbt v6 (Ironwood)", function () { }); }); + describe("getPczt", function () { + it("is undefined before a shielded output has been added", function () { + const psbt = ZcashIronwoodBitGoPsbt.createEmpty("zcashTest", walletKeys, { + blockHeight: NU6_3_TESTNET_HEIGHT, + }); + assert.strictEqual(psbt.getPczt(), undefined); + }); + + it("is present after addShieldedOutput, and survives a serialize round-trip", function () { + const psbt = buildShieldPsbt(); + const pczt = psbt.getPczt(); + assert.ok(pczt instanceof Uint8Array); + assert.ok(pczt.length > 0); + + const round = ZcashIronwoodBitGoPsbt.fromBytes(psbt.serialize(), "zcashTest"); + assert.deepStrictEqual(round.getPczt(), pczt); + }); + + it("stays present when combineProof fails, so the call is retryable", function () { + const psbt = buildShieldPsbt(); + const pczt = psbt.getPczt(); + assert.throws(() => psbt.combineProof(new Uint8Array(192))); + assert.deepStrictEqual(psbt.getPczt(), pczt); + }); + }); + it("the default memo is the ZIP-302 no-memo encoding, not all zeros", function () { // `addShieldedOutput` defaults `memo` to this. Asserted on the encoding rather than by comparing // txids across two builds: `construct_shield_pczt` draws a random rseed, so two separately-built @@ -421,8 +447,10 @@ describe("ZcashIronwoodBitGoPsbt v6 (Ironwood)", function () { // A placeholder proof of the real (4992-byte, single-action) size stands in for the external // prover; the transparent side is what this test actually exercises. + assert.ok(psbt.getPczt() !== undefined, "PCZT present before combine"); const tx = psbt.combineProof(new Uint8Array(4992)); assert.ok(tx.length > 0, "produced a broadcast-ready v6 transaction"); + assert.strictEqual(psbt.getPczt(), undefined, "combineProof drops the stored PCZT"); }); it("accepts any WalletKeysArg form, not just a RootWalletKeys instance", function () {