Browse Source
-----
Squashed commit of the following:
commit 69c0d48108314db6f0e100bea2ce5a9a3a0e9a1f
Author: Peter D. Gray <peter@conalgo.com>
Date: Fri Aug 2 14:51:33 2019 -0400
deterministic-build/requirements-hw.txt: update to version 0.7.9 of ckcc-protocol for Coldcard
commit 5cd2c528698dfb4ad248844be3c741c25aa33e38
Merge: 5e2a36a3e 537b35586
Author: Peter D. Gray <peter@conalgo.com>
Date: Fri Aug 2 14:41:59 2019 -0400
Merge branch 'multisig' of github.com:Coldcard/electrum into multisig
commit 5e2a36a3ee28780a11f789f69896e6e795621bfc
Author: Peter D. Gray <peter@conalgo.com>
Date: Fri Aug 2 14:41:49 2019 -0400
Some fixes for p2wsh-p2sh and p2wsh cases
commit 537b35586e0b1e11622a8e7d718b6fd37d47f952
Merge: a9e3ca47e 2a80f6a3a
Author: nvk <rodolfo@rnvk.org>
Date: Tue Jul 23 11:40:39 2019 -0400
Merge branch 'master' into multisig
commit a9e3ca47e189bcf0556703a4f2ca0c084638eb73
Author: Peter D. Gray <peter@conalgo.com>
Date: Mon Jun 24 13:36:41 2019 -0400
Bugfix: not all keystores have labels
commit 57783ec158af5ca8d63d596638bc3b6ee63b053f
Author: Peter D. Gray <peter@conalgo.com>
Date: Mon Jun 24 13:36:04 2019 -0400
Add address format to export data, and bugfix: use xfp_for_keystore()
commit 6f1f7673eaa340d14497b11c2453f03a73b38850
Author: Peter D. Gray <peter@conalgo.com>
Date: Fri Jun 21 09:06:49 2019 -0400
Revert "bugfix: P2SH inputs can be signed with extra signatures, more than required"
This reverts commit 75b6b663eca9e7b5edc9a463f7acd8f1c0f0a61a.
commit c322fb6dd2783e1103f5bf69ce60a365fbaf4bfe
Author: Peter D. Gray <peter@conalgo.com>
Date: Thu Jun 20 12:57:19 2019 -0400
Require latest CKCC protocol
commit 69a5b781ebc182851d2e25319b549ec58ea23eb1
Author: Peter D. Gray <peter@conalgo.com>
Date: Thu Jun 20 12:40:27 2019 -0400
gui/qt/main_window.py: add co-signer keystore label to wallet info display, and a hook for different buttons
commit 55d506d264dbb341602630c3429134e493995272
Author: Peter D. Gray <peter@conalgo.com>
Date: Thu Jun 20 12:36:10 2019 -0400
PSBT Combining/cleanup
commit 75b6b663eca9e7b5edc9a463f7acd8f1c0f0a61a
Author: Peter D. Gray <peter@conalgo.com>
Date: Thu Jun 20 10:18:02 2019 -0400
bugfix: P2SH inputs can be signed with extra signatures, more than required
commit 1bde362ddbbfd86520a7cb7bc51e0bcef06be078
Author: Peter D. Gray <peter@conalgo.com>
Date: Wed Jun 19 09:47:26 2019 -0400
Combines signed PSBT files
commit cc5c0532e52fbe282e862e20c250cc88ed435cad
Author: Peter D. Gray <peter@conalgo.com>
Date: Fri Jun 14 13:04:32 2019 -0400
Working towards multisig
commit cb20da5428ba97237006683133e10b0758999966
Author: Peter D. Gray <peter@conalgo.com>
Date: Fri Jun 14 13:04:18 2019 -0400
Refactor/import PSBT handling code into own files
commit 558ef82bb0a8c16fb4e8bd0a6a80190498f1ce57
Author: Peter D. Gray <peter@conalgo.com>
Date: Tue May 28 13:26:10 2019 -0400
plugins/hw_wallet/qt.py: show keystore label in tooltip
commit 269299df4a9eb5960b6c6ec0afcbf3ef69ad0be3
Author: Peter D. Gray <peter@conalgo.com>
Date: Mon May 27 09:32:43 2019 -0400
Swap endian of xpub fingprint values, so they are shown as BE32 in capitalized hex, rather than 0x%08x (LE32)
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
committed by
SomberNight
7 changed files with 1068 additions and 232 deletions
@ -0,0 +1,312 @@ |
|||
# |
|||
# basic_psbt.py - yet another PSBT parser/serializer but used only for test cases. |
|||
# |
|||
# - history: taken from coldcard-firmware/testing/psbt.py |
|||
# - trying to minimize electrum code in here, and generally, dependancies. |
|||
# |
|||
import io, struct |
|||
from base64 import b64decode |
|||
from binascii import a2b_hex, b2a_hex |
|||
from struct import pack, unpack |
|||
|
|||
from electrum.transaction import Transaction |
|||
|
|||
# BIP-174 (aka PSBT) defined values |
|||
# |
|||
PSBT_GLOBAL_UNSIGNED_TX = (0) |
|||
PSBT_GLOBAL_XPUB = (1) |
|||
|
|||
PSBT_IN_NON_WITNESS_UTXO = (0) |
|||
PSBT_IN_WITNESS_UTXO = (1) |
|||
PSBT_IN_PARTIAL_SIG = (2) |
|||
PSBT_IN_SIGHASH_TYPE = (3) |
|||
PSBT_IN_REDEEM_SCRIPT = (4) |
|||
PSBT_IN_WITNESS_SCRIPT = (5) |
|||
PSBT_IN_BIP32_DERIVATION = (6) |
|||
PSBT_IN_FINAL_SCRIPTSIG = (7) |
|||
PSBT_IN_FINAL_SCRIPTWITNESS = (8) |
|||
|
|||
PSBT_OUT_REDEEM_SCRIPT = (0) |
|||
PSBT_OUT_WITNESS_SCRIPT = (1) |
|||
PSBT_OUT_BIP32_DERIVATION = (2) |
|||
|
|||
# Serialization/deserialization tools |
|||
def ser_compact_size(l): |
|||
r = b"" |
|||
if l < 253: |
|||
r = struct.pack("B", l) |
|||
elif l < 0x10000: |
|||
r = struct.pack("<BH", 253, l) |
|||
elif l < 0x100000000: |
|||
r = struct.pack("<BI", 254, l) |
|||
else: |
|||
r = struct.pack("<BQ", 255, l) |
|||
return r |
|||
|
|||
def deser_compact_size(f): |
|||
try: |
|||
nit = f.read(1)[0] |
|||
except IndexError: |
|||
return None # end of file |
|||
|
|||
if nit == 253: |
|||
nit = struct.unpack("<H", f.read(2))[0] |
|||
elif nit == 254: |
|||
nit = struct.unpack("<I", f.read(4))[0] |
|||
elif nit == 255: |
|||
nit = struct.unpack("<Q", f.read(8))[0] |
|||
return nit |
|||
|
|||
def my_var_int(l): |
|||
# Bitcoin serialization of integers... directly into binary! |
|||
if l < 253: |
|||
return pack("B", l) |
|||
elif l < 0x10000: |
|||
return pack("<BH", 253, l) |
|||
elif l < 0x100000000: |
|||
return pack("<BI", 254, l) |
|||
else: |
|||
return pack("<BQ", 255, l) |
|||
|
|||
|
|||
class PSBTSection: |
|||
|
|||
def __init__(self, fd=None, idx=None): |
|||
self.defaults() |
|||
self.my_index = idx |
|||
|
|||
if not fd: return |
|||
|
|||
while 1: |
|||
ks = deser_compact_size(fd) |
|||
if ks is None: break |
|||
if ks == 0: break |
|||
|
|||
key = fd.read(ks) |
|||
vs = deser_compact_size(fd) |
|||
val = fd.read(vs) |
|||
|
|||
kt = key[0] |
|||
self.parse_kv(kt, key[1:], val) |
|||
|
|||
def serialize(self, fd, my_idx): |
|||
|
|||
def wr(ktype, val, key=b''): |
|||
fd.write(ser_compact_size(1 + len(key))) |
|||
fd.write(bytes([ktype]) + key) |
|||
fd.write(ser_compact_size(len(val))) |
|||
fd.write(val) |
|||
|
|||
self.serialize_kvs(wr) |
|||
|
|||
fd.write(b'\0') |
|||
|
|||
class BasicPSBTInput(PSBTSection): |
|||
def defaults(self): |
|||
self.utxo = None |
|||
self.witness_utxo = None |
|||
self.part_sigs = {} |
|||
self.sighash = None |
|||
self.bip32_paths = {} |
|||
self.redeem_script = None |
|||
self.witness_script = None |
|||
self.others = {} |
|||
|
|||
def __eq__(a, b): |
|||
if a.sighash != b.sighash: |
|||
if a.sighash is not None and b.sighash is not None: |
|||
return False |
|||
|
|||
rv = a.utxo == b.utxo and \ |
|||
a.witness_utxo == b.witness_utxo and \ |
|||
a.redeem_script == b.redeem_script and \ |
|||
a.witness_script == b.witness_script and \ |
|||
a.my_index == b.my_index and \ |
|||
a.bip32_paths == b.bip32_paths and \ |
|||
sorted(a.part_sigs.keys()) == sorted(b.part_sigs.keys()) |
|||
|
|||
# NOTE: equality test on signatures requires parsing DER stupidness |
|||
# and some maybe understanding of R/S values on curve that I don't have. |
|||
|
|||
return rv |
|||
|
|||
def parse_kv(self, kt, key, val): |
|||
if kt == PSBT_IN_NON_WITNESS_UTXO: |
|||
self.utxo = val |
|||
assert not key |
|||
elif kt == PSBT_IN_WITNESS_UTXO: |
|||
self.witness_utxo = val |
|||
assert not key |
|||
elif kt == PSBT_IN_PARTIAL_SIG: |
|||
self.part_sigs[key] = val |
|||
elif kt == PSBT_IN_SIGHASH_TYPE: |
|||
assert len(val) == 4 |
|||
self.sighash = struct.unpack("<I", val)[0] |
|||
assert not key |
|||
elif kt == PSBT_IN_BIP32_DERIVATION: |
|||
self.bip32_paths[key] = val |
|||
elif kt == PSBT_IN_REDEEM_SCRIPT: |
|||
self.redeem_script = val |
|||
assert not key |
|||
elif kt == PSBT_IN_WITNESS_SCRIPT: |
|||
self.witness_script = val |
|||
assert not key |
|||
elif kt in ( PSBT_IN_REDEEM_SCRIPT, |
|||
PSBT_IN_WITNESS_SCRIPT, |
|||
PSBT_IN_FINAL_SCRIPTSIG, |
|||
PSBT_IN_FINAL_SCRIPTWITNESS): |
|||
assert not key |
|||
self.others[kt] = val |
|||
else: |
|||
raise KeyError(kt) |
|||
|
|||
def serialize_kvs(self, wr): |
|||
if self.utxo: |
|||
wr(PSBT_IN_NON_WITNESS_UTXO, self.utxo) |
|||
if self.witness_utxo: |
|||
wr(PSBT_IN_WITNESS_UTXO, self.witness_utxo) |
|||
if self.redeem_script: |
|||
wr(PSBT_IN_REDEEM_SCRIPT, self.redeem_script) |
|||
if self.witness_script: |
|||
wr(PSBT_IN_WITNESS_SCRIPT, self.witness_script) |
|||
for pk, val in sorted(self.part_sigs.items()): |
|||
wr(PSBT_IN_PARTIAL_SIG, val, pk) |
|||
if self.sighash is not None: |
|||
wr(PSBT_IN_SIGHASH_TYPE, struct.pack('<I', self.sighash)) |
|||
for k in self.bip32_paths: |
|||
wr(PSBT_IN_BIP32_DERIVATION, self.bip32_paths[k], k) |
|||
for k in self.others: |
|||
wr(k, self.others[k]) |
|||
|
|||
class BasicPSBTOutput(PSBTSection): |
|||
def defaults(self): |
|||
self.redeem_script = None |
|||
self.witness_script = None |
|||
self.bip32_paths = {} |
|||
|
|||
def __eq__(a, b): |
|||
return a.redeem_script == b.redeem_script and \ |
|||
a.witness_script == b.witness_script and \ |
|||
a.my_index == b.my_index and \ |
|||
a.bip32_paths == b.bip32_paths |
|||
|
|||
def parse_kv(self, kt, key, val): |
|||
if kt == PSBT_OUT_REDEEM_SCRIPT: |
|||
self.redeem_script = val |
|||
assert not key |
|||
elif kt == PSBT_OUT_WITNESS_SCRIPT: |
|||
self.witness_script = val |
|||
assert not key |
|||
elif kt == PSBT_OUT_BIP32_DERIVATION: |
|||
self.bip32_paths[key] = val |
|||
else: |
|||
raise ValueError(kt) |
|||
|
|||
def serialize_kvs(self, wr): |
|||
if self.redeem_script: |
|||
wr(PSBT_OUT_REDEEM_SCRIPT, self.redeem_script) |
|||
if self.witness_script: |
|||
wr(PSBT_OUT_WITNESS_SCRIPT, self.witness_script) |
|||
for k in self.bip32_paths: |
|||
wr(PSBT_OUT_BIP32_DERIVATION, self.bip32_paths[k], k) |
|||
|
|||
|
|||
class BasicPSBT: |
|||
"Just? parse and store" |
|||
|
|||
def __init__(self): |
|||
|
|||
self.txn = None |
|||
self.filename = None |
|||
self.parsed_txn = None |
|||
self.xpubs = [] |
|||
|
|||
self.inputs = [] |
|||
self.outputs = [] |
|||
|
|||
def __eq__(a, b): |
|||
return a.txn == b.txn and \ |
|||
len(a.inputs) == len(b.inputs) and \ |
|||
len(a.outputs) == len(b.outputs) and \ |
|||
all(a.inputs[i] == b.inputs[i] for i in range(len(a.inputs))) and \ |
|||
all(a.outputs[i] == b.outputs[i] for i in range(len(a.outputs))) and \ |
|||
sorted(a.xpubs) == sorted(b.xpubs) |
|||
|
|||
def parse(self, raw, filename=None): |
|||
# auto-detect and decode Base64 and Hex. |
|||
if raw[0:10].lower() == b'70736274ff': |
|||
raw = a2b_hex(raw.strip()) |
|||
if raw[0:6] == b'cHNidP': |
|||
raw = b64decode(raw) |
|||
assert raw[0:5] == b'psbt\xff', "bad magic" |
|||
|
|||
self.filename = filename |
|||
|
|||
with io.BytesIO(raw[5:]) as fd: |
|||
|
|||
# globals |
|||
while 1: |
|||
ks = deser_compact_size(fd) |
|||
if ks is None: break |
|||
|
|||
if ks == 0: break |
|||
|
|||
key = fd.read(ks) |
|||
vs = deser_compact_size(fd) |
|||
val = fd.read(vs) |
|||
|
|||
kt = key[0] |
|||
if kt == PSBT_GLOBAL_UNSIGNED_TX: |
|||
self.txn = val |
|||
|
|||
self.parsed_txn = Transaction(val.hex()) |
|||
num_ins = len(self.parsed_txn.inputs()) |
|||
num_outs = len(self.parsed_txn.outputs()) |
|||
|
|||
elif kt == PSBT_GLOBAL_XPUB: |
|||
# key=(xpub) => val=(path) |
|||
self.xpubs.append( (key, val) ) |
|||
else: |
|||
raise ValueError('unknown global key type: 0x%02x' % kt) |
|||
|
|||
assert self.txn, 'missing reqd section' |
|||
|
|||
self.inputs = [BasicPSBTInput(fd, idx) for idx in range(num_ins)] |
|||
self.outputs = [BasicPSBTOutput(fd, idx) for idx in range(num_outs)] |
|||
|
|||
sep = fd.read(1) |
|||
assert sep == b'' |
|||
|
|||
return self |
|||
|
|||
def serialize(self, fd): |
|||
|
|||
def wr(ktype, val, key=b''): |
|||
fd.write(ser_compact_size(1 + len(key))) |
|||
fd.write(bytes([ktype]) + key) |
|||
fd.write(ser_compact_size(len(val))) |
|||
fd.write(val) |
|||
|
|||
fd.write(b'psbt\xff') |
|||
|
|||
wr(PSBT_GLOBAL_UNSIGNED_TX, self.txn) |
|||
|
|||
for k,v in self.xpubs: |
|||
wr(PSBT_GLOBAL_XPUB, v, key=k) |
|||
|
|||
# sep |
|||
fd.write(b'\0') |
|||
|
|||
for idx, inp in enumerate(self.inputs): |
|||
inp.serialize(fd, idx) |
|||
|
|||
for idx, outp in enumerate(self.outputs): |
|||
outp.serialize(fd, idx) |
|||
|
|||
def as_bytes(self): |
|||
with io.BytesIO() as fd: |
|||
self.serialize(fd) |
|||
return fd.getvalue() |
|||
|
|||
# EOF |
|||
|
@ -0,0 +1,391 @@ |
|||
# |
|||
# build_psbt.py - create a PSBT from (unsigned) transaction and keystore data. |
|||
# |
|||
import io, struct |
|||
from base64 import b64decode |
|||
from binascii import a2b_hex, b2a_hex |
|||
from struct import pack, unpack |
|||
|
|||
from electrum.transaction import (Transaction, multisig_script, parse_redeemScript_multisig, |
|||
NotRecognizedRedeemScript) |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.wallet import Standard_Wallet, Multisig_Wallet, Wallet |
|||
from electrum.keystore import xpubkey_to_pubkey, Xpub |
|||
from electrum.util import bfh, bh2u |
|||
from electrum.crypto import hash_160 |
|||
from electrum.bitcoin import DecodeBase58Check |
|||
|
|||
from .basic_psbt import ( |
|||
PSBT_GLOBAL_UNSIGNED_TX, PSBT_GLOBAL_XPUB, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, |
|||
PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, PSBT_IN_PARTIAL_SIG, |
|||
PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION, PSBT_OUT_REDEEM_SCRIPT) |
|||
from .basic_psbt import BasicPSBT |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.wallet import Standard_Wallet, Multisig_Wallet, Wallet |
|||
from electrum.util import bfh, bh2u |
|||
from electrum.crypto import hash_160 |
|||
from electrum.bitcoin import DecodeBase58Check |
|||
|
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
def xfp2str(xfp): |
|||
# Standardized way to show an xpub's fingerprint... it's a 4-byte string |
|||
# and not really an integer. Used to show as '0x%08x' but that's wrong endian. |
|||
return b2a_hex(pack('<I', xfp)).decode('ascii').upper() |
|||
|
|||
def xfp_from_xpub(xpub): |
|||
# sometime we need to BIP32 fingerprint value: 4 bytes of ripemd(sha256(pubkey)) |
|||
kk = bfh(Xpub.get_pubkey_from_xpub(xpub, [])) |
|||
assert len(kk) == 33 |
|||
xfp, = unpack('<I', hash_160(kk)[0:4]) |
|||
return xfp |
|||
|
|||
def packed_xfp_path(xfp, text_path, int_path=[]): |
|||
# Convert text subkey derivation path into binary format needed for PSBT |
|||
# - binary LE32 values, first one is the fingerprint |
|||
rv = pack('<I', xfp) |
|||
|
|||
for x in text_path.split('/'): |
|||
if x == 'm': continue |
|||
if x.endswith("'"): |
|||
x = int(x[:-1]) | 0x80000000 |
|||
else: |
|||
x = int(x) |
|||
rv += pack('<I', x) |
|||
|
|||
for x in int_path: |
|||
rv += pack('<I', x) |
|||
|
|||
return rv |
|||
|
|||
def unpacked_xfp_path(xfp, text_path): |
|||
# Convert text subkey derivation path into format needed for PSBT |
|||
# - binary LE32 values, first one is the fingerprint |
|||
# - but as ints, not bytes yet |
|||
rv = [xfp] |
|||
for x in text_path.split('/'): |
|||
if x == 'm': continue |
|||
if x.endswith("'"): |
|||
x = int(x[:-1]) | 0x80000000 |
|||
else: |
|||
x = int(x) |
|||
rv.append(x) |
|||
return rv |
|||
|
|||
def xfp_for_keystore(ks): |
|||
# Need the fingerprint of the MASTER key for a keystore we're playing with. |
|||
xfp = getattr(ks, 'ckcc_xfp', None) |
|||
|
|||
if xfp is None: |
|||
xfp = xfp_from_xpub(ks.get_master_public_key()) |
|||
setattr(ks, 'ckcc_xfp', xfp) |
|||
|
|||
return xfp |
|||
|
|||
|
|||
# Serialization/deserialization tools |
|||
def ser_compact_size(l): |
|||
r = b"" |
|||
if l < 253: |
|||
r = struct.pack("B", l) |
|||
elif l < 0x10000: |
|||
r = struct.pack("<BH", 253, l) |
|||
elif l < 0x100000000: |
|||
r = struct.pack("<BI", 254, l) |
|||
else: |
|||
r = struct.pack("<BQ", 255, l) |
|||
return r |
|||
|
|||
def deser_compact_size(f): |
|||
try: |
|||
nit = f.read(1)[0] |
|||
except IndexError: |
|||
return None # end of file |
|||
|
|||
if nit == 253: |
|||
nit = struct.unpack("<H", f.read(2))[0] |
|||
elif nit == 254: |
|||
nit = struct.unpack("<I", f.read(4))[0] |
|||
elif nit == 255: |
|||
nit = struct.unpack("<Q", f.read(8))[0] |
|||
return nit |
|||
|
|||
def my_var_int(l): |
|||
# Bitcoin serialization of integers... directly into binary! |
|||
if l < 253: |
|||
return pack("B", l) |
|||
elif l < 0x10000: |
|||
return pack("<BH", 253, l) |
|||
elif l < 0x100000000: |
|||
return pack("<BI", 254, l) |
|||
else: |
|||
return pack("<BQ", 255, l) |
|||
|
|||
def build_psbt(tx: Transaction, wallet: Wallet): |
|||
# Render a PSBT file, for possible upload to Coldcard. |
|||
# |
|||
# TODO this should be part of Wallet object, or maybe Transaction? |
|||
|
|||
if getattr(tx, 'raw_psbt', False): |
|||
_logger.info('PSBT cache hit') |
|||
return tx.raw_psbt |
|||
|
|||
inputs = tx.inputs() |
|||
if 'prev_tx' not in inputs[0]: |
|||
# fetch info about inputs, if needed? |
|||
# - needed during export PSBT flow, not normal online signing |
|||
wallet.add_hw_info(tx) |
|||
|
|||
# wallet.add_hw_info installs this attr |
|||
assert tx.output_info is not None, 'need data about outputs' |
|||
|
|||
# Build a map of all pubkeys needed as derivation from master XFP, in PSBT binary format |
|||
# 1) binary version of the common subpath for all keys |
|||
# m/ => fingerprint LE32 |
|||
# a/b/c => ints |
|||
# |
|||
# 2) all used keys in transaction: |
|||
# - for all inputs and outputs (when its change back) |
|||
# - for all keystores, if multisig |
|||
# |
|||
subkeys = {} |
|||
for ks in wallet.get_keystores(): |
|||
|
|||
# XFP + fixed prefix for this keystore |
|||
ks_prefix = packed_xfp_path(xfp_for_keystore(ks), ks.get_derivation()[2:]) |
|||
|
|||
# all pubkeys needed for input signing |
|||
for xpubkey, derivation in ks.get_tx_derivations(tx).items(): |
|||
pubkey = xpubkey_to_pubkey(xpubkey) |
|||
|
|||
# assuming depth two, non-harded: change + index |
|||
aa, bb = derivation |
|||
assert 0 <= aa < 0x80000000 and 0 <= bb < 0x80000000 |
|||
|
|||
subkeys[bfh(pubkey)] = ks_prefix + pack('<II', aa, bb) |
|||
|
|||
# all keys related to change outputs |
|||
for o in tx.outputs(): |
|||
if o.address in tx.output_info: |
|||
# this address "is_mine" but might not be change (if I send funds to myself) |
|||
chg_path = tx.output_info.get(o.address).address_index |
|||
|
|||
if chg_path[0] != 1 or len(chg_path) != 2: |
|||
# not change. |
|||
continue |
|||
|
|||
pubkey = ks.derive_pubkey(True, chg_path[1]) |
|||
subkeys[bfh(pubkey)] = ks_prefix + pack('<II', *chg_path) |
|||
|
|||
for txin in inputs: |
|||
assert txin['type'] != 'coinbase', _("Coinbase not supported") |
|||
|
|||
if txin['type'] in ['p2sh', 'p2wsh-p2sh', 'p2wsh']: |
|||
assert type(wallet) is Multisig_Wallet |
|||
|
|||
# Construct PSBT from start to finish. |
|||
out_fd = io.BytesIO() |
|||
out_fd.write(b'psbt\xff') |
|||
|
|||
def write_kv(ktype, val, key=b''): |
|||
# serialize helper: write w/ size and key byte |
|||
out_fd.write(my_var_int(1 + len(key))) |
|||
out_fd.write(bytes([ktype]) + key) |
|||
|
|||
if isinstance(val, str): |
|||
val = bfh(val) |
|||
|
|||
out_fd.write(my_var_int(len(val))) |
|||
out_fd.write(val) |
|||
|
|||
|
|||
# global section: just the unsigned txn |
|||
class CustomTXSerialization(Transaction): |
|||
@classmethod |
|||
def input_script(cls, txin, estimate_size=False): |
|||
return '' |
|||
|
|||
unsigned = bfh(CustomTXSerialization(tx.serialize()).serialize_to_network(witness=False)) |
|||
write_kv(PSBT_GLOBAL_UNSIGNED_TX, unsigned) |
|||
|
|||
if type(wallet) is Multisig_Wallet: |
|||
|
|||
# always put the xpubs into the PSBT, useful at least for checking |
|||
for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): |
|||
xfp = xfp_for_keystore(ks) |
|||
|
|||
dd = getattr(ks, 'derivation', 'm') |
|||
|
|||
write_kv(PSBT_GLOBAL_XPUB, packed_xfp_path(xfp, dd), DecodeBase58Check(xp)) |
|||
|
|||
# end globals section |
|||
out_fd.write(b'\x00') |
|||
|
|||
# inputs section |
|||
for txin in inputs: |
|||
if Transaction.is_segwit_input(txin): |
|||
utxo = txin['prev_tx'].outputs()[txin['prevout_n']] |
|||
spendable = txin['prev_tx'].serialize_output(utxo) |
|||
write_kv(PSBT_IN_WITNESS_UTXO, spendable) |
|||
else: |
|||
write_kv(PSBT_IN_NON_WITNESS_UTXO, str(txin['prev_tx'])) |
|||
|
|||
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) |
|||
|
|||
pubkeys = [bfh(k) for k in pubkeys] |
|||
|
|||
if type(wallet) is Multisig_Wallet: |
|||
# always need a redeem script for multisig |
|||
scr = Transaction.get_preimage_script(txin) |
|||
|
|||
if 'p2wsh' in txin['type']: |
|||
# needed for both p2wsh-p2sh and p2wsh |
|||
write_kv(PSBT_IN_WITNESS_SCRIPT, bfh(scr)) |
|||
else: |
|||
write_kv(PSBT_IN_REDEEM_SCRIPT, bfh(scr)) |
|||
|
|||
sigs = txin.get('signatures') |
|||
|
|||
for pk_pos, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)): |
|||
if pubkey in subkeys: |
|||
# faster? case ... calculated above |
|||
write_kv(PSBT_IN_BIP32_DERIVATION, subkeys[pubkey], pubkey) |
|||
else: |
|||
# when an input is partly signed, tx.get_tx_derivations() |
|||
# doesn't include that keystore's value and yet we need it |
|||
# because we need to show a correct keypath... |
|||
assert x_pubkey[0:2] == 'ff', x_pubkey |
|||
|
|||
for ks in wallet.get_keystores(): |
|||
d = ks.get_pubkey_derivation(x_pubkey) |
|||
if d is not None: |
|||
ks_path = packed_xfp_path(xfp_for_keystore(ks), ks.get_derivation()[2:], d) |
|||
write_kv(PSBT_IN_BIP32_DERIVATION, ks_path, pubkey) |
|||
break |
|||
else: |
|||
raise AssertionError("no keystore for: %s" % x_pubkey) |
|||
|
|||
if txin['type'] == 'p2wpkh-p2sh': |
|||
assert len(pubkeys) == 1, 'can be only one redeem script per input' |
|||
pa = hash_160(k) |
|||
assert len(pa) == 20 |
|||
write_kv(PSBT_IN_REDEEM_SCRIPT, b'\x00\x14'+pa) |
|||
|
|||
# optional? insert (partial) signatures that we already have |
|||
if sigs and sigs[pk_pos]: |
|||
write_kv(PSBT_IN_PARTIAL_SIG, bfh(sigs[pk_pos]), pubkey) |
|||
|
|||
out_fd.write(b'\x00') |
|||
|
|||
# outputs section |
|||
for o in tx.outputs(): |
|||
# can be empty, but must be present, and helpful to show change inputs |
|||
# wallet.add_hw_info() adds some data about change outputs into tx.output_info |
|||
if o.address in tx.output_info: |
|||
# this address "is_mine" but might not be change (if I send funds to myself) |
|||
output_info = tx.output_info.get(o.address) |
|||
chg_path, master_xpubs = output_info.address_index, output_info.sorted_xpubs |
|||
|
|||
if chg_path[0] == 1 and len(chg_path) == 2: |
|||
# it is a change output (based on our standard derivation path) |
|||
pubkeys = [bfh(i) for i in wallet.get_public_keys(o.address)] |
|||
|
|||
# always need a redeem script for multisig |
|||
if type(wallet) is Multisig_Wallet: |
|||
scr = multisig_script([bh2u(i) for i in sorted(pubkeys)], wallet.m) |
|||
write_kv(PSBT_OUT_REDEEM_SCRIPT, bfh(scr)) |
|||
|
|||
# document change output's bip32 derivation(s) |
|||
for pubkey in pubkeys: |
|||
sk = subkeys[pubkey] |
|||
write_kv(PSBT_OUT_BIP32_DERIVATION, sk, pubkey) |
|||
|
|||
if output_info.script_type == 'p2wpkh-p2sh': |
|||
assert len(pa) == 20 |
|||
assert len(pubkeys) == 1 |
|||
pa = hash_160(pubkey) |
|||
write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x14' + pa) |
|||
|
|||
out_fd.write(b'\x00') |
|||
|
|||
# capture for later use |
|||
tx.raw_psbt = out_fd.getvalue() |
|||
|
|||
return tx.raw_psbt |
|||
|
|||
|
|||
def recover_tx_from_psbt(first: BasicPSBT, wallet: Wallet) -> Transaction: |
|||
# Take a PSBT object and re-construct the Electrum transaction object. |
|||
# - does not include signatures, see merge_sigs_from_psbt |
|||
# - any PSBT in the group could be used for this purpose; all must share tx details |
|||
|
|||
tx = Transaction(first.txn.hex()) |
|||
tx.deserialize(force_full_parse=True) |
|||
|
|||
# .. add back some data that's been preserved in the PSBT, but isn't part of |
|||
# of the unsigned bitcoin txn |
|||
tx.is_partial_originally = True |
|||
|
|||
for idx, inp in enumerate(tx.inputs()): |
|||
scr = first.inputs[idx].redeem_script or first.inputs[idx].witness_script |
|||
|
|||
# XXX should use transaction.py parse_scriptSig() here! |
|||
if scr: |
|||
try: |
|||
M, N, __, pubkeys, __ = parse_redeemScript_multisig(scr) |
|||
except NotRecognizedRedeemScript: |
|||
# limitation: we can only handle M-of-N multisig here |
|||
raise ValueError("Cannot handle non M-of-N multisig input") |
|||
|
|||
inp['pubkeys'] = pubkeys |
|||
inp['x_pubkeys'] = pubkeys |
|||
inp['num_sig'] = M |
|||
inp['type'] = 'p2wsh' if first.inputs[idx].witness_script else 'p2sh' |
|||
|
|||
# bugfix: transaction.py:parse_input() puts empty dict here, but need a list |
|||
inp['signatures'] = [None] * N |
|||
|
|||
if 'prev_tx' not in inp: |
|||
# fetch info about inputs' previous txn |
|||
wallet.add_hw_info(tx) |
|||
|
|||
if 'value' not in inp: |
|||
# we'll need to know the value of the outpts used as part |
|||
# of the witness data, much later... |
|||
inp['value'] = inp['prev_tx'].outputs()[inp['prevout_n']].value |
|||
|
|||
return tx |
|||
|
|||
def merge_sigs_from_psbt(tx: Transaction, psbt: BasicPSBT): |
|||
# Take new signatures from PSBT, and merge into in-memory transaction object. |
|||
# - "we trust everyone here" ... no validation/checks |
|||
|
|||
count = 0 |
|||
for inp_idx, inp in enumerate(psbt.inputs): |
|||
if not inp.part_sigs: |
|||
continue |
|||
|
|||
scr = inp.redeem_script or inp.witness_script |
|||
|
|||
# need to map from pubkey to signing position in redeem script |
|||
M, N, _, pubkeys, _ = parse_redeemScript_multisig(scr) |
|||
#assert (M, N) == (wallet.m, wallet.n) |
|||
|
|||
for sig_pk in inp.part_sigs: |
|||
pk_pos = pubkeys.index(sig_pk.hex()) |
|||
tx.add_signature_to_txin(inp_idx, pk_pos, inp.part_sigs[sig_pk].hex()) |
|||
count += 1 |
|||
|
|||
#print("#%d: sigs = %r" % (inp_idx, tx.inputs()[inp_idx]['signatures'])) |
|||
|
|||
# reset serialization of TX |
|||
tx.raw = tx.serialize() |
|||
tx.raw_psbt = None |
|||
|
|||
return count |
|||
|
|||
# EOF |
|||
|
Loading…
Reference in new issue