qa: replace our PSBT implementation with a tweaked version of Bitcoin Core's

This replaces our existing implementation of PSBTs with a more
straightforward one, adapted from the Bitcoin Core functional tests
framework. This fixes a few flakes that occured because the previous
implementation could produce invalid PSBTs.

The Bitcoin Core implementation is pretty low level and was adapted to
treat mappings as such (the value in the PSBTMap can itself be a
mapping, like for partial signatures or BIP32 derivation paths).

The rest of the diff is adapting the users of PSBT to use the new
implementation and the clearly superior interface (yay!).
This commit is contained in:
Antoine Poinsot 2022-10-19 12:00:54 +02:00
parent eff39ee35a
commit af9f0aeaed
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
4 changed files with 190 additions and 474 deletions

View File

@ -16,6 +16,10 @@ from test_framework.serializations import (
sighash_all_witness,
CTxInWitness,
CScriptWitness,
PSBT_IN_BIP32_DERIVATION,
PSBT_IN_WITNESS_SCRIPT,
PSBT_IN_PARTIAL_SIG,
PSBT_IN_FINAL_SCRIPTWITNESS,
)
@ -65,11 +69,12 @@ class Minisafed(TailableProc):
assert isinstance(psbt, PSBT)
# Sign each input.
for i, psbt_in in enumerate(psbt.inputs):
for i, psbt_in in enumerate(psbt.i):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
der_index = next(iter(psbt_in.hd_keypaths.values()))[1]
script_code = psbt_in.witness_script
# 'hd_keypaths' is of the form {pubkey: (fingerprint (4 bytes), derivation index (4 bytes))}
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
der_index = int.from_bytes(fing_der[4:], byteorder="little", signed=True)
script_code = psbt_in.map[PSBT_IN_WITNESS_SCRIPT]
# Now sign the transaction with the key of the "owner" (the participant that
# can sign immediately without a timelock)
@ -78,10 +83,14 @@ class Minisafed(TailableProc):
self.owner_hd.get_privkey_from_path([der_index])
)
pubkey = privkey.public_key.format()
assert pubkey in psbt_in.hd_keypaths.keys()
assert pubkey in psbt_in.map[PSBT_IN_BIP32_DERIVATION].keys(), (
pubkey,
psbt_in.map[PSBT_IN_BIP32_DERIVATION].keys(),
)
sig = privkey.sign(sighash, hasher=None) + b"\x01"
logging.debug(f"Adding signature {sig.hex()} for pubkey {pubkey.hex()}")
psbt_in.partial_sigs[pubkey] = sig
assert PSBT_IN_PARTIAL_SIG not in psbt_in.map
psbt_in.map[PSBT_IN_PARTIAL_SIG] = {pubkey: sig}
return psbt
@ -95,25 +104,28 @@ class Minisafed(TailableProc):
assert isinstance(psbt, PSBT)
# Create a witness for each input of the transaction.
for i, psbt_in in enumerate(psbt.inputs):
for i, psbt_in in enumerate(psbt.i):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
der_index = next(iter(psbt_in.hd_keypaths.values()))[1]
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
der_index = int.from_bytes(fing_der[4:], byteorder="little", signed=True)
# Create a copy of the descriptor to derive it at the index used in this input.
# Then create a satisfaction for it using the signature we just created.
desc = Descriptor.from_str(str(self.main_desc))
desc.derive(der_index)
sat_material = SatisfactionMaterial(
signatures=psbt_in.partial_sigs,
signatures=psbt_in.map[PSBT_IN_PARTIAL_SIG],
)
stack = desc.satisfy(sat_material)
logging.debug(f"Satisfaction for {desc} is {[e.hex() for e in stack]}")
# Update the transaction inside the PSBT directly.
assert stack is not None
psbt_in.final_script_witness = CTxInWitness(CScriptWitness(stack))
psbt.tx.wit.vtxinwit.append(psbt_in.final_script_witness)
psbt_in.map[PSBT_IN_FINAL_SCRIPTWITNESS] = CTxInWitness(
CScriptWitness(stack)
)
psbt.tx.wit.vtxinwit.append(psbt_in.map[PSBT_IN_FINAL_SCRIPTWITNESS])
return psbt

View File

@ -1,9 +1,13 @@
#!/usr/bin/env python3
# Stolen from https://github.com/achow101/psbt-simple-signer/blob/5def3622a09f5bcb76ae79707f0790d050291474/serializations.py
# PSBT serialization was authored by Andrew Chow (achow101)
#
# Taken then adapted from:
# - Initially https://github.com/achow101/psbt-simple-signer/blob/5def3622a09f5bcb76ae79707f0790d050291474/serializations.py
# - Then from the October 2022 Bitcoin Core functional test for the new PSBTMap class
#
# Copyright (c) 2010 ArtForz -- public domain half-a-node
# Copyright (c) 2012 Jeff Garzik
# Copyright (c) 2010-2016 The Bitcoin Core developers
# Copyright (c) 2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Bitcoin Object Python Serializations
@ -17,7 +21,7 @@ CTransaction,CTxIn, CTxOut, etc....:
ser_*, deser_*: functions that handle serialization/deserialization
"""
from io import BytesIO, BufferedReader
from io import BytesIO
from codecs import encode
import struct
import binascii
@ -185,25 +189,20 @@ def bytes_to_hex_str(s):
return binascii.hexlify(s)
# Deserialize from a hex string representation (eg from RPC)
def FromHex(obj, hex_string):
obj.deserialize(BytesIO(hex_str_to_bytes(hex_string)))
# like from_hex, but without the hex part
def from_binary(cls, stream):
"""deserialize a binary stream (or bytes object) into an object"""
# handle bytes object by turning it into a stream
was_bytes = isinstance(stream, bytes)
if was_bytes:
stream = BytesIO(stream)
obj = cls()
obj.deserialize(stream)
if was_bytes:
assert len(stream.read()) == 0
return obj
# Convert a binary-serializable object to hex (eg for submission via RPC)
def ToHex(obj):
return bytes_to_hex_str(obj.serialize())
def Base64ToHex(s):
return binascii.hexlify(base64.b64decode(s))
def HexToBase64(s):
return base64.b64encode(binascii.unhexlify(s))
def ser_sig_der(r, s):
sig = b"\x30"
@ -576,433 +575,147 @@ class CTransaction(object):
)
def DeserializeHDKeypath(f, key, hd_keypaths):
if len(key) != 34 and len(key) != 66:
raise IOError(
"Size of key was not the expected size for the type partial signature pubkey"
)
pubkey = key[1:]
if pubkey in hd_keypaths:
raise IOError(
"Duplicate key, input partial signature for pubkey already provided"
)
# global types
PSBT_GLOBAL_UNSIGNED_TX = 0x00
PSBT_GLOBAL_XPUB = 0x01
PSBT_GLOBAL_TX_VERSION = 0x02
PSBT_GLOBAL_FALLBACK_LOCKTIME = 0x03
PSBT_GLOBAL_INPUT_COUNT = 0x04
PSBT_GLOBAL_OUTPUT_COUNT = 0x05
PSBT_GLOBAL_TX_MODIFIABLE = 0x06
PSBT_GLOBAL_VERSION = 0xFB
PSBT_GLOBAL_PROPRIETARY = 0xFC
value = deser_string(f)
hd_keypaths[pubkey] = struct.unpack("<" + "I" * (len(value) // 4), value)
# per-input types
PSBT_IN_NON_WITNESS_UTXO = 0x00
PSBT_IN_WITNESS_UTXO = 0x01
PSBT_IN_PARTIAL_SIG = 0x02
PSBT_IN_SIGHASH_TYPE = 0x03
PSBT_IN_REDEEM_SCRIPT = 0x04
PSBT_IN_WITNESS_SCRIPT = 0x05
PSBT_IN_BIP32_DERIVATION = 0x06
PSBT_IN_FINAL_SCRIPTSIG = 0x07
PSBT_IN_FINAL_SCRIPTWITNESS = 0x08
PSBT_IN_POR_COMMITMENT = 0x09
PSBT_IN_RIPEMD160 = 0x0A
PSBT_IN_SHA256 = 0x0B
PSBT_IN_HASH160 = 0x0C
PSBT_IN_HASH256 = 0x0D
PSBT_IN_PREVIOUS_TXID = 0x0E
PSBT_IN_OUTPUT_INDEX = 0x0F
PSBT_IN_SEQUENCE = 0x10
PSBT_IN_REQUIRED_TIME_LOCKTIME = 0x11
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME = 0x12
PSBT_IN_TAP_KEY_SIG = 0x13
PSBT_IN_TAP_SCRIPT_SIG = 0x14
PSBT_IN_TAP_LEAF_SCRIPT = 0x15
PSBT_IN_TAP_BIP32_DERIVATION = 0x16
PSBT_IN_TAP_INTERNAL_KEY = 0x17
PSBT_IN_TAP_MERKLE_ROOT = 0x18
PSBT_IN_PROPRIETARY = 0xFC
# per-output types
PSBT_OUT_REDEEM_SCRIPT = 0x00
PSBT_OUT_WITNESS_SCRIPT = 0x01
PSBT_OUT_BIP32_DERIVATION = 0x02
PSBT_OUT_AMOUNT = 0x03
PSBT_OUT_SCRIPT = 0x04
PSBT_OUT_TAP_INTERNAL_KEY = 0x05
PSBT_OUT_TAP_TREE = 0x06
PSBT_OUT_TAP_BIP32_DERIVATION = 0x07
PSBT_OUT_PROPRIETARY = 0xFC
def SerializeHDKeypath(hd_keypaths, type):
r = b""
for pubkey, path in hd_keypaths.items():
r += ser_string(type + pubkey)
packed = struct.pack("<" + "I" * len(path), *path)
r += ser_string(packed)
return r
class PSBTMap:
"""Class for serializing and deserializing PSBT maps"""
def __init__(self, map=None):
self.map = map if map is not None else {}
# NOTE: this implementation assumes that the keytype from bip174 is always 1 byte,
# as it detects mappings (like bip32 derivations, partial sigs, ..) based on this.
def deserialize(self, f):
m = {}
while True:
k = deser_string(f)
if len(k) == 0:
break
v = deser_string(f)
if len(k) == 1:
k = k[0]
assert k not in m
m[k] = v
else:
typ, k = k[0], k[1:]
if typ not in m:
m[typ] = {k: v}
else:
m[typ][k] = v
self.map = m
def serialize(self):
m = b""
for key_type in sorted(self.map):
psbt_val = self.map[key_type]
if isinstance(key_type, int) and 0 <= key_type and key_type <= 255:
key_type = bytes([key_type])
if isinstance(psbt_val, dict):
for key_data, val_data in psbt_val.items():
k = key_type + key_data
m += ser_compact_size(len(k)) + k
m += ser_compact_size(len(val_data)) + val_data
else:
m += ser_compact_size(len(key_type)) + key_type
m += ser_compact_size(len(psbt_val)) + psbt_val
m += b"\x00"
return m
class PartiallySignedInput:
def __init__(self):
self.non_witness_utxo = None
self.witness_utxo = None
self.partial_sigs = {}
self.sighash = 0
self.redeem_script = b""
self.witness_script = b""
self.hd_keypaths = {}
self.final_script_sig = b""
self.final_script_witness = CTxInWitness()
self.unknown = {}
class PSBT:
"""Class for serializing and deserializing PSBTs"""
def set_null(self):
self.non_witness_utxo = None
self.witness_utxo = None
self.partial_sigs.clear()
self.sighash = 0
self.redeem_script = b""
self.witness_script = b""
self.hd_keypaths.clear()
self.final_script_sig = b""
self.final_script_witness = CTxInWitness()
self.unknown.clear()
def __init__(self, *, g=None, i=None, o=None):
self.g = g if g is not None else PSBTMap()
self.i = i if i is not None else []
self.o = o if o is not None else []
self.tx = None
def deserialize(self, f):
while True:
# read the key
try:
key = deser_string(f)
except Exception:
break
# Check for separator
if len(key) == 0:
break
# First byte of key is the type
key_type = struct.unpack("b", bytearray([key[0]]))[0]
if key_type == 0:
if self.non_witness_utxo:
raise IOError(
"Duplicate Key, input non witness utxo already provided"
)
elif len(key) != 1:
raise IOError("non witness utxo key is more than one byte type")
self.non_witness_utxo = CTransaction()
value = BufferedReader(BytesIO(deser_string(f)))
self.non_witness_utxo.deserialize(value)
self.non_witness_utxo.rehash()
elif key_type == 1:
if self.witness_utxo:
raise IOError("Duplicate Key, input witness utxo already provided")
elif len(key) != 1:
raise IOError("witness utxo key is more than one byte type")
self.witness_utxo = CTxOut()
value = BufferedReader(BytesIO(deser_string(f)))
self.witness_utxo.deserialize(value)
elif key_type == 2:
if len(key) != 34 and len(key) != 66:
raise IOError(
"Size of key was not the expected size for the type partial signature pubkey"
)
pubkey = key[1:]
if pubkey in self.partial_sigs:
raise IOError(
"Duplicate key, input partial signature for pubkey already provided"
)
sig = deser_string(f)
self.partial_sigs[pubkey] = sig
elif key_type == 3:
if self.sighash > 0:
raise IOError("Duplicate key, input sighash type already provided")
elif len(key) != 1:
raise IOError("sighash key is more than one byte type")
value = deser_string(f)
self.sighash = struct.unpack("<I", value)[0]
elif key_type == 4:
if len(self.redeem_script) != 0:
raise IOError("Duplicate key, input redeemScript already provided")
elif len(key) != 1:
raise IOError("redeemScript key is more than one byte type")
self.redeem_script = deser_string(f)
elif key_type == 5:
if len(self.witness_script) != 0:
raise IOError("Duplicate key, input witnessScript already provided")
elif len(key) != 1:
raise IOError("witnessScript key is more than one byte type")
self.witness_script = deser_string(f)
elif key_type == 6:
DeserializeHDKeypath(f, key, self.hd_keypaths)
elif key_type == 7:
if len(self.final_script_sig) != 0:
raise IOError(
"Duplicate key, input final scriptSig already provided"
)
elif len(key) != 1:
raise IOError("final scriptSig key is more than one byte type")
self.final_script_sig = deser_string(f)
elif key_type == 8:
if not self.final_script_witness.is_null():
raise IOError(
"Duplicate key, input final scriptWitness already provided"
)
elif len(key) != 1:
raise IOError("final scriptWitness key is more than one byte type")
value = BufferedReader(BytesIO(deser_string(f)))
self.final_script_witness.deserialize(value)
else:
if key in self.unknown:
raise IOError(
"Duplicate key, key for unknown value already provided"
)
value = deser_string(f)
self.unknown[key] = value
assert f.read(5) == b"psbt\xff"
self.g = from_binary(PSBTMap, f)
assert 0 in self.g.map
self.tx = from_binary(CTransaction, self.g.map[0])
self.i = [from_binary(PSBTMap, f) for _ in self.tx.vin]
self.o = [from_binary(PSBTMap, f) for _ in self.tx.vout]
return self
def serialize(self):
r = b""
assert isinstance(self.g, PSBTMap)
assert isinstance(self.i, list) and all(isinstance(x, PSBTMap) for x in self.i)
assert isinstance(self.o, list) and all(isinstance(x, PSBTMap) for x in self.o)
assert 0 in self.g.map
tx = from_binary(CTransaction, self.g.map[0])
assert len(tx.vin) == len(self.i)
assert len(tx.vout) == len(self.o)
if self.non_witness_utxo:
r += ser_string(b"\x00")
tx = self.non_witness_utxo.serialize_with_witness()
r += ser_string(tx)
psbt = [x.serialize() for x in [self.g] + self.i + self.o]
return b"psbt\xff" + b"".join(psbt)
elif self.witness_utxo:
r += ser_string(b"\x01")
tx = self.witness_utxo.serialize()
r += ser_string(tx)
def make_blank(self):
"""
Remove all fields except for PSBT_GLOBAL_UNSIGNED_TX
"""
for m in self.i + self.o:
m.map.clear()
if len(self.final_script_sig) == 0 and self.final_script_witness.is_null():
for pubkey, sig in self.partial_sigs.items():
r += ser_string(b"\x02" + pubkey)
r += ser_string(sig)
self.g = PSBTMap(map={0: self.g.map[0]})
if self.sighash > 0:
r += ser_string(b"\x03")
r += ser_string(struct.pack("<I", self.sighash))
def to_base64(self):
return base64.b64encode(self.serialize()).decode("utf8")
if len(self.redeem_script) != 0:
r += ser_string(b"\x04")
r += ser_string(self.redeem_script)
if len(self.witness_script) != 0:
r += ser_string(b"\x05")
r += ser_string(self.witness_script)
r += SerializeHDKeypath(self.hd_keypaths, b"\x06")
if len(self.final_script_sig) != 0:
r += ser_string(b"\x07")
r += ser_string(self.final_script_sig)
if not self.final_script_witness.is_null():
r += ser_string(b"\x08")
r += self.final_script_witness.serialize()
for key, value in self.unknown:
r += ser_string(key)
r += ser_string(value)
r += b"\x00"
return r
def is_sane(self):
# Cannot have both witness and non-witness utxos
if self.witness_utxo and self.non_witness_utxo:
return False
# if we have witness script or scriptwitness, must have witness utxo
if len(self.witness_script) != 0 and not self.witness_utxo:
return False
if not self.final_script_witness.is_null() and not self.witness_utxo:
return False
return True
class PartiallySignedOutput:
def __init__(self):
self.redeem_script = b""
self.witness_script = b""
self.hd_keypaths = {}
self.unknown = {}
def set_null(self):
self.redeem_script = b""
self.witness_script = b""
self.hd_keypaths.clear()
self.unknown.clear()
def deserialize(self, f):
while True:
# read the key
try:
key = deser_string(f)
except Exception:
break
# Check for separator
if len(key) == 0:
break
# First byte of key is the type
key_type = struct.unpack("b", bytearray([key[0]]))[0]
if key_type == 0:
if len(self.redeem_script) != 0:
raise IOError("Duplicate key, output redeemScript already provided")
elif len(key) != 1:
raise IOError("Output redeemScript key is more than one byte type")
self.redeem_script = deser_string(f)
elif key_type == 1:
if len(self.witness_script) != 0:
raise IOError(
"Duplicate key, output witnessScript already provided"
)
elif len(key) != 1:
raise IOError("Output witnessScript key is more than one byte type")
self.witness_script = deser_string(f)
elif key_type == 2:
DeserializeHDKeypath(f, key, self.hd_keypaths)
else:
if key in self.unknown:
raise IOError(
"Duplicate key, key for unknown value already provided"
)
value = deser_string(f)
self.unknown[key] = value
def serialize(self):
r = b""
if len(self.redeem_script) != 0:
r += ser_string(b"\x00")
r += ser_string(self.redeem_script)
if len(self.witness_script) != 0:
r += ser_string(b"\x01")
r += ser_string(self.witness_script)
r += SerializeHDKeypath(self.hd_keypaths, b"\x02")
for key, value in self.unknown:
r += ser_string(key)
r += ser_string(value)
r += b"\x00"
return r
class PSBT(object):
def __init__(self, tx=None):
if tx:
self.tx = tx
else:
self.tx = CTransaction()
self.inputs = []
self.outputs = []
self.unknown = []
def deserialize(self, psbt):
hexstring = Base64ToHex(psbt.strip())
f = BufferedReader(BytesIO(binascii.unhexlify(hexstring)))
# Read the magic bytes
magic = f.read(5)
if magic != b"psbt\xff":
raise IOError("invalid magic")
# Read loop
separators = 0
psbt_input = PartiallySignedInput()
in_globals = True
while True:
# read the key
try:
key = deser_string(f)
except Exception:
break
# Check for separator
if len(key) == 0:
break
# First byte of key is the type
key_type = struct.unpack("b", bytearray([key[0]]))[0]
# Do stuff based on type
if key_type == 0x00:
# Checks for correctness
if not self.tx.is_null:
raise IOError("Duplicate key, unsigned tx already provided")
elif len(key) > 1:
raise IOError("Global unsigned tx key is more than one byte type")
# read in value
value = BufferedReader(BytesIO(deser_string(f)))
self.tx.deserialize(value)
# Make sure that all scriptSigs and scriptWitnesses are empty
for txin in self.tx.vin:
if len(txin.scriptSig) != 0 or not self.tx.wit.is_null():
raise IOError(
"Unsigned tx does not have empty scriptSigs and scriptWitnesses"
)
else:
if key in self.unknown:
raise IOError(
"Duplicate key, key for unknown value already provided"
)
value = deser_string(f)
self.unknown[key] = value
# make sure that we got an unsigned tx
if self.tx.is_null():
raise IOError("No unsigned trasaction was provided")
# Read input data
for txin in self.tx.vin:
input = PartiallySignedInput()
input.deserialize(f)
self.inputs.append(input)
if (
input.non_witness_utxo
and input.non_witness_utxo.rehash()
and input.non_witness_utxo.sha256 != txin.prevout.sha256
):
raise IOError("Non-witness UTXO does not match outpoint hash")
if len(self.inputs) != len(self.tx.vin):
raise IOError(
"Inputs provided does not match the number of inputs in transaction"
)
# Read output data
for txout in self.tx.vout:
output = PartiallySignedOutput()
output.deserialize(f)
self.outputs.append(output)
if len(self.outputs) != len(self.tx.vout):
raise IOError(
"Outputs provided does not match the number of outputs in transaction"
)
if not self.is_sane():
raise IOError("PSBT is not sane")
def serialize(self):
r = b""
# magic bytes
r += b"psbt\xff"
# unsigned tx flag
r += b"\x01\x00"
# write serialized tx
tx = self.tx.serialize_with_witness()
r += ser_compact_size(len(tx))
r += tx
# separator
r += b"\x00"
# unknowns
for key, value in self.unknown:
r += ser_string(key)
r += ser_string(value)
# inputs
for input in self.inputs:
r += input.serialize()
# outputs
for output in self.outputs:
r += output.serialize()
# return hex string
return HexToBase64(binascii.hexlify(r)).decode()
def is_sane(self):
for input in self.inputs:
if not input.is_sane():
return False
return True
@classmethod
def from_base64(cls, b64psbt):
return from_binary(cls, base64.b64decode(b64psbt))
# Sighash serializations
@ -1034,13 +747,14 @@ def sighash_all_witness(script_code, psbt, i, acp=False):
sighash_type = b"\x01\x00\x00\x00" if not acp else b"\x81\x00\x00\x00"
# Make sighash preimage
prev_txo = from_binary(CTxOut, psbt.i[i].map[PSBT_IN_WITNESS_UTXO])
preimage = b""
preimage += struct.pack("<i", psbt.tx.nVersion)
preimage += hashPrevouts
preimage += hashSequence
preimage += psbt.tx.vin[i].prevout.serialize()
preimage += ser_string(script_code)
preimage += struct.pack("<q", psbt.inputs[i].witness_utxo.nValue)
preimage += struct.pack("<q", prev_txo.nValue)
preimage += struct.pack("<I", psbt.tx.vin[i].nSequence)
preimage += hashOutputs
preimage += struct.pack("<I", psbt.tx.nLockTime)

View File

@ -67,9 +67,7 @@ def spend_coins(minisafed, bitcoind, coins):
}
res = minisafed.rpc.createspend([c["outpoint"] for c in coins], destinations, 1)
psbt = PSBT()
psbt.deserialize(res["psbt"])
signed_psbt = minisafed.sign_psbt(psbt)
signed_psbt = minisafed.sign_psbt(PSBT.from_base64(res["psbt"]))
finalized_psbt = minisafed.finalize_psbt(signed_psbt)
tx = finalized_psbt.tx.serialize_with_witness().hex()
bitcoind.rpc.sendrawtransaction(tx)

View File

@ -1,7 +1,7 @@
import pytest
from fixtures import *
from test_framework.serializations import PSBT
from test_framework.serializations import PSBT, PSBT_IN_PARTIAL_SIG
from test_framework.utils import wait_for, COIN, RpcError, get_txid, spend_coins
@ -104,13 +104,12 @@ def test_create_spend(minisafed, bitcoind):
assert "psbt" in res
# The transaction must contain a change output.
spend_psbt = PSBT()
spend_psbt.deserialize(res["psbt"])
assert len(spend_psbt.outputs) == 4
spend_psbt = PSBT.from_base64(res["psbt"])
assert len(spend_psbt.o) == 4
assert len(spend_psbt.tx.vout) == 4
# We can sign it and broadcast it.
signed_psbt = minisafed.sign_psbt(spend_psbt)
signed_psbt = minisafed.sign_psbt(PSBT.from_base64(res["psbt"]))
finalized_psbt = minisafed.finalize_psbt(signed_psbt)
tx = finalized_psbt.tx.serialize_with_witness().hex()
bitcoind.rpc.sendrawtransaction(tx)
@ -156,16 +155,14 @@ def test_list_spend(minisafed, bitcoind):
assert second_psbt["change_index"] is None
# If we delete the first one, we'll get only the second one.
first_psbt = PSBT()
first_psbt.deserialize(res["psbt"])
first_psbt = PSBT.from_base64(res["psbt"])
minisafed.rpc.delspendtx(first_psbt.tx.txid().hex())
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 1
assert list_res[0]["psbt"] == res_b["psbt"]
# If we delete the second one, result will be empty.
second_psbt = PSBT()
second_psbt.deserialize(res_b["psbt"])
second_psbt = PSBT.from_base64(res_b["psbt"])
minisafed.rpc.delspendtx(second_psbt.tx.txid().hex())
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 0
@ -191,20 +188,18 @@ def test_update_spend(minisafed, bitcoind):
assert list_res[0]["psbt"] == res["psbt"]
# Keep a copy for later.
psbt_no_sig = PSBT()
psbt_no_sig.deserialize(res["psbt"])
psbt_no_sig = PSBT.from_base64(res["psbt"])
# We can add a signature and update it
psbt_sig_a = PSBT()
psbt_sig_a.deserialize(res["psbt"])
psbt_sig_a = PSBT.from_base64(res["psbt"])
dummy_pk_a = bytes.fromhex(
"0375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c"
)
dummy_sig_a = bytes.fromhex(
"304402202b925395cfeaa0171a7a92982bb4891acc4a312cbe7691d8375d36796d5b570a0220378a8ab42832848e15d1aedded5fb360fedbdd6c39226144e527f0f1e19d5398"
)
psbt_sig_a.inputs[0].partial_sigs[dummy_pk_a] = dummy_sig_a
psbt_sig_a_ser = psbt_sig_a.serialize()
psbt_sig_a.i[0].map[PSBT_IN_PARTIAL_SIG] = {dummy_pk_a: dummy_sig_a}
psbt_sig_a_ser = psbt_sig_a.to_base64()
minisafed.rpc.updatespend(psbt_sig_a_ser)
# We'll get it when querying
@ -213,26 +208,24 @@ def test_update_spend(minisafed, bitcoind):
assert list_res[0]["psbt"] == psbt_sig_a_ser
# We can add another signature to the empty PSBT and update it again
psbt_sig_b = PSBT()
psbt_sig_b.deserialize(res["psbt"])
psbt_sig_b = PSBT.from_base64(res["psbt"])
dummy_pk_b = bytes.fromhex(
"03a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff"
)
dummy_sig_b = bytes.fromhex(
"3044022005aebcd649fb8965f0591710fb3704931c3e8118ee60dd44917479f63ceba6d4022018b212900e5a80e9452366894de37f0d02fb9c89f1e94f34fb6ed7fd71c15c41"
)
psbt_sig_b.inputs[0].partial_sigs[dummy_pk_b] = dummy_sig_b
psbt_sig_b_ser = psbt_sig_b.serialize()
psbt_sig_b.i[0].map[PSBT_IN_PARTIAL_SIG] = {dummy_pk_b: dummy_sig_b}
psbt_sig_b_ser = psbt_sig_b.to_base64()
minisafed.rpc.updatespend(psbt_sig_b_ser)
# It will have merged both.
list_res = minisafed.rpc.listspendtxs()["spend_txs"]
assert len(list_res) == 1
psbt_merged = PSBT()
psbt_merged.deserialize(list_res[0]["psbt"])
assert len(psbt_merged.inputs[0].partial_sigs) == 2
assert psbt_merged.inputs[0].partial_sigs[dummy_pk_a] == dummy_sig_a
assert psbt_merged.inputs[0].partial_sigs[dummy_pk_b] == dummy_sig_b
psbt_merged = PSBT.from_base64(list_res[0]["psbt"])
assert len(psbt_merged.i[0].map[PSBT_IN_PARTIAL_SIG]) == 2
assert psbt_merged.i[0].map[PSBT_IN_PARTIAL_SIG][dummy_pk_a] == dummy_sig_a
assert psbt_merged.i[0].map[PSBT_IN_PARTIAL_SIG][dummy_pk_b] == dummy_sig_b
def test_broadcast_spend(minisafed, bitcoind):
@ -245,8 +238,7 @@ def test_broadcast_spend(minisafed, bitcoind):
bitcoind.rpc.getnewaddress(): 200_000,
}
res = minisafed.rpc.createspend(outpoints, destinations, 6)
psbt = PSBT()
psbt.deserialize(res["psbt"])
psbt = PSBT.from_base64(res["psbt"])
txid = psbt.tx.txid().hex()
# We can't broadcast an unknown Spend
@ -257,8 +249,8 @@ def test_broadcast_spend(minisafed, bitcoind):
# We can't broadcast an unsigned transaction
with pytest.raises(RpcError, match="Failed to finalize the spend transaction.*"):
minisafed.rpc.broadcastspend(txid)
signed_psbt = minisafed.sign_psbt(psbt)
minisafed.rpc.updatespend(signed_psbt.serialize())
signed_psbt = minisafed.sign_psbt(PSBT.from_base64(res["psbt"]))
minisafed.rpc.updatespend(signed_psbt.to_base64())
# Now we've signed and stored it, the daemon will take care of finalizing
# the PSBT before broadcasting the transaction.