Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Psbt APIs #305

Merged
merged 9 commits into from
Mar 14, 2022
7 changes: 3 additions & 4 deletions examples/psbt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ extern crate miniscript;

use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex;

use miniscript::psbt::{extract, finalize};
use miniscript::psbt::PsbtExt;

fn main() {
// Test vectors from BIP 174
Expand All @@ -18,13 +17,13 @@ fn main() {
let secp = bitcoin::secp256k1::Secp256k1::verification_only();
// Assuming all partial sigs are filled in.
// Construct a generic finalizer
finalize(&mut psbt, &secp).unwrap();
psbt.finalize_mut(&secp).unwrap();
// println!("{:?}", psbt);

assert_eq!(psbt, expected_finalized_psbt);

// Extract the transaction from the psbt
let tx = extract(&psbt, &secp).unwrap();
let tx = psbt.extract(&secp).unwrap();

let expected: bitcoin::Transaction = deserialize(&Vec::<u8>::from_hex("0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000").unwrap()).unwrap();
// println!("{:?}", tx);
Expand Down
19 changes: 14 additions & 5 deletions integration_test/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
use bitcoin::{Amount, OutPoint, Transaction, TxIn, TxOut, Txid};
mod read_file;
use miniscript::miniscript::iter;
use miniscript::psbt::PsbtExt;
use miniscript::DescriptorTrait;
use miniscript::MiniscriptKey;
use miniscript::{Miniscript, Segwitv0};
Expand Down Expand Up @@ -196,7 +197,9 @@ fn main() {
let amt = btc(1).as_sat();
let mut sighash_cache = bitcoin::util::sighash::SigHashCache::new(&psbts[i].unsigned_tx);
let sighash_ty = bitcoin::EcdsaSigHashType::All;
let sighash = sighash_cache.segwit_signature_hash(0, &ms.encode(), amt, sighash_ty).unwrap();
let sighash = sighash_cache
.segwit_signature_hash(0, &ms.encode(), amt, sighash_ty)
.unwrap();

// requires both signing and verification because we check the tx
// after we psbt extract it
Expand All @@ -207,7 +210,13 @@ fn main() {
for sk in sks_reqd {
let sig = secp.sign_ecdsa(&msg, &sk);
let pk = pks[sks.iter().position(|&x| x == sk).unwrap()];
psbts[i].inputs[0].partial_sigs.insert(pk.inner, bitcoin::EcdsaSig { sig, hash_ty: sighash_ty });
psbts[i].inputs[0].partial_sigs.insert(
pk.inner,
bitcoin::EcdsaSig {
sig,
hash_ty: sighash_ty,
},
);
}
// Add the hash preimages to the psbt
psbts[i].inputs[0].sha256_preimages.insert(
Expand All @@ -229,11 +238,11 @@ fn main() {
);
// Finalize the transaction using psbt
// Let miniscript do it's magic!
if let Err(e) = miniscript::psbt::finalize_mall(&mut psbts[i], &secp) {
if let Err(e) = psbts[i].finalize_mall_mut(&secp) {
// All miniscripts should satisfy
panic!("Could not satisfy: error{} ms:{} at ind:{}", e, ms, i);
panic!("Could not satisfy: error{} ms:{} at ind:{}", e[0], ms, i);
} else {
let tx = miniscript::psbt::extract(&psbts[i], &secp).unwrap();
let tx = psbts[i].extract(&secp).unwrap();

// Send the transactions to bitcoin node for mining.
// Regtest mode has standardness checks
Expand Down
2 changes: 1 addition & 1 deletion src/descriptor/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ impl FromStr for DescriptorPublicKey {
}

/// Descriptor key conversion error
#[derive(Debug, PartialEq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum ConversionError {
/// Attempted to convert a key with a wildcard to a bitcoin public key
Wildcard,
Expand Down
171 changes: 83 additions & 88 deletions src/psbt/finalizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@
//!

use bitcoin::util::sighash::Prevouts;
use util::{script_is_v1_tr, witness_size};
use util::witness_size;

use super::{sanity_check, Psbt};
use super::{Error, InputError, PsbtInputSatisfier};
use bitcoin::blockdata::witness::Witness;
use bitcoin::secp256k1::{self, Secp256k1};
use bitcoin::util::key::XOnlyPublicKey;
use bitcoin::util::taproot::LeafVersion;
use bitcoin::{self, EcdsaSigHashType, PublicKey, Script};
use bitcoin::{self, PublicKey, Script};
use descriptor::DescriptorTrait;
use interpreter;
use Descriptor;
Expand All @@ -45,7 +45,7 @@ fn construct_tap_witness(
sat: &PsbtInputSatisfier,
allow_mall: bool,
) -> Result<Vec<Vec<u8>>, InputError> {
assert!(script_is_v1_tr(&spk));
assert!(spk.is_v1_p2tr());

// try the script spend path first
if let Some(sig) =
Expand Down Expand Up @@ -97,12 +97,12 @@ fn construct_tap_witness(
}

// Get the scriptpubkey for the psbt input
fn get_scriptpubkey(psbt: &Psbt, index: usize) -> Result<&Script, InputError> {
pub(super) fn get_scriptpubkey(psbt: &Psbt, index: usize) -> Result<&Script, InputError> {
get_utxo(psbt, index).map(|utxo| &utxo.script_pubkey)
}

// Get the spending utxo for this psbt input
fn get_utxo(psbt: &Psbt, index: usize) -> Result<&bitcoin::TxOut, InputError> {
pub(super) fn get_utxo(psbt: &Psbt, index: usize) -> Result<&bitcoin::TxOut, InputError> {
let inp = &psbt.inputs[index];
let utxo = if let Some(ref witness_utxo) = inp.witness_utxo {
&witness_utxo
Expand All @@ -116,7 +116,7 @@ fn get_utxo(psbt: &Psbt, index: usize) -> Result<&bitcoin::TxOut, InputError> {
}

/// Get the Prevouts for the psbt
fn prevouts<'a>(psbt: &'a Psbt) -> Result<Vec<bitcoin::TxOut>, super::Error> {
pub(super) fn prevouts<'a>(psbt: &'a Psbt) -> Result<Vec<bitcoin::TxOut>, super::Error> {
let mut utxos = vec![];
for i in 0..psbt.inputs.len() {
let utxo_ref = get_utxo(psbt, i).map_err(|e| Error::InputError(e, i))?;
Expand Down Expand Up @@ -284,31 +284,43 @@ pub fn interpreter_check<C: secp256k1::Verification>(
) -> Result<(), Error> {
let utxos = prevouts(&psbt)?;
let utxos = &Prevouts::All(&utxos);
for (index, input) in psbt.inputs.iter().enumerate() {
let spk = get_scriptpubkey(psbt, index).map_err(|e| Error::InputError(e, index))?;
let empty_script_sig = Script::new();
let empty_witness = Witness::default();
let script_sig = input.final_script_sig.as_ref().unwrap_or(&empty_script_sig);
let witness = input
.final_script_witness
.as_ref()
.map(|wit_slice| Witness::from_vec(wit_slice.to_vec())) // TODO: Update rust-bitcoin psbt API to use witness
.unwrap_or(empty_witness);
for (index, _input) in psbt.inputs.iter().enumerate() {
interpreter_inp_check(psbt, secp, index, utxos)?;
}
Ok(())
}

// Now look at all the satisfied constraints. If everything is filled in
// corrected, there should be no errors
// Interpreter check
{
let cltv = psbt.unsigned_tx.lock_time;
let csv = psbt.unsigned_tx.input[index].sequence;
let interpreter =
interpreter::Interpreter::from_txdata(spk, &script_sig, &witness, cltv, csv)
.map_err(|e| Error::InputError(InputError::Interpreter(e), index))?;
let iter = interpreter.iter(secp, &psbt.unsigned_tx, index, &utxos);
if let Some(error) = iter.filter_map(Result::err).next() {
return Err(Error::InputError(InputError::Interpreter(error), index));
};
}
// Run the miniscript interpreter on a single psbt input
fn interpreter_inp_check<C: secp256k1::Verification>(
psbt: &Psbt,
secp: &Secp256k1<C>,
index: usize,
utxos: &Prevouts,
) -> Result<(), Error> {
let input = &psbt.inputs[index];
let spk = get_scriptpubkey(psbt, index).map_err(|e| Error::InputError(e, index))?;
let empty_script_sig = Script::new();
let empty_witness = Witness::default();
let script_sig = input.final_script_sig.as_ref().unwrap_or(&empty_script_sig);
let witness = input
.final_script_witness
.as_ref()
.map(|wit_slice| Witness::from_vec(wit_slice.to_vec())) // TODO: Update rust-bitcoin psbt API to use witness
.unwrap_or(empty_witness);

// Now look at all the satisfied constraints. If everything is filled in
// corrected, there should be no errors
// Interpreter check
{
let cltv = psbt.unsigned_tx.lock_time;
let csv = psbt.unsigned_tx.input[index].sequence;
let interpreter =
interpreter::Interpreter::from_txdata(spk, &script_sig, &witness, cltv, csv)
.map_err(|e| Error::InputError(InputError::Interpreter(e), index))?;
let iter = interpreter.iter(secp, &psbt.unsigned_tx, index, &utxos);
if let Some(error) = iter.filter_map(Result::err).next() {
return Err(Error::InputError(InputError::Interpreter(error), index));
};
}
Ok(())
}
Expand All @@ -322,6 +334,7 @@ pub fn interpreter_check<C: secp256k1::Verification>(
/// finalized psbt which involves checking the signatures/ preimages/timelocks.
/// The functions fails it is not possible to satisfy any of the inputs non-malleably
/// See [finalize_mall] if you want to allow malleable satisfactions
#[deprecated(since = "7.0", note = "Please use PsbtExt::finalize instead")]
pub fn finalize<C: secp256k1::Verification>(
psbt: &mut Psbt,
secp: &Secp256k1<C>,
Expand All @@ -344,64 +357,45 @@ pub fn finalize_helper<C: secp256k1::Verification>(
) -> Result<(), super::Error> {
sanity_check(psbt)?;

// Check well-formedness of input data
for (n, input) in psbt.inputs.iter().enumerate() {
// TODO: fix this after https://github.com/rust-bitcoin/rust-bitcoin/issues/838
let target_ecdsa_sighash_ty = match input.sighash_type {
Some(psbt_hash_ty) => psbt_hash_ty
.ecdsa_hash_ty()
.map_err(|e| Error::InputError(InputError::NonStandardSigHashType(e), n))?,
None => EcdsaSigHashType::All,
};
for (key, ecdsa_sig) in &input.partial_sigs {
let flag = bitcoin::EcdsaSigHashType::from_u32_standard(ecdsa_sig.hash_ty as u32)
.map_err(|_| {
super::Error::InputError(
InputError::Interpreter(interpreter::Error::NonStandardSigHash(
ecdsa_sig.to_vec(),
)),
n,
)
})?;
if target_ecdsa_sighash_ty != flag {
return Err(Error::InputError(
InputError::WrongSigHashFlag {
required: target_ecdsa_sighash_ty,
got: flag,
pubkey: bitcoin::PublicKey::new(*key),
},
n,
));
}
// Signatures are well-formed in psbt partial sigs
}
}

// Actually construct the witnesses
for index in 0..psbt.inputs.len() {
let (witness, script_sig) = {
let spk = get_scriptpubkey(psbt, index).map_err(|e| Error::InputError(e, index))?;
let sat = PsbtInputSatisfier::new(&psbt, index);
finalize_input(psbt, index, secp, allow_mall)?;
}
// Interpreter is already run inside finalize_input for each input
Ok(())
}

if script_is_v1_tr(spk) {
// Deal with tr case separately, unfortunately we cannot infer the full descriptor for Tr
let wit = construct_tap_witness(spk, &sat, allow_mall)
.map_err(|e| Error::InputError(e, index))?;
(wit, Script::new())
} else {
// Get a descriptor for this input.
let desc = get_descriptor(&psbt, index).map_err(|e| Error::InputError(e, index))?;
pub(super) fn finalize_input<C: secp256k1::Verification>(
psbt: &mut Psbt,
index: usize,
secp: &Secp256k1<C>,
allow_mall: bool,
) -> Result<(), super::Error> {
let (witness, script_sig) = {
let spk = get_scriptpubkey(psbt, index).map_err(|e| Error::InputError(e, index))?;
let sat = PsbtInputSatisfier::new(&psbt, index);
Copy link
Member

@apoelstra apoelstra Mar 11, 2022

Choose a reason for hiding this comment

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

In 815d012:

I think we should make this function generic over the satisfier type, and expose additional methods in the PsbtExt trait that users can use to provide their own Satisfiers. (The existing methods would have this line let sat = PsbtInputSatisfier::new(&psbt, index); and pass that into this function.)

I think this, maybe in conjunction with some extensions to the Satisfier trait, would give Jeremy the tools to implement what he's getting at here.

Edit: never mind, I was confused about finalizing vs updating

Copy link
Contributor

@JeremyRubin JeremyRubin Mar 11, 2022

Choose a reason for hiding this comment

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

i actually think that here, this is OK. Since we're finalizing here, not signing, all the data should already be in here.

What might work is an API that takes a PSBT input as a Processing(PsbtInputRef) and then you can call many fn try_sat(self, s: dyn Satisfier) -> Processing and then lastly call Processing::finalize_input(self) -> Result<Psbt, Error> or something like that which gives you back your Psbt in the end.

This way you could try passing the input in the processing phase to multiple signers, without doing intepreter checks, and then do a final "try to finalize call" which then either succeeds or fails.

One important cleanup: if it does fail, we should make sure the PSBT is not modified at all so that we can retry signing. edit: I guess you can always clone the psbt before you try...

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yep, I understand.

Your proposed API is interesting but I think too complicated for this API (and this major rev).


//generate the satisfaction witness and scriptsig
if !allow_mall {
desc.get_satisfaction(PsbtInputSatisfier::new(&psbt, index))
} else {
desc.get_satisfaction_mall(PsbtInputSatisfier::new(&psbt, index))
}
.map_err(|e| Error::InputError(InputError::MiniscriptError(e), index))?
if spk.is_v1_p2tr() {
// Deal with tr case separately, unfortunately we cannot infer the full descriptor for Tr
let wit = construct_tap_witness(spk, &sat, allow_mall)
.map_err(|e| Error::InputError(e, index))?;
(wit, Script::new())
} else {
// Get a descriptor for this input.
let desc = get_descriptor(&psbt, index).map_err(|e| Error::InputError(e, index))?;

//generate the satisfaction witness and scriptsig
let sat = PsbtInputSatisfier::new(&psbt, index);
if !allow_mall {
desc.get_satisfaction(sat)
} else {
desc.get_satisfaction_mall(sat)
}
};
.map_err(|e| Error::InputError(InputError::MiniscriptError(e), index))?
}
};

{
let input = &mut psbt.inputs[index];
//Fill in the satisfactions
input.final_script_sig = if script_sig.is_empty() {
Expand Down Expand Up @@ -434,16 +428,17 @@ pub fn finalize_helper<C: secp256k1::Verification>(
input.tap_internal_key = None; // x017
input.tap_merkle_root = None; // 0x018
}
// Double check everything with the interpreter
// This only checks whether the script will be executed
// correctly by the bitcoin interpreter under the current
// psbt context.
interpreter_check(&psbt, secp)?;
let utxos = prevouts(&psbt)?;
let utxos = &Prevouts::All(&utxos);
interpreter_inp_check(psbt, secp, index, utxos)?;

Ok(())
}

#[cfg(test)]
mod tests {
use psbt::PsbtExt;

use super::*;

use bitcoin::consensus::encode::deserialize;
Expand All @@ -454,7 +449,7 @@ mod tests {
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = deserialize(&Vec::<u8>::from_hex("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000").unwrap()).unwrap();

let secp = Secp256k1::verification_only();
finalize(&mut psbt, &secp).unwrap();
psbt.finalize_mut(&secp).unwrap();

let expected: bitcoin::util::psbt::PartiallySignedTransaction = deserialize(&Vec::<u8>::from_hex("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000").unwrap()).unwrap();
assert_eq!(psbt, expected);
Expand Down
Loading