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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/wasm-utxo/js/descriptorWallet/Psbt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export class Psbt extends PsbtBase<WasmPsbt> implements IPsbt {
this._wasm.update_output_with_descriptor(outputIndex, descriptor);
}

/** Add a 32-byte SHA256 preimage for Miniscript descriptor finalization. */
addSha256Preimage(inputIndex: number, preimage: Uint8Array): void {
this._wasm.add_sha256_preimage(inputIndex, preimage);
}

// -- Signing --

signWithXprv(xprv: string): SignPsbtResult {
Expand Down Expand Up @@ -140,6 +145,11 @@ export class Psbt extends PsbtBase<WasmPsbt> implements IPsbt {
this._wasm.finalize_mut();
}

/** Finalize one Miniscript input without requiring all inputs to be complete. */
finalizeInput(inputIndex: number): void {
this._wasm.finalize_input(inputIndex);
}

extractTransaction(): Transaction {
return Transaction.fromWasm(this._wasm.extract_transaction());
}
Expand Down
29 changes: 29 additions & 0 deletions packages/wasm-utxo/src/wasm/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::wasm::psbt_ops::WasmPsbtOps;
use crate::wasm::try_into_js_value::TryIntoJsValue;
use crate::wasm::WrapDescriptor;
use crate::zcash::transaction::{ZcashTransactionParts, ZCASH_SAPLING_VERSION_GROUP_ID};
use miniscript::bitcoin::hashes::{sha256, Hash};
use miniscript::bitcoin::locktime::absolute::LockTime;
use miniscript::bitcoin::secp256k1::Secp256k1;
use miniscript::bitcoin::transaction::{Transaction, Version};
Expand Down Expand Up @@ -358,6 +359,27 @@ impl WrapPsbt {
}
}

/// Add a 32-byte SHA256 preimage to an input's standard BIP174 metadata.
///
/// Descriptor finalization uses this metadata to satisfy `sha256(H)`
/// Miniscript fragments when `H == sha256(preimage)`.
pub fn add_sha256_preimage(
&mut self,
input_index: usize,
preimage: Vec<u8>,
) -> Result<(), WasmUtxoError> {
let preimage: [u8; 32] = preimage
.try_into()
.map_err(|_| WasmUtxoError::new("sha256 preimage must be 32 bytes"))?;
let input = self.0.inputs.get_mut(input_index).ok_or_else(|| {
WasmUtxoError::new(&format!("Input index {} out of bounds", input_index))
})?;
input
.sha256_preimages
.insert(sha256::Hash::hash(&preimage), preimage.to_vec());
Ok(())
}

pub fn sign_with_xprv(&mut self, xprv: String) -> Result<JsValue, WasmUtxoError> {
let key = bip32::Xpriv::from_str(&xprv).map_err(|_| WasmUtxoError::new("Invalid xprv"))?;
self.0
Expand Down Expand Up @@ -528,6 +550,13 @@ impl WrapPsbt {
.map_err(WasmUtxoError::from_errors)
}

/// Finalize one Miniscript input, preserving any other incomplete inputs.
pub fn finalize_input(&mut self, input_index: usize) -> Result<(), WasmUtxoError> {
self.0
.finalize_inp_mut(&Secp256k1::verification_only(), input_index)
.map_err(|error| WasmUtxoError::new(&error.to_string()))
}

/// Finalize all Zcash transparent inputs using ZIP-243 sighash verification.
///
/// Use this instead of `finalize_mut()` for Zcash PSBTs signed with
Expand Down
81 changes: 80 additions & 1 deletion packages/wasm-utxo/test/pox5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
type DescriptorNode,
type MiniscriptNode,
} from "../js/ast/index.js";
import { Descriptor, Miniscript } from "../js/index.js";
import { Descriptor, Miniscript, Psbt } from "../js/index.js";
import { getKey, getKeyTriple } from "../js/testutils/keys.js";

// PoX-5 Bitcoin Staking lockup script (P2WSH + CLTV conditional branch).
//
Expand Down Expand Up @@ -72,6 +73,64 @@ const POX5_DESCRIPTOR_NODE: DescriptorNode = { wsh: POX5_MINISCRIPT_NODE };
const POX5_MINISCRIPT = formatNode(POX5_MINISCRIPT_NODE);
const POX5_DESCRIPTOR = formatNode(POX5_DESCRIPTOR_NODE);

function createEarlyExitPsbt(): {
psbt: Psbt;
principalPreimage: Buffer;
} {
const [user, backup, bitgo] = getKeyTriple("pox5-finalization");
const earlyExit = getKey("pox5-early-exit");
const incompleteKey = getKey("pox5-incomplete");
const principalPreimage = Buffer.alloc(32, 0x42);
const descriptor = Descriptor.fromString(
formatNode({
wsh: {
and_v: [
{
"v:or_i": [
{ after: UNLOCK_HEIGHT },
{
and_v: [
{
"v:sha256": crypto.createHash("sha256").update(principalPreimage).digest("hex"),
},
{ pk: Buffer.from(earlyExit.publicKey).toString("hex") },
],
},
],
},
{
multi: [
2,
Buffer.from(user.publicKey).toString("hex"),
Buffer.from(backup.publicKey).toString("hex"),
Buffer.from(bitgo.publicKey).toString("hex"),
],
},
],
},
}),
"definite",
);
const scriptPubKey = descriptor.scriptPubkey();
const psbt = Psbt.create(2, 0);
psbt.addInput("01".repeat(32), 0, 100_000n, scriptPubKey, 0xfffffffe);
psbt.addOutput(scriptPubKey, 90_000n);
psbt.updateInputWithDescriptor(0, descriptor);

const incompleteDescriptor = Descriptor.fromString(
formatNode({ wsh: { pk: Buffer.from(incompleteKey.publicKey).toString("hex") } }),
"definite",
);
psbt.addInput("02".repeat(32), 0, 100_000n, incompleteDescriptor.scriptPubkey(), 0xfffffffe);
psbt.updateInputWithDescriptor(1, incompleteDescriptor);

for (const key of [user, backup, earlyExit]) {
assert.ok(key.privateKey, "test key must include private key material");
psbt.signWithPrv(key.privateKey);
}
return { psbt, principalPreimage };
}

// Expected script flow, verified structurally against construct-lockup-script (pox-5.clar:3711-3732).
//
// OP_IF <height> OP_CLTV
Expand Down Expand Up @@ -162,6 +221,26 @@ describe("PoX-5 Bitcoin Staking lockup script", function () {
});
});

describe("PSBT early-exit finalization", function () {
it("satisfies the SHA256 branch from standard PSBT preimage metadata", function () {
const { psbt, principalPreimage } = createEarlyExitPsbt();

// Leave a second descriptor input incomplete to prove this only finalizes
// the requested input rather than requiring every input to be complete.
assert.throws(() => psbt.finalizeInput(0), /satisfy|preimage|finalize/i);
psbt.addSha256Preimage(0, principalPreimage);
psbt.finalizeInput(0);

assert.deepStrictEqual(psbt.getPartialSignatures(0), []);
assert.throws(() => psbt.finalizeInput(1), /satisfy|signature|finalize/i);
});

it("rejects non-32-byte SHA256 preimages", function () {
const { psbt } = createEarlyExitPsbt();
assert.throws(() => psbt.addSha256Preimage(0, Buffer.alloc(31)), /32 bytes/);
});
});

describe("AST round-trip", function () {
it("fromDescriptor produces expected formatNode output", function () {
const desc = Descriptor.fromString(POX5_DESCRIPTOR, "definite");
Expand Down
Loading