electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 707b74d22b28d942c445754311736f158e505990
parent 6d12ebabbb686bcd028b659c6a6ce2cb4782bc14
Author: ThomasV <thomasv@electrum.org>
Date:   Thu,  7 Nov 2019 17:10:20 +0100

Merge pull request #5721 from SomberNight/201910_psbt

integrate PSBT support natively. WIP
Diffstat:
Melectrum/address_synchronizer.py | 94+++++++++++++++++++++++++++++++++++++------------------------------------------
Melectrum/base_wizard.py | 9++++++---
Melectrum/bip32.py | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Melectrum/bitcoin.py | 24++++++++++++++++--------
Melectrum/coinchooser.py | 81++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Melectrum/commands.py | 97++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Melectrum/ecc.py | 10++++++++++
Melectrum/gui/kivy/main_window.py | 16++++++----------
Melectrum/gui/kivy/uix/dialogs/__init__.py | 11++++++++---
Melectrum/gui/kivy/uix/dialogs/tx_dialog.py | 37++++++++++++++++++++++++++++---------
Melectrum/gui/kivy/uix/screens.py | 12+++++-------
Melectrum/gui/qt/address_dialog.py | 12++++++++----
Melectrum/gui/qt/main_window.py | 104++++++++++++++++++++++++++++++++++++++++----------------------------------------
Melectrum/gui/qt/paytoedit.py | 40+++++++++++++++++++---------------------
Melectrum/gui/qt/transaction_dialog.py | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Melectrum/gui/qt/util.py | 9++++++---
Melectrum/gui/qt/utxo_list.py | 48+++++++++++++++++++++++++-----------------------
Melectrum/gui/stdio.py | 9+++++----
Melectrum/gui/text.py | 9+++++----
Melectrum/json_db.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Melectrum/keystore.py | 372++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Melectrum/lnchannel.py | 48++++++++++++++++++++++--------------------------
Melectrum/lnpeer.py | 28+++++++++++++++-------------
Melectrum/lnsweep.py | 113++++++++++++++++++++++++++++++++++---------------------------------------------
Melectrum/lnutil.py | 105++++++++++++++++++++++++++++++++++++++-----------------------------------------
Melectrum/lnwatcher.py | 6++++--
Melectrum/lnworker.py | 2+-
Melectrum/network.py | 5+++--
Melectrum/paymentrequest.py | 23++++++++++++-----------
Melectrum/plugin.py | 8++++++--
Melectrum/plugins/audio_modem/qt.py | 8++++++--
Delectrum/plugins/coldcard/basic_psbt.py | 313-------------------------------------------------------------------------------
Delectrum/plugins/coldcard/build_psbt.py | 397-------------------------------------------------------------------------------
Melectrum/plugins/coldcard/coldcard.py | 169+++++++++++++++++++++++++++++++------------------------------------------------
Melectrum/plugins/coldcard/qt.py | 145++++++++++---------------------------------------------------------------------
Melectrum/plugins/cosigner_pool/qt.py | 48++++++++++++++++++++++++------------------------
Melectrum/plugins/digitalbitbox/digitalbitbox.py | 103+++++++++++++++++++++++++++++++++----------------------------------------------
Melectrum/plugins/digitalbitbox/qt.py | 12++++++------
Melectrum/plugins/greenaddress_instant/qt.py | 9+++++----
Melectrum/plugins/hw_wallet/plugin.py | 47++++++++++++++++++++++++++++++++++-------------
Melectrum/plugins/keepkey/keepkey.py | 220+++++++++++++++++++++++++++++++++++--------------------------------------------
Melectrum/plugins/ledger/ledger.py | 141++++++++++++++++++++++++++++++++++---------------------------------------------
Melectrum/plugins/safe_t/safe_t.py | 220+++++++++++++++++++++++++++++++++++--------------------------------------------
Melectrum/plugins/trezor/trezor.py | 155++++++++++++++++++++++++++++++++++++-------------------------------------------
Melectrum/plugins/trustedcoin/cmdline.py | 2+-
Aelectrum/plugins/trustedcoin/legacy_tx_format.py | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/plugins/trustedcoin/trustedcoin.py | 37++++++++++++++++++++++---------------
Melectrum/scripts/bip70.py | 3++-
Melectrum/segwit_addr.py | 2++
Melectrum/synchronizer.py | 6+++---
Melectrum/tests/regtest/regtest.sh | 6+++---
Melectrum/tests/test_bitcoin.py | 10+++++++++-
Melectrum/tests/test_commands.py | 21+++++++++++++++++++++
Melectrum/tests/test_lnchannel.py | 2+-
Melectrum/tests/test_lnutil.py | 11++++-------
Aelectrum/tests/test_psbt.py | 269+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/tests/test_transaction.py | 289+++++++++++++++++++++++++++++++++++++------------------------------------------
Melectrum/tests/test_wallet.py | 2+-
Melectrum/tests/test_wallet_vertical.py | 463++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Melectrum/transaction.py | 2130+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Melectrum/util.py | 6++++--
Melectrum/wallet.py | 537++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
62 files changed, 4171 insertions(+), 3420 deletions(-)

diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py @@ -29,9 +29,9 @@ from collections import defaultdict from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence from . import bitcoin -from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY +from .bitcoin import COINBASE_MATURITY from .util import profiler, bfh, TxMinedInfo -from .transaction import Transaction, TxOutput +from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint, PartialTransaction from .synchronizer import Synchronizer from .verifier import SPV from .blockchain import hash_header @@ -125,12 +125,12 @@ class AddressSynchronizer(Logger): """Return number of transactions where address is involved.""" return len(self._history_local.get(addr, ())) - def get_txin_address(self, txi) -> Optional[str]: - addr = txi.get('address') - if addr and addr != "(pubkey)": - return addr - prevout_hash = txi.get('prevout_hash') - prevout_n = txi.get('prevout_n') + def get_txin_address(self, txin: TxInput) -> Optional[str]: + if isinstance(txin, PartialTxInput): + if txin.address: + return txin.address + prevout_hash = txin.prevout.txid.hex() + prevout_n = txin.prevout.out_idx for addr in self.db.get_txo_addresses(prevout_hash): l = self.db.get_txo_addr(prevout_hash, addr) for n, v, is_cb in l: @@ -138,14 +138,8 @@ class AddressSynchronizer(Logger): return addr return None - def get_txout_address(self, txo: TxOutput): - if txo.type == TYPE_ADDRESS: - addr = txo.address - elif txo.type == TYPE_PUBKEY: - addr = bitcoin.public_key_to_p2pkh(bfh(txo.address)) - else: - addr = None - return addr + def get_txout_address(self, txo: TxOutput) -> Optional[str]: + return txo.address def load_unverified_transactions(self): # review transactions that are in the history @@ -183,7 +177,7 @@ class AddressSynchronizer(Logger): if self.synchronizer: self.synchronizer.add(address) - def get_conflicting_transactions(self, tx_hash, tx, include_self=False): + def get_conflicting_transactions(self, tx_hash, tx: Transaction, include_self=False): """Returns a set of transaction hashes from the wallet history that are directly conflicting with tx, i.e. they have common outpoints being spent with tx. @@ -194,10 +188,10 @@ class AddressSynchronizer(Logger): conflicting_txns = set() with self.transaction_lock: for txin in tx.inputs(): - if txin['type'] == 'coinbase': + if txin.is_coinbase(): continue - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] + prevout_hash = txin.prevout.txid.hex() + prevout_n = txin.prevout.out_idx spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n) if spending_tx_hash is None: continue @@ -213,7 +207,7 @@ class AddressSynchronizer(Logger): conflicting_txns -= {tx_hash} return conflicting_txns - def add_transaction(self, tx_hash, tx, allow_unrelated=False) -> bool: + def add_transaction(self, tx_hash, tx: Transaction, allow_unrelated=False) -> bool: """Returns whether the tx was successfully added to the wallet history.""" assert tx_hash, tx_hash assert tx, tx @@ -226,7 +220,7 @@ class AddressSynchronizer(Logger): # BUT we track is_mine inputs in a txn, and during subsequent calls # of add_transaction tx, we might learn of more-and-more inputs of # being is_mine, as we roll the gap_limit forward - is_coinbase = tx.inputs()[0]['type'] == 'coinbase' + is_coinbase = tx.inputs()[0].is_coinbase() tx_height = self.get_tx_height(tx_hash).height if not allow_unrelated: # note that during sync, if the transactions are not properly sorted, @@ -277,11 +271,11 @@ class AddressSynchronizer(Logger): self._get_addr_balance_cache.pop(addr, None) # invalidate cache return for txi in tx.inputs(): - if txi['type'] == 'coinbase': + if txi.is_coinbase(): continue - prevout_hash = txi['prevout_hash'] - prevout_n = txi['prevout_n'] - ser = prevout_hash + ':%d' % prevout_n + prevout_hash = txi.prevout.txid.hex() + prevout_n = txi.prevout.out_idx + ser = txi.prevout.to_str() self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash) add_value_from_prev_output() # add outputs @@ -310,10 +304,10 @@ class AddressSynchronizer(Logger): if tx is not None: # if we have the tx, this branch is faster for txin in tx.inputs(): - if txin['type'] == 'coinbase': + if txin.is_coinbase(): continue - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] + prevout_hash = txin.prevout.txid.hex() + prevout_n = txin.prevout.out_idx self.db.remove_spent_outpoint(prevout_hash, prevout_n) else: # expensive but always works @@ -572,7 +566,7 @@ class AddressSynchronizer(Logger): return cached_local_height return self.network.get_local_height() if self.network else self.db.get('stored_height', 0) - def add_future_tx(self, tx, num_blocks): + def add_future_tx(self, tx: Transaction, num_blocks): with self.lock: self.add_transaction(tx.txid(), tx) self.future_tx[tx.txid()] = num_blocks @@ -649,14 +643,16 @@ class AddressSynchronizer(Logger): if self.is_mine(addr): is_mine = True is_relevant = True - d = self.db.get_txo_addr(txin['prevout_hash'], addr) + d = self.db.get_txo_addr(txin.prevout.txid.hex(), addr) for n, v, cb in d: - if n == txin['prevout_n']: + if n == txin.prevout.out_idx: value = v break else: value = None if value is None: + value = txin.value_sats() + if value is None: is_pruned = True else: v_in += value @@ -736,23 +732,19 @@ class AddressSynchronizer(Logger): sent[txi] = height return received, sent - def get_addr_utxo(self, address): + def get_addr_utxo(self, address: str) -> Dict[TxOutpoint, PartialTxInput]: coins, spent = self.get_addr_io(address) for txi in spent: coins.pop(txi) out = {} - for txo, v in coins.items(): + for prevout_str, v in coins.items(): tx_height, value, is_cb = v - prevout_hash, prevout_n = txo.split(':') - x = { - 'address':address, - 'value':value, - 'prevout_n':int(prevout_n), - 'prevout_hash':prevout_hash, - 'height':tx_height, - 'coinbase':is_cb - } - out[txo] = x + prevout = TxOutpoint.from_str(prevout_str) + utxo = PartialTxInput(prevout=prevout) + utxo._trusted_address = address + utxo._trusted_value_sats = value + utxo.block_height = tx_height + out[prevout] = utxo return out # return the total amount ever received by an address @@ -799,7 +791,8 @@ class AddressSynchronizer(Logger): @with_local_height_cached def get_utxos(self, domain=None, *, excluded_addresses=None, - mature_only: bool = False, confirmed_only: bool = False, nonlocal_only: bool = False): + mature_only: bool = False, confirmed_only: bool = False, + nonlocal_only: bool = False) -> Sequence[PartialTxInput]: coins = [] if domain is None: domain = self.get_addresses() @@ -809,14 +802,15 @@ class AddressSynchronizer(Logger): mempool_height = self.get_local_height() + 1 # height of next block for addr in domain: utxos = self.get_addr_utxo(addr) - for x in utxos.values(): - if confirmed_only and x['height'] <= 0: + for utxo in utxos.values(): + if confirmed_only and utxo.block_height <= 0: continue - if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL: + if nonlocal_only and utxo.block_height == TX_HEIGHT_LOCAL: continue - if mature_only and x['coinbase'] and x['height'] + COINBASE_MATURITY > mempool_height: + if (mature_only and utxo.prevout.is_coinbase() + and utxo.block_height + COINBASE_MATURITY > mempool_height): continue - coins.append(x) + coins.append(utxo) continue return coins diff --git a/electrum/base_wizard.py b/electrum/base_wizard.py @@ -33,7 +33,7 @@ from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any, Dict, Optional from . import bitcoin from . import keystore from . import mnemonic -from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation +from .bip32 import is_bip32_derivation, xpub_type, normalize_bip32_derivation, BIP32Node from .keystore import bip44_derivation, purpose48_derivation from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet, Abstract_Wallet) @@ -230,7 +230,7 @@ class BaseWizard(Logger): assert bitcoin.is_private_key(pk) txin_type, pubkey = k.import_privkey(pk, None) addr = bitcoin.pubkey_to_address(txin_type, pubkey) - self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None} + self.data['addresses'][addr] = {'type':txin_type, 'pubkey':pubkey} self.keystores.append(k) else: return self.terminate() @@ -394,7 +394,7 @@ class BaseWizard(Logger): # For segwit, a custom path is used, as there is no standard at all. default_choice_idx = 2 choices = [ - ('standard', 'legacy multisig (p2sh)', "m/45'/0"), + ('standard', 'legacy multisig (p2sh)', normalize_bip32_derivation("m/45'/0")), ('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')), ('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')), ] @@ -420,16 +420,19 @@ class BaseWizard(Logger): from .keystore import hardware_keystore try: xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) + root_xpub = self.plugin.get_xpub(device_info.device.id_, 'm', 'standard', self) except ScriptTypeNotSupported: raise # this is handled in derivation_dialog except BaseException as e: self.logger.exception('') self.show_error(e) return + xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower() d = { 'type': 'hardware', 'hw_type': name, 'derivation': derivation, + 'root_fingerprint': xfp, 'xpub': xpub, 'label': device_info.label, } diff --git a/electrum/bip32.py b/electrum/bip32.py @@ -3,7 +3,7 @@ # file LICENCE or http://www.opensource.org/licenses/mit-license.php import hashlib -from typing import List, Tuple, NamedTuple, Union, Iterable +from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional from .util import bfh, bh2u, BitcoinException from . import constants @@ -116,7 +116,7 @@ class BIP32Node(NamedTuple): eckey: Union[ecc.ECPubkey, ecc.ECPrivkey] chaincode: bytes depth: int = 0 - fingerprint: bytes = b'\x00'*4 + fingerprint: bytes = b'\x00'*4 # as in serialized format, this is the *parent's* fingerprint child_number: bytes = b'\x00'*4 @classmethod @@ -161,7 +161,18 @@ class BIP32Node(NamedTuple): eckey=ecc.ECPrivkey(master_k), chaincode=master_c) + @classmethod + def from_bytes(cls, b: bytes) -> 'BIP32Node': + if len(b) != 78: + raise Exception(f"unexpected xkey raw bytes len {len(b)} != 78") + xkey = EncodeBase58Check(b) + return cls.from_xkey(xkey) + def to_xprv(self, *, net=None) -> str: + payload = self.to_xprv_bytes(net=net) + return EncodeBase58Check(payload) + + def to_xprv_bytes(self, *, net=None) -> bytes: if not self.is_private(): raise Exception("cannot serialize as xprv; private key missing") payload = (xprv_header(self.xtype, net=net) + @@ -172,9 +183,13 @@ class BIP32Node(NamedTuple): bytes([0]) + self.eckey.get_secret_bytes()) assert len(payload) == 78, f"unexpected xprv payload len {len(payload)}" - return EncodeBase58Check(payload) + return payload def to_xpub(self, *, net=None) -> str: + payload = self.to_xpub_bytes(net=net) + return EncodeBase58Check(payload) + + def to_xpub_bytes(self, *, net=None) -> bytes: payload = (xpub_header(self.xtype, net=net) + bytes([self.depth]) + self.fingerprint + @@ -182,7 +197,7 @@ class BIP32Node(NamedTuple): self.chaincode + self.eckey.get_public_key_bytes(compressed=True)) assert len(payload) == 78, f"unexpected xpub payload len {len(payload)}" - return EncodeBase58Check(payload) + return payload def to_xkey(self, *, net=None) -> str: if self.is_private(): @@ -190,6 +205,12 @@ class BIP32Node(NamedTuple): else: return self.to_xpub(net=net) + def to_bytes(self, *, net=None) -> bytes: + if self.is_private(): + return self.to_xprv_bytes(net=net) + else: + return self.to_xpub_bytes(net=net) + def convert_to_public(self) -> 'BIP32Node': if not self.is_private(): return self @@ -248,6 +269,12 @@ class BIP32Node(NamedTuple): fingerprint=fingerprint, child_number=child_number) + def calc_fingerprint_of_this_node(self) -> bytes: + """Returns the fingerprint of this node. + Note that self.fingerprint is of the *parent*. + """ + return hash_160(self.eckey.get_public_key_bytes(compressed=True))[0:4] + def xpub_type(x): return BIP32Node.from_xkey(x).xtype @@ -308,7 +335,7 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]: return path -def convert_bip32_intpath_to_strpath(path: List[int]) -> str: +def convert_bip32_intpath_to_strpath(path: Sequence[int]) -> str: s = "m/" for child_index in path: if not isinstance(child_index, int): @@ -336,8 +363,40 @@ def is_bip32_derivation(s: str) -> bool: return True -def normalize_bip32_derivation(s: str) -> str: +def normalize_bip32_derivation(s: Optional[str]) -> Optional[str]: + if s is None: + return None if not is_bip32_derivation(s): raise ValueError(f"invalid bip32 derivation: {s}") ints = convert_bip32_path_to_list_of_uint32(s) return convert_bip32_intpath_to_strpath(ints) + + +def is_all_public_derivation(path: Union[str, Iterable[int]]) -> bool: + """Returns whether all levels in path use non-hardened derivation.""" + if isinstance(path, str): + path = convert_bip32_path_to_list_of_uint32(path) + for child_index in path: + if child_index < 0: + raise ValueError('the bip32 index needs to be non-negative') + if child_index & BIP32_PRIME: + return False + return True + + +def root_fp_and_der_prefix_from_xkey(xkey: str) -> Tuple[Optional[str], Optional[str]]: + """Returns the root bip32 fingerprint and the derivation path from the + root to the given xkey, if they can be determined. Otherwise (None, None). + """ + node = BIP32Node.from_xkey(xkey) + derivation_prefix = None + root_fingerprint = None + assert node.depth >= 0, node.depth + if node.depth == 0: + derivation_prefix = 'm' + root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower() + elif node.depth == 1: + child_number_int = int.from_bytes(node.child_number, 'big') + derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int]) + root_fingerprint = node.fingerprint.hex() + return root_fingerprint, derivation_prefix diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py @@ -45,6 +45,7 @@ COIN = 100000000 TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000 # supported types of transaction outputs +# TODO kill these with fire TYPE_ADDRESS = 0 TYPE_PUBKEY = 1 TYPE_SCRIPT = 2 @@ -237,6 +238,9 @@ def script_num_to_hex(i: int) -> str: def var_int(i: int) -> str: # https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer + # https://github.com/bitcoin/bitcoin/blob/efe1ee0d8d7f82150789f1f6840f139289628a2b/src/serialize.h#L247 + # "CompactSize" + assert i >= 0, i if i<0xfd: return int_to_hex(i) elif i<=0xffff: @@ -372,24 +376,28 @@ def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: else: raise NotImplementedError(txin_type) -def redeem_script_to_address(txin_type: str, redeem_script: str, *, net=None) -> str: + +# TODO this method is confusingly named +def redeem_script_to_address(txin_type: str, scriptcode: str, *, net=None) -> str: if net is None: net = constants.net if txin_type == 'p2sh': - return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net) + # given scriptcode is a redeem_script + return hash160_to_p2sh(hash_160(bfh(scriptcode)), net=net) elif txin_type == 'p2wsh': - return script_to_p2wsh(redeem_script, net=net) + # given scriptcode is a witness_script + return script_to_p2wsh(scriptcode, net=net) elif txin_type == 'p2wsh-p2sh': - scriptSig = p2wsh_nested_script(redeem_script) - return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net) + # given scriptcode is a witness_script + redeem_script = p2wsh_nested_script(scriptcode) + return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net) else: raise NotImplementedError(txin_type) def script_to_address(script: str, *, net=None) -> str: from .transaction import get_address_from_output_script - t, addr = get_address_from_output_script(bfh(script), net=net) - assert t == TYPE_ADDRESS - return addr + return get_address_from_output_script(bfh(script), net=net) + def address_to_script(addr: str, *, net=None) -> str: if net is None: net = constants.net diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py @@ -24,11 +24,11 @@ # SOFTWARE. from collections import defaultdict from math import floor, log10 -from typing import NamedTuple, List, Callable +from typing import NamedTuple, List, Callable, Sequence, Union, Dict, Tuple from decimal import Decimal -from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address -from .transaction import Transaction, TxOutput +from .bitcoin import sha256, COIN, is_address +from .transaction import Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput from .util import NotEnoughFunds from .logging import Logger @@ -73,21 +73,21 @@ class PRNG: class Bucket(NamedTuple): desc: str - weight: int # as in BIP-141 - value: int # in satoshis - effective_value: int # estimate of value left after subtracting fees. in satoshis - coins: List[dict] # UTXOs - min_height: int # min block height where a coin was confirmed - witness: bool # whether any coin uses segwit + weight: int # as in BIP-141 + value: int # in satoshis + effective_value: int # estimate of value left after subtracting fees. in satoshis + coins: List[PartialTxInput] # UTXOs + min_height: int # min block height where a coin was confirmed + witness: bool # whether any coin uses segwit class ScoredCandidate(NamedTuple): penalty: float - tx: Transaction + tx: PartialTransaction buckets: List[Bucket] -def strip_unneeded(bkts, sufficient_funds): +def strip_unneeded(bkts: List[Bucket], sufficient_funds) -> List[Bucket]: '''Remove buckets that are unnecessary in achieving the spend amount''' if sufficient_funds([], bucket_value_sum=0): # none of the buckets are needed @@ -108,26 +108,27 @@ class CoinChooserBase(Logger): def __init__(self): Logger.__init__(self) - def keys(self, coins): + def keys(self, coins: Sequence[PartialTxInput]) -> Sequence[str]: raise NotImplementedError - def bucketize_coins(self, coins, *, fee_estimator_vb): + def bucketize_coins(self, coins: Sequence[PartialTxInput], *, fee_estimator_vb): keys = self.keys(coins) - buckets = defaultdict(list) + buckets = defaultdict(list) # type: Dict[str, List[PartialTxInput]] for key, coin in zip(keys, coins): buckets[key].append(coin) # fee_estimator returns fee to be paid, for given vbytes. # guess whether it is just returning a constant as follows. constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200) - def make_Bucket(desc, coins): + def make_Bucket(desc: str, coins: List[PartialTxInput]): witness = any(Transaction.is_segwit_input(coin, guess_for_address=True) for coin in coins) # note that we're guessing whether the tx uses segwit based # on this single bucket weight = sum(Transaction.estimated_input_weight(coin, witness) for coin in coins) - value = sum(coin['value'] for coin in coins) - min_height = min(coin['height'] for coin in coins) + value = sum(coin.value_sats() for coin in coins) + min_height = min(coin.block_height for coin in coins) + assert min_height is not None # the fee estimator is typically either a constant or a linear function, # so the "function:" effective_value(bucket) will be homomorphic for addition # i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2) @@ -148,10 +149,12 @@ class CoinChooserBase(Logger): return list(map(make_Bucket, buckets.keys(), buckets.values())) - def penalty_func(self, base_tx, *, tx_from_buckets) -> Callable[[List[Bucket]], ScoredCandidate]: + def penalty_func(self, base_tx, *, + tx_from_buckets: Callable[[List[Bucket]], Tuple[PartialTransaction, List[PartialTxOutput]]]) \ + -> Callable[[List[Bucket]], ScoredCandidate]: raise NotImplementedError - def _change_amounts(self, tx, count, fee_estimator_numchange) -> List[int]: + def _change_amounts(self, tx: PartialTransaction, count: int, fee_estimator_numchange) -> List[int]: # Break change up if bigger than max_change output_amounts = [o.value for o in tx.outputs()] # Don't split change of less than 0.02 BTC @@ -205,7 +208,8 @@ class CoinChooserBase(Logger): return amounts - def _change_outputs(self, tx, change_addrs, fee_estimator_numchange, dust_threshold): + def _change_outputs(self, tx: PartialTransaction, change_addrs, fee_estimator_numchange, + dust_threshold) -> List[PartialTxOutput]: amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange) assert min(amounts) >= 0 assert len(change_addrs) >= len(amounts) @@ -213,21 +217,23 @@ class CoinChooserBase(Logger): # If change is above dust threshold after accounting for the # size of the change output, add it to the transaction. amounts = [amount for amount in amounts if amount >= dust_threshold] - change = [TxOutput(TYPE_ADDRESS, addr, amount) + change = [PartialTxOutput.from_address_and_value(addr, amount) for addr, amount in zip(change_addrs, amounts)] return change - def _construct_tx_from_selected_buckets(self, *, buckets, base_tx, change_addrs, - fee_estimator_w, dust_threshold, base_weight): + def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket], + base_tx: PartialTransaction, change_addrs, + fee_estimator_w, dust_threshold, + base_weight) -> Tuple[PartialTransaction, List[PartialTxOutput]]: # make a copy of base_tx so it won't get mutated - tx = Transaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:]) + tx = PartialTransaction.from_io(base_tx.inputs()[:], base_tx.outputs()[:]) tx.add_inputs([coin for b in buckets for coin in b.coins]) tx_weight = self._get_tx_weight(buckets, base_weight=base_weight) # change is sent back to sending address unless specified if not change_addrs: - change_addrs = [tx.inputs()[0]['address']] + change_addrs = [tx.inputs()[0].address] # note: this is not necessarily the final "first input address" # because the inputs had not been sorted at this point assert is_address(change_addrs[0]) @@ -240,7 +246,7 @@ class CoinChooserBase(Logger): return tx, change - def _get_tx_weight(self, buckets, *, base_weight) -> int: + def _get_tx_weight(self, buckets: Sequence[Bucket], *, base_weight: int) -> int: """Given a collection of buckets, return the total weight of the resulting transaction. base_weight is the weight of the tx that includes the fixed (non-change) @@ -260,8 +266,9 @@ class CoinChooserBase(Logger): return total_weight - def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator_vb, - dust_threshold): + def make_tx(self, *, coins: Sequence[PartialTxInput], inputs: List[PartialTxInput], + outputs: List[PartialTxOutput], change_addrs: Sequence[str], + fee_estimator_vb: Callable, dust_threshold: int) -> PartialTransaction: """Select unspent coins to spend to pay outputs. If the change is greater than dust_threshold (after adding the change output to the transaction) it is kept, otherwise none is sent and it is @@ -276,11 +283,11 @@ class CoinChooserBase(Logger): assert outputs, 'tx outputs cannot be empty' # Deterministic randomness from coins - utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins] - self.p = PRNG(''.join(sorted(utxos))) + utxos = [c.prevout.serialize_to_network() for c in coins] + self.p = PRNG(b''.join(sorted(utxos))) # Copy the outputs so when adding change we don't modify "outputs" - base_tx = Transaction.from_io(inputs[:], outputs[:]) + base_tx = PartialTransaction.from_io(inputs[:], outputs[:]) input_value = base_tx.input_value() # Weight of the transaction with no inputs and no change @@ -331,14 +338,15 @@ class CoinChooserBase(Logger): return tx - def choose_buckets(self, buckets, sufficient_funds, + def choose_buckets(self, buckets: List[Bucket], + sufficient_funds: Callable, penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate: raise NotImplemented('To be subclassed') class CoinChooserRandom(CoinChooserBase): - def bucket_candidates_any(self, buckets, sufficient_funds): + def bucket_candidates_any(self, buckets: List[Bucket], sufficient_funds) -> List[List[Bucket]]: '''Returns a list of bucket sets.''' if not buckets: raise NotEnoughFunds() @@ -373,7 +381,8 @@ class CoinChooserRandom(CoinChooserBase): candidates = [[buckets[n] for n in c] for c in candidates] return [strip_unneeded(c, sufficient_funds) for c in candidates] - def bucket_candidates_prefer_confirmed(self, buckets, sufficient_funds): + def bucket_candidates_prefer_confirmed(self, buckets: List[Bucket], + sufficient_funds) -> List[List[Bucket]]: """Returns a list of bucket sets preferring confirmed coins. Any bucket can be: @@ -433,13 +442,13 @@ class CoinChooserPrivacy(CoinChooserRandom): """ def keys(self, coins): - return [coin['address'] for coin in coins] + return [coin.scriptpubkey.hex() for coin in coins] def penalty_func(self, base_tx, *, tx_from_buckets): min_change = min(o.value for o in base_tx.outputs()) * 0.75 max_change = max(o.value for o in base_tx.outputs()) * 1.33 - def penalty(buckets) -> ScoredCandidate: + def penalty(buckets: List[Bucket]) -> ScoredCandidate: # Penalize using many buckets (~inputs) badness = len(buckets) - 1 tx, change_outputs = tx_from_buckets(buckets) diff --git a/electrum/commands.py b/electrum/commands.py @@ -35,16 +35,17 @@ import asyncio import inspect from functools import wraps, partial from decimal import Decimal -from typing import Optional, TYPE_CHECKING, Dict +from typing import Optional, TYPE_CHECKING, Dict, List from .import util, ecc from .util import bfh, bh2u, format_satoshis, json_decode, json_encode, is_hash256_str, is_hex_str, to_bytes, timestamp_to_datetime from .util import standardize_path from . import bitcoin -from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS +from .bitcoin import is_address, hash_160, COIN from .bip32 import BIP32Node from .i18n import _ -from .transaction import Transaction, multisig_script, TxOutput +from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput, + tx_from_any, PartialTxInput, TxOutpoint) from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED from .synchronizer import Notifier from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text @@ -299,11 +300,13 @@ class Commands: async def listunspent(self, wallet: Abstract_Wallet = None): """List unspent outputs. Returns the list of unspent transaction outputs in your wallet.""" - l = copy.deepcopy(wallet.get_utxos()) - for i in l: - v = i["value"] - i["value"] = str(Decimal(v)/COIN) if v is not None else None - return l + coins = [] + for txin in wallet.get_utxos(): + d = txin.to_json() + v = d.pop("value_sats") + d["value"] = str(Decimal(v)/COIN) if v is not None else None + coins.append(d) + return coins @command('n') async def getaddressunspent(self, address): @@ -320,46 +323,50 @@ class Commands: Outputs must be a list of {'address':address, 'value':satoshi_amount}. """ keypairs = {} - inputs = jsontx.get('inputs') - outputs = jsontx.get('outputs') + inputs = [] # type: List[PartialTxInput] locktime = jsontx.get('lockTime', 0) - for txin in inputs: - if txin.get('output'): - prevout_hash, prevout_n = txin['output'].split(':') - txin['prevout_n'] = int(prevout_n) - txin['prevout_hash'] = prevout_hash - sec = txin.get('privkey') + for txin_dict in jsontx.get('inputs'): + if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None: + prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n'])) + elif txin_dict.get('output'): + prevout = TxOutpoint.from_str(txin_dict['output']) + else: + raise Exception("missing prevout for txin") + txin = PartialTxInput(prevout=prevout) + txin._trusted_value_sats = int(txin_dict['value']) + sec = txin_dict.get('privkey') if sec: txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) keypairs[pubkey] = privkey, compressed - txin['type'] = txin_type - txin['x_pubkeys'] = [pubkey] - txin['signatures'] = [None] - txin['num_sig'] = 1 - - outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs] - tx = Transaction.from_io(inputs, outputs, locktime=locktime) + txin.script_type = txin_type + txin.pubkeys = [bfh(pubkey)] + txin.num_sig = 1 + inputs.append(txin) + + outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout['value'])) + for txout in jsontx.get('outputs')] + tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime) tx.sign(keypairs) - return tx.as_dict() + return tx.serialize() @command('wp') async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None): """Sign a transaction. The wallet keys will be used unless a private key is provided.""" - tx = Transaction(tx) + tx = PartialTransaction(tx) if privkey: txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey) pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex() tx.sign({pubkey:(privkey2, compressed)}) else: wallet.sign_transaction(tx, password) - return tx.as_dict() + return tx.serialize() @command('') async def deserialize(self, tx): """Deserialize a serialized transaction""" - tx = Transaction(tx) - return tx.deserialize(force_full_parse=True) + tx = tx_from_any(tx) + return tx.to_json() @command('n') async def broadcast(self, tx): @@ -392,9 +399,9 @@ class Commands: if isinstance(address, str): address = address.strip() if is_address(address): - return wallet.export_private_key(address, password)[0] + return wallet.export_private_key(address, password) domain = address - return [wallet.export_private_key(address, password)[0] for address in domain] + return [wallet.export_private_key(address, password) for address in domain] @command('w') async def ismine(self, address, wallet: Abstract_Wallet = None): @@ -513,8 +520,13 @@ class Commands: privkeys = privkey.split() self.nocheck = nocheck #dest = self._resolver(destination) - tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax) - return tx.as_dict() if tx else None + tx = sweep(privkeys, + network=self.network, + config=self.config, + to_address=destination, + fee=tx_fee, + imax=imax) + return tx.serialize() if tx else None @command('wp') async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None): @@ -541,17 +553,20 @@ class Commands: for address, amount in outputs: address = self._resolver(address, wallet) amount = satoshis(amount) - final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount)) + final_outputs.append(PartialTxOutput.from_address_and_value(address, amount)) coins = wallet.get_spendable_coins(domain_addr) if domain_coins is not None: - coins = [coin for coin in coins if (coin['prevout_hash'] + ':' + str(coin['prevout_n']) in domain_coins)] + coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)] if feerate is not None: fee_per_kb = 1000 * Decimal(feerate) fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb) else: fee_estimator = fee - tx = wallet.make_unsigned_transaction(coins, final_outputs, fee_estimator, change_addr) + tx = wallet.make_unsigned_transaction(coins=coins, + outputs=final_outputs, + fee=fee_estimator, + change_addr=change_addr) if locktime is not None: tx.locktime = locktime if rbf is None: @@ -581,7 +596,7 @@ class Commands: rbf=rbf, password=password, locktime=locktime) - return tx.as_dict() + return tx.serialize() @command('wp') async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, @@ -602,7 +617,7 @@ class Commands: rbf=rbf, password=password, locktime=locktime) - return tx.as_dict() + return tx.serialize() @command('w') async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None): @@ -703,7 +718,7 @@ class Commands: raise Exception("Unknown transaction") if tx.txid() != txid: raise Exception("Mismatching txid") - return tx.as_dict() + return tx.serialize() @command('') async def encrypt(self, pubkey, message) -> str: @@ -960,7 +975,7 @@ class Commands: chan_id, _ = channel_id_from_funding_tx(txid, int(index)) chan = wallet.lnworker.channels[chan_id] tx = chan.force_close_tx() - return tx.as_dict() + return tx.serialize() def eval_bool(x: str) -> bool: if x == 'false': return False @@ -1037,7 +1052,7 @@ command_options = { # don't use floats because of rounding errors -from .transaction import tx_from_str +from .transaction import convert_tx_str_to_hex json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x))) arg_types = { 'num': int, @@ -1046,7 +1061,7 @@ arg_types = { 'year': int, 'from_height': int, 'to_height': int, - 'tx': tx_from_str, + 'tx': convert_tx_str_to_hex, 'pubkeys': json_loads, 'jsontx': json_loads, 'inputs': json_loads, diff --git a/electrum/ecc.py b/electrum/ecc.py @@ -25,6 +25,7 @@ import base64 import hashlib +import functools from typing import Union, Tuple, Optional import ecdsa @@ -181,6 +182,7 @@ class _PubkeyForPointAtInfinity: point = ecdsa.ellipticcurve.INFINITY +@functools.total_ordering class ECPubkey(object): def __init__(self, b: Optional[bytes]): @@ -257,6 +259,14 @@ class ECPubkey(object): def __ne__(self, other): return not (self == other) + def __hash__(self): + return hash(self._pubkey.point.x()) + + def __lt__(self, other): + if not isinstance(other, ECPubkey): + raise TypeError('comparison not defined for ECPubkey and {}'.format(type(other))) + return self._pubkey.point.x() < other._pubkey.point.x() + def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: sha256d(msg_magic(x))) -> None: assert_bytes(message) h = algo(message) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py @@ -9,7 +9,6 @@ import threading import asyncio from typing import TYPE_CHECKING, Optional -from electrum.bitcoin import TYPE_ADDRESS from electrum.storage import WalletStorage, StorageReadWriteError from electrum.wallet import Wallet, InternalAddressCorruption from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter @@ -398,12 +397,9 @@ class ElectrumWindow(App): self.set_ln_invoice(data) return # try to decode transaction - from electrum.transaction import Transaction - from electrum.util import bh2u + from electrum.transaction import tx_from_any try: - text = bh2u(base_decode(data, None, base=43)) - tx = Transaction(text) - tx.deserialize() + tx = tx_from_any(data) except: tx = None if tx: @@ -855,7 +851,7 @@ class ElectrumWindow(App): self._trigger_update_status() def get_max_amount(self): - from electrum.transaction import TxOutput + from electrum.transaction import PartialTxOutput if run_hook('abort_send', self): return '' inputs = self.wallet.get_spendable_coins(None) @@ -866,9 +862,9 @@ class ElectrumWindow(App): addr = str(self.send_screen.screen.address) if not addr: addr = self.wallet.dummy_address() - outputs = [TxOutput(TYPE_ADDRESS, addr, '!')] + outputs = [PartialTxOutput.from_address_and_value(addr, '!')] try: - tx = self.wallet.make_unsigned_transaction(inputs, outputs) + tx = self.wallet.make_unsigned_transaction(coins=inputs, outputs=outputs) except NoDynamicFeeEstimates as e: Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e))) return '' @@ -1199,7 +1195,7 @@ class ElectrumWindow(App): if not self.wallet.can_export(): return try: - key = str(self.wallet.export_private_key(addr, password)[0]) + key = str(self.wallet.export_private_key(addr, password)) pk_label.data = key except InvalidPassword: self.show_error("Invalid PIN") diff --git a/electrum/gui/kivy/uix/dialogs/__init__.py b/electrum/gui/kivy/uix/dialogs/__init__.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING, Sequence + from kivy.app import App from kivy.clock import Clock from kivy.factory import Factory @@ -8,6 +10,9 @@ from kivy.uix.boxlayout import BoxLayout from electrum.gui.kivy.i18n import _ +if TYPE_CHECKING: + from ...main_window import ElectrumWindow + from electrum.transaction import TxOutput class AnimatedPopup(Factory.Popup): @@ -202,13 +207,13 @@ class OutputList(RecycleView): def __init__(self, **kwargs): super(OutputList, self).__init__(**kwargs) - self.app = App.get_running_app() + self.app = App.get_running_app() # type: ElectrumWindow - def update(self, outputs): + def update(self, outputs: Sequence['TxOutput']): res = [] for o in outputs: value = self.app.format_amount_and_units(o.value) - res.append({'address': o.address, 'value': value}) + res.append({'address': o.get_ui_address_str(), 'value': value}) self.data = res diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -1,5 +1,6 @@ +import copy from datetime import datetime -from typing import NamedTuple, Callable +from typing import NamedTuple, Callable, TYPE_CHECKING from kivy.app import App from kivy.factory import Factory @@ -16,6 +17,10 @@ from electrum.gui.kivy.i18n import _ from electrum.util import InvalidPassword from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.wallet import CannotBumpFee +from electrum.transaction import Transaction, PartialTransaction + +if TYPE_CHECKING: + from ...main_window import ElectrumWindow Builder.load_string(''' @@ -121,11 +126,16 @@ class TxDialog(Factory.Popup): def __init__(self, app, tx): Factory.Popup.__init__(self) - self.app = app + self.app = app # type: ElectrumWindow self.wallet = self.app.wallet - self.tx = tx + self.tx = tx # type: Transaction self._action_button_fn = lambda btn: None + # if the wallet can populate the inputs with more info, do it now. + # as a result, e.g. we might learn an imported address tx is segwit, + # or that a beyond-gap-limit address is is_mine + tx.add_info_from_wallet(self.wallet) + def on_open(self): self.update() @@ -150,6 +160,7 @@ class TxDialog(Factory.Popup): self.date_label = '' self.date_str = '' + self.can_sign = self.wallet.can_sign(self.tx) if amount is None: self.amount_str = _("Transaction unrelated to your wallet") elif amount > 0: @@ -158,15 +169,18 @@ class TxDialog(Factory.Popup): else: self.is_mine = True self.amount_str = format_amount(-amount) - if fee is not None: + risk_of_burning_coins = (isinstance(self.tx, PartialTransaction) + and self.can_sign + and fee is not None + and self.tx.is_there_risk_of_burning_coins_as_fees()) + if fee is not None and not risk_of_burning_coins: self.fee_str = format_amount(fee) fee_per_kb = fee / self.tx.estimated_size() * 1000 self.feerate_str = self.app.format_fee_rate(fee_per_kb) else: self.fee_str = _('unknown') self.feerate_str = _('unknown') - self.can_sign = self.wallet.can_sign(self.tx) - self.ids.output_list.update(self.tx.get_outputs_for_UI()) + self.ids.output_list.update(self.tx.outputs()) self.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL self.update_action_button() @@ -252,10 +266,15 @@ class TxDialog(Factory.Popup): def show_qr(self): from electrum.bitcoin import base_encode, bfh - raw_tx = str(self.tx) - text = bfh(raw_tx) + original_raw_tx = str(self.tx) + tx = copy.deepcopy(self.tx) # make copy as we mutate tx + if isinstance(tx, PartialTransaction): + # this makes QR codes a lot smaller (or just possible in the first place!) + tx.convert_all_utxos_to_witness_utxos() + + text = tx.serialize_as_bytes() text = base_encode(text, base=43) - self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx) + self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=original_raw_tx) def remove_local_tx(self): txid = self.tx.txid() diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py @@ -22,11 +22,10 @@ from kivy.lang import Builder from kivy.factory import Factory from kivy.utils import platform -from electrum.bitcoin import TYPE_ADDRESS from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum import bitcoin, constants -from electrum.transaction import TxOutput, Transaction, tx_from_str +from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, TxMinedInfo, get_request_status, pr_expiration_values from electrum.plugin import run_hook @@ -276,8 +275,7 @@ class SendScreen(CScreen): return # try to decode as transaction try: - raw_tx = tx_from_str(data) - tx = Transaction(raw_tx) + tx = tx_from_any(data) tx.deserialize() except: tx = None @@ -313,7 +311,7 @@ class SendScreen(CScreen): if not bitcoin.is_address(address): self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) return - outputs = [TxOutput(TYPE_ADDRESS, address, amount)] + outputs = [PartialTxOutput.from_address_and_value(address, amount)] return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI) def do_save(self): @@ -353,11 +351,11 @@ class SendScreen(CScreen): def _do_pay_onchain(self, invoice, rbf): # make unsigned transaction - outputs = invoice['outputs'] # type: List[TxOutput] + outputs = invoice['outputs'] # type: List[PartialTxOutput] amount = sum(map(lambda x: x.value, outputs)) coins = self.app.wallet.get_spendable_coins(None) try: - tx = self.app.wallet.make_unsigned_transaction(coins, outputs, None) + tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs) except NotEnoughFunds: self.app.show_error(_("Not enough funds")) return diff --git a/electrum/gui/qt/address_dialog.py b/electrum/gui/qt/address_dialog.py @@ -84,16 +84,20 @@ class AddressDialog(WindowModalDialog): pubkey_e.setReadOnly(True) vbox.addWidget(pubkey_e) - try: - redeem_script = self.wallet.pubkeys_to_redeem_script(pubkeys) - except BaseException as e: - redeem_script = None + redeem_script = self.wallet.get_redeem_script(address) if redeem_script: vbox.addWidget(QLabel(_("Redeem Script") + ':')) redeem_e = ShowQRTextEdit(text=redeem_script) redeem_e.addCopyButton(self.app) vbox.addWidget(redeem_e) + witness_script = self.wallet.get_witness_script(address) + if witness_script: + vbox.addWidget(QLabel(_("Witness Script") + ':')) + witness_e = ShowQRTextEdit(text=witness_script) + witness_e.addCopyButton(self.app) + vbox.addWidget(witness_e) + vbox.addWidget(QLabel(_("History"))) addr_hist_model = AddressHistoryModel(self.parent, self.address) self.hw = HistoryList(self.parent, addr_hist_model) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py @@ -36,7 +36,7 @@ import base64 from functools import partial import queue import asyncio -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, Sequence, List, Union from PyQt5.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal @@ -50,7 +50,7 @@ from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget import electrum from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands, coinchooser, paymentrequest) -from electrum.bitcoin import COIN, is_address, TYPE_ADDRESS +from electrum.bitcoin import COIN, is_address from electrum.plugin import run_hook from electrum.i18n import _ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, @@ -64,7 +64,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis, InvalidBitcoinURI, InvoiceError) from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.lnutil import PaymentFailure, SENT, RECEIVED -from electrum.transaction import Transaction, TxOutput +from electrum.transaction import (Transaction, PartialTxInput, + PartialTransaction, PartialTxOutput) from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, sweep_preparations, InternalAddressCorruption) @@ -922,7 +923,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def show_transaction(self, tx, *, invoice=None, tx_desc=None): '''tx_desc is set only for txs created in the Send tab''' - show_transaction(tx, self, invoice=invoice, desc=tx_desc) + show_transaction(tx, parent=self, invoice=invoice, desc=tx_desc) def create_receive_tab(self): # A 4-column grid layout. All the stretch is in the last column. @@ -1434,11 +1435,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def update_fee(self): self.require_fee_update = True - def get_payto_or_dummy(self): - r = self.payto_e.get_recipient() + def get_payto_or_dummy(self) -> bytes: + r = self.payto_e.get_destination_scriptpubkey() if r: return r - return (TYPE_ADDRESS, self.wallet.dummy_address()) + return bfh(bitcoin.address_to_script(self.wallet.dummy_address())) def do_update_fee(self): '''Recalculate the fee. If the fee was manually input, retain it, but @@ -1461,13 +1462,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): coins = self.get_coins() if not outputs: - _type, addr = self.get_payto_or_dummy() - outputs = [TxOutput(_type, addr, amount)] + scriptpubkey = self.get_payto_or_dummy() + outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)] is_sweep = bool(self.tx_external_keypairs) make_tx = lambda fee_est: \ self.wallet.make_unsigned_transaction( - coins, outputs, - fixed_fee=fee_est, is_sweep=is_sweep) + coins=coins, + outputs=outputs, + fee=fee_est, + is_sweep=is_sweep) try: tx = make_tx(fee_estimator) self.not_enough_funds = False @@ -1546,7 +1549,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): menu.addAction(_("Remove"), lambda: self.from_list_delete(item)) menu.exec_(self.from_list.viewport().mapToGlobal(position)) - def set_pay_from(self, coins): + def set_pay_from(self, coins: Sequence[PartialTxInput]): self.pay_from = list(coins) self.redraw_from_list() @@ -1555,12 +1558,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.from_label.setHidden(len(self.pay_from) == 0) self.from_list.setHidden(len(self.pay_from) == 0) - def format(x): - h = x.get('prevout_hash') - return h[0:10] + '...' + h[-10:] + ":%d"%x.get('prevout_n') + '\t' + "%s"%x.get('address') + '\t' + def format(txin: PartialTxInput): + h = txin.prevout.txid.hex() + out_idx = txin.prevout.out_idx + addr = txin.address + return h[0:10] + '...' + h[-10:] + ":%d"%out_idx + '\t' + addr + '\t' for coin in self.pay_from: - item = QTreeWidgetItem([format(coin), self.format_amount(coin['value'])]) + item = QTreeWidgetItem([format(coin), self.format_amount(coin.value_sats())]) item.setFont(0, QFont(MONOSPACE_FONT)) self.from_list.addTopLevelItem(item) @@ -1620,14 +1625,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): fee_estimator = None return fee_estimator - def read_outputs(self): + def read_outputs(self) -> List[PartialTxOutput]: if self.payment_request: outputs = self.payment_request.get_outputs() else: outputs = self.payto_e.get_outputs(self.max_button.isChecked()) return outputs - def check_send_tab_onchain_outputs_and_show_errors(self, outputs) -> bool: + def check_send_tab_onchain_outputs_and_show_errors(self, outputs: List[PartialTxOutput]) -> bool: """Returns whether there are errors with outputs. Also shows error dialog to user if so. """ @@ -1636,12 +1641,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return True for o in outputs: - if o.address is None: + if o.scriptpubkey is None: self.show_error(_('Bitcoin Address is None')) return True - if o.type == TYPE_ADDRESS and not bitcoin.is_address(o.address): - self.show_error(_('Invalid Bitcoin Address')) - return True if o.value is None: self.show_error(_('Invalid Amount')) return True @@ -1749,20 +1751,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): return elif invoice['type'] == PR_TYPE_ONCHAIN: message = invoice['message'] - outputs = invoice['outputs'] + outputs = invoice['outputs'] # type: List[PartialTxOutput] else: raise Exception('unknown invoice type') if run_hook('abort_send', self): return - outputs = [TxOutput(*x) for x in outputs] + for txout in outputs: + assert isinstance(txout, PartialTxOutput) fee_estimator = self.get_send_fee_estimator() coins = self.get_coins() try: is_sweep = bool(self.tx_external_keypairs) tx = self.wallet.make_unsigned_transaction( - coins, outputs, fixed_fee=fee_estimator, + coins=coins, + outputs=outputs, + fee=fee_estimator, is_sweep=is_sweep) except (NotEnoughFunds, NoDynamicFeeEstimates) as e: self.show_message(str(e)) @@ -1837,7 +1842,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): def sign_tx(self, tx, callback, password): self.sign_tx_with_password(tx, callback, password) - def sign_tx_with_password(self, tx, callback, password): + def sign_tx_with_password(self, tx: PartialTransaction, callback, password): '''Sign the transaction in a separate thread. When done, calls the callback with a success code of True or False. ''' @@ -1849,13 +1854,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success if self.tx_external_keypairs: # can sign directly - task = partial(Transaction.sign, tx, self.tx_external_keypairs) + task = partial(tx.sign, self.tx_external_keypairs) else: task = partial(self.wallet.sign_transaction, tx, password) msg = _('Signing transaction...') WaitingDialog(self, msg, task, on_success, on_failure) - def broadcast_transaction(self, tx, *, invoice=None, tx_desc=None): + def broadcast_transaction(self, tx: Transaction, *, invoice=None, tx_desc=None): def broadcast_thread(): # non-GUI thread @@ -1879,7 +1884,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if pr: self.payment_request = None refund_address = self.wallet.get_receiving_address() - coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address) + coro = pr.send_payment_and_receive_paymentack(tx.serialize(), refund_address) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) ack_status, ack_msg = fut.result(timeout=20) self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") @@ -2077,7 +2082,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.utxo_list.update() self.update_fee() - def set_frozen_state_of_coins(self, utxos, freeze: bool): + def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool): self.wallet.set_frozen_state_of_coins(utxos, freeze) self.utxo_list.update() self.update_fee() @@ -2124,7 +2129,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): else: return self.wallet.get_spendable_coins(None) - def spend_coins(self, coins): + def spend_coins(self, coins: Sequence[PartialTxInput]): self.set_pay_from(coins) self.set_onchain(len(coins) > 0) self.show_send_tab() @@ -2527,7 +2532,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if not address: return try: - pk, redeem_script = self.wallet.export_private_key(address, password) + pk = self.wallet.export_private_key(address, password) except Exception as e: self.logger.exception('') self.show_message(repr(e)) @@ -2542,11 +2547,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): keys_e = ShowQRTextEdit(text=pk) keys_e.addCopyButton(self.app) vbox.addWidget(keys_e) - if redeem_script: - vbox.addWidget(QLabel(_("Redeem Script") + ':')) - rds_e = ShowQRTextEdit(text=redeem_script) - rds_e.addCopyButton(self.app) - vbox.addWidget(rds_e) + # if redeem_script: + # vbox.addWidget(QLabel(_("Redeem Script") + ':')) + # rds_e = ShowQRTextEdit(text=redeem_script) + # rds_e.addCopyButton(self.app) + # vbox.addWidget(rds_e) vbox.addLayout(Buttons(CloseButton(d))) d.setLayout(vbox) d.exec_() @@ -2718,11 +2723,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): d = PasswordDialog(parent, msg) return d.run() - def tx_from_text(self, txt) -> Optional[Transaction]: - from electrum.transaction import tx_from_str + def tx_from_text(self, data: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']: + from electrum.transaction import tx_from_any try: - tx = tx_from_str(txt) - return Transaction(tx) + return tx_from_any(data) except BaseException as e: self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e)) return @@ -2741,25 +2745,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.pay_to_URI(data) return # else if the user scanned an offline signed tx - try: - data = bh2u(bitcoin.base_decode(data, length=None, base=43)) - except BaseException as e: - self.show_error((_('Could not decode QR code')+':\n{}').format(repr(e))) - return tx = self.tx_from_text(data) if not tx: return self.show_transaction(tx) def read_tx_from_file(self) -> Optional[Transaction]: - fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn") + fileName = self.getOpenFileName(_("Select your transaction file"), "*.txn;;*.psbt") if not fileName: return try: with open(fileName, "r") as f: - file_content = f.read() + file_content = f.read() # type: Union[str, bytes] except (ValueError, IOError, os.error) as reason: - self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), title=_("Unable to read file or no transaction found")) + self.show_critical(_("Electrum was unable to open your transaction file") + "\n" + str(reason), + title=_("Unable to read file or no transaction found")) return return self.tx_from_text(file_content) @@ -2831,7 +2831,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): time.sleep(0.1) if done or cancelled: break - privkey = self.wallet.export_private_key(addr, password)[0] + privkey = self.wallet.export_private_key(addr, password) private_keys[addr] = privkey self.computing_privkeys_signal.emit() if not cancelled: @@ -3130,7 +3130,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): vbox.addLayout(Buttons(CloseButton(d))) d.exec_() - def cpfp(self, parent_tx: Transaction, new_tx: Transaction) -> None: + def cpfp(self, parent_tx: Transaction, new_tx: PartialTransaction) -> None: total_size = parent_tx.estimated_size() + new_tx.estimated_size() parent_txid = parent_tx.txid() assert parent_txid @@ -3257,7 +3257,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): new_tx.set_rbf(False) self.show_transaction(new_tx, tx_desc=tx_label) - def save_transaction_into_wallet(self, tx): + def save_transaction_into_wallet(self, tx: Transaction): win = self.top_level_window() try: if not self.wallet.add_transaction(tx.txid(), tx): diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py @@ -25,13 +25,13 @@ import re from decimal import Decimal -from typing import NamedTuple, Sequence +from typing import NamedTuple, Sequence, Optional, List from PyQt5.QtGui import QFontMetrics from electrum import bitcoin from electrum.util import bfh -from electrum.transaction import TxOutput, push_script +from electrum.transaction import push_script, PartialTxOutput from electrum.bitcoin import opcodes from electrum.logging import Logger from electrum.lnaddr import LnDecodeException @@ -65,12 +65,12 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.heightMax = 150 self.c = None self.textChanged.connect(self.check_text) - self.outputs = [] + self.outputs = [] # type: List[PartialTxOutput] self.errors = [] # type: Sequence[PayToLineError] self.is_pr = False self.is_alias = False self.update_size() - self.payto_address = None + self.payto_scriptpubkey = None # type: Optional[bytes] self.lightning_invoice = None self.previous_payto = '' @@ -86,19 +86,19 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): def setExpired(self): self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) - def parse_address_and_amount(self, line): + def parse_address_and_amount(self, line) -> PartialTxOutput: x, y = line.split(',') - out_type, out = self.parse_output(x) + scriptpubkey = self.parse_output(x) amount = self.parse_amount(y) - return TxOutput(out_type, out, amount) + return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount) - def parse_output(self, x): + def parse_output(self, x) -> bytes: try: address = self.parse_address(x) - return bitcoin.TYPE_ADDRESS, address + return bfh(bitcoin.address_to_script(address)) except: script = self.parse_script(x) - return bitcoin.TYPE_SCRIPT, script + return bfh(script) def parse_script(self, x): script = '' @@ -131,9 +131,9 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): return # filter out empty lines lines = [i for i in self.lines() if i] - outputs = [] + outputs = [] # type: List[PartialTxOutput] total = 0 - self.payto_address = None + self.payto_scriptpubkey = None self.lightning_invoice = None if len(lines) == 1: data = lines[0] @@ -152,10 +152,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.lightning_invoice = lower return try: - self.payto_address = self.parse_output(data) + self.payto_scriptpubkey = self.parse_output(data) except: pass - if self.payto_address: + if self.payto_scriptpubkey: self.win.set_onchain(True) self.win.lock_amount(False) return @@ -177,7 +177,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): self.win.max_button.setChecked(is_max) self.outputs = outputs - self.payto_address = None + self.payto_scriptpubkey = None if self.win.max_button.isChecked(): self.win.do_update_fee() @@ -188,18 +188,16 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger): def get_errors(self) -> Sequence[PayToLineError]: return self.errors - def get_recipient(self): - return self.payto_address + def get_destination_scriptpubkey(self) -> Optional[bytes]: + return self.payto_scriptpubkey def get_outputs(self, is_max): - if self.payto_address: + if self.payto_scriptpubkey: if is_max: amount = '!' else: amount = self.amount_edit.get_amount() - - _type, addr = self.payto_address - self.outputs = [TxOutput(_type, addr, amount)] + self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)] return self.outputs[:] diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py @@ -26,12 +26,12 @@ import sys import copy import datetime -import json import traceback -from typing import TYPE_CHECKING +import time +from typing import TYPE_CHECKING, Callable from PyQt5.QtCore import QSize, Qt -from PyQt5.QtGui import QTextCharFormat, QBrush, QFont +from PyQt5.QtGui import QTextCharFormat, QBrush, QFont, QPixmap from PyQt5.QtWidgets import (QDialog, QLabel, QPushButton, QHBoxLayout, QVBoxLayout, QTextEdit, QFrame, QAction, QToolButton, QMenu) import qrcode @@ -42,11 +42,12 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config from electrum.util import bfh -from electrum.transaction import SerializationError, Transaction +from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput from electrum.logging import get_logger -from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton, - MONOSPACE_FONT, ColorScheme, ButtonsLineEdit) +from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton, icon_path, + MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog, + char_width_in_lineedit) if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -60,9 +61,9 @@ _logger = get_logger(__name__) dialogs = [] # Otherwise python randomly garbage collects the dialogs... -def show_transaction(tx, parent, *, invoice=None, desc=None, prompt_if_unsaved=False): +def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', invoice=None, desc=None, prompt_if_unsaved=False): try: - d = TxDialog(tx, parent, invoice, desc, prompt_if_unsaved) + d = TxDialog(tx, parent=parent, invoice=invoice, desc=desc, prompt_if_unsaved=prompt_if_unsaved) except SerializationError as e: _logger.exception('unable to deserialize the transaction') parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) @@ -73,7 +74,7 @@ def show_transaction(tx, parent, *, invoice=None, desc=None, prompt_if_unsaved=F class TxDialog(QDialog, MessageBoxMixin): - def __init__(self, tx: Transaction, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved): + def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', invoice, desc, prompt_if_unsaved): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' @@ -96,8 +97,8 @@ class TxDialog(QDialog, MessageBoxMixin): # if the wallet can populate the inputs with more info, do it now. # as a result, e.g. we might learn an imported address tx is segwit, - # in which case it's ok to display txid - tx.add_inputs_info(self.wallet) + # or that a beyond-gap-limit address is is_mine + tx.add_info_from_wallet(self.wallet) self.setMinimumWidth(950) self.setWindowTitle(_("Transaction")) @@ -115,7 +116,15 @@ class TxDialog(QDialog, MessageBoxMixin): self.add_tx_stats(vbox) vbox.addSpacing(10) - self.add_io(vbox) + + self.inputs_header = QLabel() + vbox.addWidget(self.inputs_header) + self.inputs_textedit = QTextEditWithDefaultSize() + vbox.addWidget(self.inputs_textedit) + self.outputs_header = QLabel() + vbox.addWidget(self.outputs_header) + self.outputs_textedit = QTextEditWithDefaultSize() + vbox.addWidget(self.outputs_textedit) self.sign_button = b = QPushButton(_("Sign")) b.clicked.connect(self.sign) @@ -136,23 +145,35 @@ class TxDialog(QDialog, MessageBoxMixin): b.clicked.connect(self.close) b.setDefault(True) - export_actions_menu = QMenu() - action = QAction(_("Copy to clipboard"), self) - action.triggered.connect(lambda: parent.app.clipboard().setText((lambda: str(self.tx))())) - export_actions_menu.addAction(action) - action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self) - action.triggered.connect(self.show_qr) - export_actions_menu.addAction(action) - action = QAction(_("Export to file"), self) - action.triggered.connect(self.export) - export_actions_menu.addAction(action) + self.export_actions_menu = export_actions_menu = QMenu() + self.add_export_actions_to_menu(export_actions_menu) + export_actions_menu.addSeparator() + if isinstance(tx, PartialTransaction): + export_for_coinjoin_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates")) + self.add_export_actions_to_menu(export_for_coinjoin_submenu, gettx=self._gettx_for_coinjoin) + self.export_actions_button = QToolButton() self.export_actions_button.setText(_("Export")) self.export_actions_button.setMenu(export_actions_menu) self.export_actions_button.setPopupMode(QToolButton.InstantPopup) + partial_tx_actions_menu = QMenu() + ptx_merge_sigs_action = QAction(_("Merge signatures from"), self) + ptx_merge_sigs_action.triggered.connect(self.merge_sigs) + partial_tx_actions_menu.addAction(ptx_merge_sigs_action) + ptx_join_txs_action = QAction(_("Join inputs/outputs"), self) + ptx_join_txs_action.triggered.connect(self.join_tx_with_another) + partial_tx_actions_menu.addAction(ptx_join_txs_action) + self.partial_tx_actions_button = QToolButton() + self.partial_tx_actions_button.setText(_("Combine")) + self.partial_tx_actions_button.setMenu(partial_tx_actions_menu) + self.partial_tx_actions_button.setPopupMode(QToolButton.InstantPopup) + # Action buttons - self.buttons = [self.sign_button, self.broadcast_button, self.cancel_button] + self.buttons = [] + if isinstance(tx, PartialTransaction): + self.buttons.append(self.partial_tx_actions_button) + self.buttons += [self.sign_button, self.broadcast_button, self.cancel_button] # Transaction sharing buttons self.sharing_buttons = [self.export_actions_button, self.save_button] @@ -189,8 +210,43 @@ class TxDialog(QDialog, MessageBoxMixin): # Override escape-key to close normally (and invoke closeEvent) self.close() - def show_qr(self): - text = bfh(str(self.tx)) + def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None: + if gettx is None: + gettx = lambda: None + + action = QAction(_("Copy to clipboard"), self) + action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx())) + menu.addAction(action) + + qr_icon = "qrcode_white.png" if ColorScheme.dark_scheme else "qrcode.png" + action = QAction(read_QIcon(qr_icon), _("Show as QR code"), self) + action.triggered.connect(lambda: self.show_qr(tx=gettx())) + menu.addAction(action) + + action = QAction(_("Export to file"), self) + action.triggered.connect(lambda: self.export_to_file(tx=gettx())) + menu.addAction(action) + + def _gettx_for_coinjoin(self) -> PartialTransaction: + if not isinstance(self.tx, PartialTransaction): + raise Exception("Can only export partial transactions for coinjoins.") + tx = copy.deepcopy(self.tx) + tx.prepare_for_export_for_coinjoin() + return tx + + def copy_to_clipboard(self, *, tx: Transaction = None): + if tx is None: + tx = self.tx + self.main_window.app.clipboard().setText(str(tx)) + + def show_qr(self, *, tx: Transaction = None): + if tx is None: + tx = self.tx + tx = copy.deepcopy(tx) # make copy as we mutate tx + if isinstance(tx, PartialTransaction): + # this makes QR codes a lot smaller (or just possible in the first place!) + tx.convert_all_utxos_to_witness_utxos() + text = tx.serialize_as_bytes() text = base_encode(text, base=43) try: self.main_window.show_qrcode(text, 'Transaction', parent=self) @@ -222,17 +278,68 @@ class TxDialog(QDialog, MessageBoxMixin): self.saved = True self.main_window.pop_top_level_window(self) - - def export(self): - name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn' - fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn") - if fileName: + def export_to_file(self, *, tx: Transaction = None): + if tx is None: + tx = self.tx + if isinstance(tx, PartialTransaction): + tx.finalize_psbt() + if tx.is_complete(): + name = 'signed_%s.txn' % (tx.txid()[0:8]) + else: + name = self.wallet.basename() + time.strftime('-%Y%m%d-%H%M.psbt') + fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn;;*.psbt") + if not fileName: + return + if tx.is_complete(): # network tx hex with open(fileName, "w+") as f: - f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n') - self.show_message(_("Transaction exported successfully")) - self.saved = True + network_tx_hex = tx.serialize_to_network() + f.write(network_tx_hex + '\n') + else: # if partial: PSBT bytes + assert isinstance(tx, PartialTransaction) + with open(fileName, "wb+") as f: + f.write(tx.serialize_as_bytes()) + + self.show_message(_("Transaction exported successfully")) + self.saved = True + + def merge_sigs(self): + if not isinstance(self.tx, PartialTransaction): + return + text = text_dialog(self, _('Input raw transaction'), + _("Transaction to merge signatures from") + ":", + _("Load transaction")) + if not text: + return + tx = self.main_window.tx_from_text(text) + if not tx: + return + try: + self.tx.combine_with_other_psbt(tx) + except Exception as e: + self.show_error(_("Error combining partial transactions") + ":\n" + repr(e)) + return + self.update() + + def join_tx_with_another(self): + if not isinstance(self.tx, PartialTransaction): + return + text = text_dialog(self, _('Input raw transaction'), + _("Transaction to join with") + " (" + _("add inputs and outputs") + "):", + _("Load transaction")) + if not text: + return + tx = self.main_window.tx_from_text(text) + if not tx: + return + try: + self.tx.join_with_other_psbt(tx) + except Exception as e: + self.show_error(_("Error joining partial transactions") + ":\n" + repr(e)) + return + self.update() def update(self): + self.update_io() desc = self.desc base_unit = self.main_window.base_unit() format_amount = self.main_window.format_amount @@ -287,13 +394,17 @@ class TxDialog(QDialog, MessageBoxMixin): feerate_warning = simple_config.FEERATE_WARNING_HIGH_FEE if fee_rate > feerate_warning: fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!' + if isinstance(self.tx, PartialTransaction): + risk_of_burning_coins = (can_sign and fee is not None + and self.tx.is_there_risk_of_burning_coins_as_fees()) + self.fee_warning_icon.setVisible(risk_of_burning_coins) self.amount_label.setText(amount_str) self.fee_label.setText(fee_str) self.size_label.setText(size_str) run_hook('transaction_dialog_update', self) - def add_io(self, vbox): - vbox.addWidget(QLabel(_("Inputs") + ' (%d)'%len(self.tx.inputs()))) + def update_io(self): + self.inputs_header.setText(_("Inputs") + ' (%d)'%len(self.tx.inputs())) ext = QTextCharFormat() rec = QTextCharFormat() rec.setBackground(QBrush(ColorScheme.GREEN.as_color(background=True))) @@ -315,39 +426,39 @@ class TxDialog(QDialog, MessageBoxMixin): def format_amount(amt): return self.main_window.format_amount(amt, whitespaces=True) - i_text = QTextEditWithDefaultSize() + i_text = self.inputs_textedit + i_text.clear() i_text.setFont(QFont(MONOSPACE_FONT)) i_text.setReadOnly(True) cursor = i_text.textCursor() - for x in self.tx.inputs(): - if x['type'] == 'coinbase': + for txin in self.tx.inputs(): + if txin.is_coinbase(): cursor.insertText('coinbase') else: - prevout_hash = x.get('prevout_hash') - prevout_n = x.get('prevout_n') + prevout_hash = txin.prevout.txid.hex() + prevout_n = txin.prevout.out_idx cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext) - addr = self.wallet.get_txin_address(x) + addr = self.wallet.get_txin_address(txin) if addr is None: addr = '' cursor.insertText(addr, text_format(addr)) - if x.get('value'): - cursor.insertText(format_amount(x['value']), ext) + if isinstance(txin, PartialTxInput) and txin.value_sats() is not None: + cursor.insertText(format_amount(txin.value_sats()), ext) cursor.insertBlock() - vbox.addWidget(i_text) - vbox.addWidget(QLabel(_("Outputs") + ' (%d)'%len(self.tx.outputs()))) - o_text = QTextEditWithDefaultSize() + self.outputs_header.setText(_("Outputs") + ' (%d)'%len(self.tx.outputs())) + o_text = self.outputs_textedit + o_text.clear() o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setReadOnly(True) cursor = o_text.textCursor() - for o in self.tx.get_outputs_for_UI(): - addr, v = o.address, o.value + for o in self.tx.outputs(): + addr, v = o.get_ui_address_str(), o.value cursor.insertText(addr, text_format(addr)) if v is not None: cursor.insertText('\t', ext) cursor.insertText(format_amount(v), ext) cursor.insertBlock() - vbox.addWidget(o_text) def add_tx_stats(self, vbox): hbox_stats = QHBoxLayout() @@ -362,8 +473,24 @@ class TxDialog(QDialog, MessageBoxMixin): vbox_left.addWidget(self.date_label) self.amount_label = TxDetailLabel() vbox_left.addWidget(self.amount_label) + + fee_hbox = QHBoxLayout() self.fee_label = TxDetailLabel() - vbox_left.addWidget(self.fee_label) + fee_hbox.addWidget(self.fee_label) + self.fee_warning_icon = QLabel() + pixmap = QPixmap(icon_path("warning")) + pixmap_size = round(2 * char_width_in_lineedit()) + pixmap = pixmap.scaled(pixmap_size, pixmap_size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.fee_warning_icon.setPixmap(pixmap) + self.fee_warning_icon.setToolTip(_("Warning") + ": " + + _("The fee could not be verified. Signing non-segwit inputs is risky:\n" + "if this transaction was maliciously modified before you sign,\n" + "you might end up paying a higher mining fee than displayed.")) + self.fee_warning_icon.setVisible(False) + fee_hbox.addWidget(self.fee_warning_icon) + fee_hbox.addStretch(1) + vbox_left.addLayout(fee_hbox) + vbox_left.addStretch(1) hbox_stats.addLayout(vbox_left, 50) diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py @@ -840,13 +840,16 @@ def export_meta_gui(electrum_window, title, exporter): def get_parent_main_window(widget): """Returns a reference to the ElectrumWindow this widget belongs to.""" from .main_window import ElectrumWindow + from .transaction_dialog import TxDialog for _ in range(100): if widget is None: return None - if not isinstance(widget, ElectrumWindow): - widget = widget.parentWidget() - else: + if isinstance(widget, ElectrumWindow): return widget + elif isinstance(widget, TxDialog): + return widget.main_window + else: + widget = widget.parentWidget() return None diff --git a/electrum/gui/qt/utxo_list.py b/electrum/gui/qt/utxo_list.py @@ -23,7 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from typing import Optional, List +from typing import Optional, List, Dict from enum import IntEnum from PyQt5.QtCore import Qt @@ -31,9 +31,11 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont from PyQt5.QtWidgets import QAbstractItemView, QMenu from electrum.i18n import _ +from electrum.transaction import PartialTxInput from .util import MyTreeView, ColorScheme, MONOSPACE_FONT + class UTXOList(MyTreeView): class Columns(IntEnum): @@ -64,21 +66,21 @@ class UTXOList(MyTreeView): def update(self): self.wallet = self.parent.wallet utxos = self.wallet.get_utxos() - self.utxo_dict = {} + self.utxo_dict = {} # type: Dict[str, PartialTxInput] self.model().clear() self.update_headers(self.__class__.headers) - for idx, x in enumerate(utxos): - self.insert_utxo(idx, x) + for idx, utxo in enumerate(utxos): + self.insert_utxo(idx, utxo) self.filter() - def insert_utxo(self, idx, x): - address = x['address'] - height = x.get('height') - name = x.get('prevout_hash') + ":%d"%x.get('prevout_n') - name_short = x.get('prevout_hash')[:16] + '...' + ":%d"%x.get('prevout_n') - self.utxo_dict[name] = x - label = self.wallet.get_label(x.get('prevout_hash')) - amount = self.parent.format_amount(x['value'], whitespaces=True) + def insert_utxo(self, idx, utxo: PartialTxInput): + address = utxo.address + height = utxo.block_height + name = utxo.prevout.to_str() + name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx + self.utxo_dict[name] = utxo + label = self.wallet.get_label(utxo.prevout.txid.hex()) + amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True) labels = [name_short, address, label, amount, '%d'%height] utxo_item = [QStandardItem(x) for x in labels] self.set_editability(utxo_item) @@ -89,7 +91,7 @@ class UTXOList(MyTreeView): if self.wallet.is_frozen_address(address): utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True)) utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen')) - if self.wallet.is_frozen_coin(x): + if self.wallet.is_frozen_coin(utxo): utxo_item[self.Columns.OUTPOINT].setBackground(ColorScheme.BLUE.as_color(True)) utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}") else: @@ -114,26 +116,26 @@ class UTXOList(MyTreeView): menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins)) assert len(coins) >= 1, len(coins) if len(coins) == 1: - utxo_dict = coins[0] - addr = utxo_dict['address'] - txid = utxo_dict['prevout_hash'] + utxo = coins[0] + addr = utxo.address + txid = utxo.prevout.txid.hex() # "Details" tx = self.wallet.db.get_transaction(txid) if tx: label = self.wallet.get_label(txid) or None # Prefer None if empty (None hides the Description: field in the window) - menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, label)) + menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx, tx_desc=label)) # "Copy ..." idx = self.indexAt(position) if not idx.isValid(): return self.add_copy_menu(menu, idx) # "Freeze coin" - if not self.wallet.is_frozen_coin(utxo_dict): - menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], True)) + if not self.wallet.is_frozen_coin(utxo): + menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True)) else: menu.addSeparator() menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False) - menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], False)) + menu.addAction(_("Unfreeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], False)) menu.addSeparator() # "Freeze address" if not self.wallet.is_frozen_address(addr): @@ -146,9 +148,9 @@ class UTXOList(MyTreeView): else: # multiple items selected menu.addSeparator() - addrs = [utxo_dict['address'] for utxo_dict in coins] - is_coin_frozen = [self.wallet.is_frozen_coin(utxo_dict) for utxo_dict in coins] - is_addr_frozen = [self.wallet.is_frozen_address(utxo_dict['address']) for utxo_dict in coins] + addrs = [utxo.address for utxo in coins] + is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins] + is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins] if not all(is_coin_frozen): menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True)) if any(is_coin_frozen): diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py @@ -5,8 +5,8 @@ import logging from electrum import WalletStorage, Wallet from electrum.util import format_satoshis -from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS -from electrum.transaction import TxOutput +from electrum.bitcoin import is_address, COIN +from electrum.transaction import PartialTxOutput from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.logging import console_stderr_handler @@ -197,8 +197,9 @@ class ElectrumGui: if c == "n": return try: - tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], - password, self.config, fee) + tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)], + password=password, + fee=fee) except Exception as e: print(repr(e)) return diff --git a/electrum/gui/text.py b/electrum/gui/text.py @@ -9,8 +9,8 @@ import logging import electrum from electrum.util import format_satoshis -from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS -from electrum.transaction import TxOutput +from electrum.bitcoin import is_address, COIN +from electrum.transaction import PartialTxOutput from electrum.wallet import Wallet from electrum.storage import WalletStorage from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed @@ -360,8 +360,9 @@ class ElectrumGui: else: password = None try: - tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], - password, self.config, fee) + tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)], + password=password, + fee=fee) except Exception as e: self.show_message(repr(e)) return diff --git a/electrum/json_db.py b/electrum/json_db.py @@ -28,7 +28,7 @@ import json import copy import threading from collections import defaultdict -from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple +from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence from . import util, bitcoin from .util import profiler, WalletFileException, multisig_type, TxMinedInfo @@ -40,15 +40,11 @@ from .logging import Logger OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 19 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 20 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format -class JsonDBJsonEncoder(util.MyEncoder): - def default(self, obj): - if isinstance(obj, Transaction): - return str(obj) - return super().default(obj) +JsonDBJsonEncoder = util.MyEncoder class TxFeesValue(NamedTuple): @@ -217,6 +213,7 @@ class JsonDB(Logger): self._convert_version_17() self._convert_version_18() self._convert_version_19() + self._convert_version_20() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() @@ -425,10 +422,10 @@ class JsonDB(Logger): for txid, raw_tx in transactions.items(): tx = Transaction(raw_tx) for txin in tx.inputs(): - if txin['type'] == 'coinbase': + if txin.is_coinbase(): continue - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] + prevout_hash = txin.prevout.txid.hex() + prevout_n = txin.prevout.out_idx spent_outpoints[prevout_hash][str(prevout_n)] = txid self.put('spent_outpoints', spent_outpoints) @@ -448,10 +445,45 @@ class JsonDB(Logger): self.put('tx_fees', None) self.put('seed_version', 19) - # def _convert_version_20(self): - # TODO for "next" upgrade: - # - move "pw_hash_version" from keystore to storage - # pass + def _convert_version_20(self): + # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores. + # store explicit None values if we cannot retroactively determine them + if not self._is_upgrade_method_needed(19, 19): + return + + from .bip32 import BIP32Node, convert_bip32_intpath_to_strpath + # note: This upgrade method reimplements bip32.root_fp_and_der_prefix_from_xkey. + # This is done deliberately, to avoid introducing that method as a dependency to this upgrade. + for ks_name in ('keystore', *['x{}/'.format(i) for i in range(1, 16)]): + ks = self.get(ks_name, None) + if ks is None: continue + xpub = ks.get('xpub', None) + if xpub is None: continue + bip32node = BIP32Node.from_xkey(xpub) + # derivation prefix + derivation_prefix = ks.get('derivation', None) + if derivation_prefix is None: + assert bip32node.depth >= 0, bip32node.depth + if bip32node.depth == 0: + derivation_prefix = 'm' + elif bip32node.depth == 1: + child_number_int = int.from_bytes(bip32node.child_number, 'big') + derivation_prefix = convert_bip32_intpath_to_strpath([child_number_int]) + ks['derivation'] = derivation_prefix + # root fingerprint + root_fingerprint = ks.get('ckcc_xfp', None) + if root_fingerprint is not None: + root_fingerprint = root_fingerprint.to_bytes(4, byteorder="little", signed=False).hex().lower() + if root_fingerprint is None: + if bip32node.depth == 0: + root_fingerprint = bip32node.calc_fingerprint_of_this_node().hex().lower() + elif bip32node.depth == 1: + root_fingerprint = bip32node.fingerprint.hex() + ks['root_fingerprint'] = root_fingerprint + ks.pop('ckcc_xfp', None) + self.put(ks_name, ks) + + self.put('seed_version', 20) def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): @@ -758,16 +790,16 @@ class JsonDB(Logger): @modifier def add_change_address(self, addr): - self._addr_to_addr_index[addr] = (True, len(self.change_addresses)) + self._addr_to_addr_index[addr] = (1, len(self.change_addresses)) self.change_addresses.append(addr) @modifier def add_receiving_address(self, addr): - self._addr_to_addr_index[addr] = (False, len(self.receiving_addresses)) + self._addr_to_addr_index[addr] = (0, len(self.receiving_addresses)) self.receiving_addresses.append(addr) @locked - def get_address_index(self, address): + def get_address_index(self, address) -> Optional[Sequence[int]]: return self._addr_to_addr_index.get(address) @modifier @@ -801,11 +833,11 @@ class JsonDB(Logger): self.data['addresses'][name] = [] self.change_addresses = self.data['addresses']['change'] self.receiving_addresses = self.data['addresses']['receiving'] - self._addr_to_addr_index = {} # key: address, value: (is_change, index) + self._addr_to_addr_index = {} # type: Dict[str, Sequence[int]] # key: address, value: (is_change, index) for i, addr in enumerate(self.receiving_addresses): - self._addr_to_addr_index[addr] = (False, i) + self._addr_to_addr_index[addr] = (0, i) for i, addr in enumerate(self.change_addresses): - self._addr_to_addr_index[addr] = (True, i) + self._addr_to_addr_index[addr] = (1, i) @profiler def _load_transactions(self): diff --git a/electrum/keystore.py b/electrum/keystore.py @@ -26,16 +26,17 @@ from unicodedata import normalize import hashlib -from typing import Tuple, TYPE_CHECKING, Union, Sequence +import re +from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple from . import bitcoin, ecc, constants, bip32 -from .bitcoin import (deserialize_privkey, serialize_privkey, - public_key_to_p2pkh) +from .bitcoin import deserialize_privkey, serialize_privkey from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, - is_xpub, is_xprv, BIP32Node) + is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, + convert_bip32_intpath_to_strpath) from .ecc import string_to_number, number_to_string from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, - SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion) + SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160) from .util import (InvalidPassword, WalletFileException, BitcoinException, bh2u, bfh, inv_dict) from .mnemonic import Mnemonic, load_wordlist, seed_type, is_seed @@ -43,13 +44,14 @@ from .plugin import run_hook from .logging import Logger if TYPE_CHECKING: - from .transaction import Transaction + from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput class KeyStore(Logger): def __init__(self): Logger.__init__(self) + self.is_requesting_to_be_rewritten_to_wallet_file = False # type: bool def has_seed(self): return False @@ -67,25 +69,19 @@ class KeyStore(Logger): """Returns whether the keystore can be encrypted with a password.""" raise NotImplementedError() - def get_tx_derivations(self, tx): + def get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[str, Union[Sequence[int], str]]: keypairs = {} for txin in tx.inputs(): - num_sig = txin.get('num_sig') - if num_sig is None: + if txin.is_complete(): continue - x_signatures = txin['signatures'] - signatures = [sig for sig in x_signatures if sig] - if len(signatures) == num_sig: - # input is complete - continue - for k, x_pubkey in enumerate(txin['x_pubkeys']): - if x_signatures[k] is not None: + for pubkey in txin.pubkeys: + if pubkey in txin.part_sigs: # this pubkey already signed continue - derivation = self.get_pubkey_derivation(x_pubkey) + derivation = self.get_pubkey_derivation(pubkey, txin) if not derivation: continue - keypairs[x_pubkey] = derivation + keypairs[pubkey.hex()] = derivation return keypairs def can_sign(self, tx): @@ -108,9 +104,64 @@ class KeyStore(Logger): def decrypt_message(self, sequence, message, password) -> bytes: raise NotImplementedError() # implemented by subclasses - def sign_transaction(self, tx: 'Transaction', password) -> None: + def sign_transaction(self, tx: 'PartialTransaction', password) -> None: raise NotImplementedError() # implemented by subclasses + def get_pubkey_derivation(self, pubkey: bytes, + txinout: Union['PartialTxInput', 'PartialTxOutput'], + *, only_der_suffix=True) \ + -> Union[Sequence[int], str, None]: + """Returns either a derivation int-list if the pubkey can be HD derived from this keystore, + the pubkey itself (hex) if the pubkey belongs to the keystore but not HD derived, + or None if the pubkey is unrelated. + """ + def test_der_suffix_against_pubkey(der_suffix: Sequence[int], pubkey: bytes) -> bool: + if len(der_suffix) != 2: + return False + if pubkey.hex() != self.derive_pubkey(*der_suffix): + return False + return True + + if hasattr(self, 'get_root_fingerprint'): + if pubkey not in txinout.bip32_paths: + return None + fp_found, path_found = txinout.bip32_paths[pubkey] + der_suffix = None + full_path = None + # try fp against our root + my_root_fingerprint_hex = self.get_root_fingerprint() + my_der_prefix_str = self.get_derivation_prefix() + ks_der_prefix = convert_bip32_path_to_list_of_uint32(my_der_prefix_str) if my_der_prefix_str else None + if (my_root_fingerprint_hex is not None and ks_der_prefix is not None and + fp_found.hex() == my_root_fingerprint_hex): + if path_found[:len(ks_der_prefix)] == ks_der_prefix: + der_suffix = path_found[len(ks_der_prefix):] + if not test_der_suffix_against_pubkey(der_suffix, pubkey): + der_suffix = None + # try fp against our intermediate fingerprint + if (der_suffix is None and hasattr(self, 'xpub') and + fp_found == BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node()): + der_suffix = path_found + if not test_der_suffix_against_pubkey(der_suffix, pubkey): + der_suffix = None + if der_suffix is None: + return None + if ks_der_prefix is not None: + full_path = ks_der_prefix + list(der_suffix) + return der_suffix if only_der_suffix else full_path + return None + + def find_my_pubkey_in_txinout( + self, txinout: Union['PartialTxInput', 'PartialTxOutput'], + *, only_der_suffix: bool = False + ) -> Tuple[Optional[bytes], Optional[List[int]]]: + # note: we assume that this cosigner only has one pubkey in this txin/txout + for pubkey in txinout.bip32_paths: + path = self.get_pubkey_derivation(pubkey, txinout, only_der_suffix=only_der_suffix) + if path and not isinstance(path, (str, bytes)): + return pubkey, list(path) + return None, None + class Software_KeyStore(KeyStore): @@ -210,14 +261,10 @@ class Imported_KeyStore(Software_KeyStore): raise InvalidPassword() return privkey, compressed - def get_pubkey_derivation(self, x_pubkey): - if x_pubkey[0:2] in ['02', '03', '04']: - if x_pubkey in self.keypairs.keys(): - return x_pubkey - elif x_pubkey[0:2] == 'fd': - addr = bitcoin.script_to_address(x_pubkey[2:]) - if addr in self.addresses: - return self.addresses[addr].get('pubkey') + def get_pubkey_derivation(self, pubkey, txin, *, only_der_suffix=True): + if pubkey.hex() in self.keypairs: + return pubkey.hex() + return None def update_password(self, old_password, new_password): self.check_password(old_password) @@ -230,7 +277,6 @@ class Imported_KeyStore(Software_KeyStore): self.pw_hash_version = PW_HASH_VERSION_LATEST - class Deterministic_KeyStore(Software_KeyStore): def __init__(self, d): @@ -277,15 +323,85 @@ class Deterministic_KeyStore(Software_KeyStore): class Xpub: - def __init__(self): + def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None): self.xpub = None self.xpub_receive = None self.xpub_change = None + # "key origin" info (subclass should persist these): + self._derivation_prefix = derivation_prefix # type: Optional[str] + self._root_fingerprint = root_fingerprint # type: Optional[str] + def get_master_public_key(self): return self.xpub - def derive_pubkey(self, for_change, n): + def get_derivation_prefix(self) -> Optional[str]: + """Returns to bip32 path from some root node to self.xpub + Note that the return value might be None; if it is unknown. + """ + return self._derivation_prefix + + def get_root_fingerprint(self) -> Optional[str]: + """Returns the bip32 fingerprint of the top level node. + This top level node is the node at the beginning of the derivation prefix, + i.e. applying the derivation prefix to it will result self.xpub + Note that the return value might be None; if it is unknown. + """ + return self._root_fingerprint + + def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int], *, + only_der_suffix: bool = True) -> Tuple[bytes, Sequence[int]]: + """Returns fingerprint and derivation path corresponding to a derivation suffix. + The fingerprint is either the root fp or the intermediate fp, depending on what is available + and 'only_der_suffix', and the derivation path is adjusted accordingly. + """ + fingerprint_hex = self.get_root_fingerprint() + der_prefix_str = self.get_derivation_prefix() + if not only_der_suffix and fingerprint_hex is not None and der_prefix_str is not None: + # use root fp, and true full path + fingerprint_bytes = bfh(fingerprint_hex) + der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str) + else: + # use intermediate fp, and claim der suffix is the full path + fingerprint_bytes = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node() + der_prefix_ints = convert_bip32_path_to_list_of_uint32('m') + der_full = der_prefix_ints + list(der_suffix) + return fingerprint_bytes, der_full + + def get_xpub_to_be_used_in_partial_tx(self, *, only_der_suffix: bool) -> str: + assert self.xpub + fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[], + only_der_suffix=only_der_suffix) + bip32node = BIP32Node.from_xkey(self.xpub) + depth = len(der_full) + child_number_int = der_full[-1] if len(der_full) >= 1 else 0 + child_number_bytes = child_number_int.to_bytes(length=4, byteorder="big") + fingerprint = bytes(4) if depth == 0 else bip32node.fingerprint + bip32node = bip32node._replace(depth=depth, + fingerprint=fingerprint, + child_number=child_number_bytes) + return bip32node.to_xpub() + + def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node): + assert self.xpub + # try to derive ourselves from what we were given + child_node1 = root_node.subkey_at_private_derivation(derivation_prefix) + child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True) + child_node2 = BIP32Node.from_xkey(self.xpub) + child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True) + if child_pubkey_bytes1 != child_pubkey_bytes2: + raise Exception("(xpub, derivation_prefix, root_node) inconsistency") + self.add_key_origin(derivation_prefix=derivation_prefix, + root_fingerprint=root_node.calc_fingerprint_of_this_node().hex().lower()) + + def add_key_origin(self, *, derivation_prefix: Optional[str], root_fingerprint: Optional[str]): + assert self.xpub + self._root_fingerprint = root_fingerprint + self._derivation_prefix = normalize_bip32_derivation(derivation_prefix) + + def derive_pubkey(self, for_change, n) -> str: + for_change = int(for_change) + assert for_change in (0, 1) xpub = self.xpub_change if for_change else self.xpub_receive if xpub is None: rootnode = BIP32Node.from_xkey(self.xpub) @@ -301,54 +417,13 @@ class Xpub: node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence) return node.eckey.get_public_key_hex(compressed=True) - def get_xpubkey(self, c, i): - def encode_path_int(path_int) -> str: - if path_int < 0xffff: - hex = bitcoin.int_to_hex(path_int, 2) - else: - hex = 'ffff' + bitcoin.int_to_hex(path_int, 4) - return hex - s = ''.join(map(encode_path_int, (c, i))) - return 'ff' + bh2u(bitcoin.DecodeBase58Check(self.xpub)) + s - - @classmethod - def parse_xpubkey(self, pubkey): - # type + xpub + derivation - assert pubkey[0:2] == 'ff' - pk = bfh(pubkey) - # xpub: - pk = pk[1:] - xkey = bitcoin.EncodeBase58Check(pk[0:78]) - # derivation: - dd = pk[78:] - s = [] - while dd: - # 2 bytes for derivation path index - n = int.from_bytes(dd[0:2], byteorder="little") - dd = dd[2:] - # in case of overflow, drop these 2 bytes; and use next 4 bytes instead - if n == 0xffff: - n = int.from_bytes(dd[0:4], byteorder="little") - dd = dd[4:] - s.append(n) - assert len(s) == 2 - return xkey, s - - def get_pubkey_derivation(self, x_pubkey): - if x_pubkey[0:2] != 'ff': - return - xpub, derivation = self.parse_xpubkey(x_pubkey) - if self.xpub != xpub: - return - return derivation - class BIP32_KeyStore(Deterministic_KeyStore, Xpub): type = 'bip32' def __init__(self, d): - Xpub.__init__(self) + Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint')) Deterministic_KeyStore.__init__(self, d) self.xpub = d.get('xpub') self.xprv = d.get('xprv') @@ -360,6 +435,8 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): d = Deterministic_KeyStore.dump(self) d['xpub'] = self.xpub d['xprv'] = self.xprv + d['derivation'] = self.get_derivation_prefix() + d['root_fingerprint'] = self.get_root_fingerprint() return d def get_master_private_key(self, password): @@ -388,14 +465,22 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): def is_watching_only(self): return self.xprv is None + def add_xpub(self, xpub): + assert is_xpub(xpub) + self.xpub = xpub + root_fingerprint, derivation_prefix = bip32.root_fp_and_der_prefix_from_xkey(xpub) + self.add_key_origin(derivation_prefix=derivation_prefix, root_fingerprint=root_fingerprint) + def add_xprv(self, xprv): + assert is_xprv(xprv) self.xprv = xprv - self.xpub = bip32.xpub_from_xprv(xprv) + self.add_xpub(bip32.xpub_from_xprv(xprv)) def add_xprv_from_seed(self, bip32_seed, xtype, derivation): rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype) node = rootnode.subkey_at_private_derivation(derivation) self.add_xprv(node.to_xprv()) + self.add_key_origin_from_root_node(derivation_prefix=derivation, root_node=rootnode) def get_private_key(self, sequence, password): xprv = self.get_master_private_key(password) @@ -415,6 +500,7 @@ class Old_KeyStore(Deterministic_KeyStore): def __init__(self, d): Deterministic_KeyStore.__init__(self, d) self.mpk = d.get('mpk') + self._root_fingerprint = None def get_hex_seed(self, password): return pw_decode(self.seed, password, version=self.pw_hash_version).encode('utf8') @@ -477,7 +563,7 @@ class Old_KeyStore(Deterministic_KeyStore): public_key = master_public_key + z*ecc.generator() return public_key.get_public_key_hex(compressed=False) - def derive_pubkey(self, for_change, n): + def derive_pubkey(self, for_change, n) -> str: return self.get_pubkey_from_mpk(self.mpk, for_change, n) def get_private_key_from_stretched_exponent(self, for_change, n, secexp): @@ -508,31 +594,25 @@ class Old_KeyStore(Deterministic_KeyStore): def get_master_public_key(self): return self.mpk - def get_xpubkey(self, for_change, n): - s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n))) - return 'fe' + self.mpk + s - - @classmethod - def parse_xpubkey(self, x_pubkey): - assert x_pubkey[0:2] == 'fe' - pk = x_pubkey[2:] - mpk = pk[0:128] - dd = pk[128:] - s = [] - while dd: - n = int(bitcoin.rev_hex(dd[0:4]), 16) - dd = dd[4:] - s.append(n) - assert len(s) == 2 - return mpk, s - - def get_pubkey_derivation(self, x_pubkey): - if x_pubkey[0:2] != 'fe': - return - mpk, derivation = self.parse_xpubkey(x_pubkey) - if self.mpk != mpk: - return - return derivation + def get_derivation_prefix(self) -> str: + return 'm' + + def get_root_fingerprint(self) -> str: + if self._root_fingerprint is None: + master_public_key = ecc.ECPubkey(bfh('04'+self.mpk)) + xfp = hash_160(master_public_key.get_public_key_bytes(compressed=True))[0:4] + self._root_fingerprint = xfp.hex().lower() + return self._root_fingerprint + + # TODO Old_KeyStore and Xpub could share a common baseclass? + def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int], *, + only_der_suffix: bool = True) -> Tuple[bytes, Sequence[int]]: + fingerprint_hex = self.get_root_fingerprint() + der_prefix_str = self.get_derivation_prefix() + fingerprint_bytes = bfh(fingerprint_hex) + der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str) + der_full = der_prefix_ints + list(der_suffix) + return fingerprint_bytes, der_full def update_password(self, old_password, new_password): self.check_password(old_password) @@ -554,14 +634,13 @@ class Hardware_KeyStore(KeyStore, Xpub): type = 'hardware' def __init__(self, d): - Xpub.__init__(self) + Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint')) KeyStore.__init__(self) # Errors and other user interaction is done through the wallet's # handler. The handler is per-window and preserved across # device reconnects self.xpub = d.get('xpub') self.label = d.get('label') - self.derivation = d.get('derivation') self.handler = None run_hook('init_keystore', self) @@ -582,7 +661,8 @@ class Hardware_KeyStore(KeyStore, Xpub): 'type': self.type, 'hw_type': self.hw_type, 'xpub': self.xpub, - 'derivation':self.derivation, + 'derivation': self.get_derivation_prefix(), + 'root_fingerprint': self.get_root_fingerprint(), 'label':self.label, } @@ -624,6 +704,16 @@ class Hardware_KeyStore(KeyStore, Xpub): def ready_to_sign(self): return super().ready_to_sign() and self.has_usable_connection_with_device() + def opportunistically_fill_in_missing_info_from_device(self, client): + assert client is not None + if self._root_fingerprint is None: + # digitalbitbox (at least) does not reveal xpubs corresponding to unhardened paths + # so ask for a direct child, and read out fingerprint from that: + child_of_root_xpub = client.get_xpub("m/0'", xtype='standard') + root_fingerprint = BIP32Node.from_xkey(child_of_root_xpub).fingerprint.hex().lower() + self._root_fingerprint = root_fingerprint + self.is_requesting_to_be_rewritten_to_wallet_file = True + def bip39_normalize_passphrase(passphrase): return normalize('NFKD', passphrase or '') @@ -684,16 +774,17 @@ PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES) def xtype_from_derivation(derivation: str) -> str: """Returns the script type to be used for this derivation.""" - if derivation.startswith("m/84'"): - return 'p2wpkh' - elif derivation.startswith("m/49'"): - return 'p2wpkh-p2sh' - elif derivation.startswith("m/44'"): - return 'standard' - elif derivation.startswith("m/45'"): - return 'standard' - bip32_indices = convert_bip32_path_to_list_of_uint32(derivation) + if len(bip32_indices) >= 1: + if bip32_indices[0] == 84 + BIP32_PRIME: + return 'p2wpkh' + elif bip32_indices[0] == 49 + BIP32_PRIME: + return 'p2wpkh-p2sh' + elif bip32_indices[0] == 44 + BIP32_PRIME: + return 'standard' + elif bip32_indices[0] == 45 + BIP32_PRIME: + return 'standard' + if len(bip32_indices) >= 4: if bip32_indices[0] == 48 + BIP32_PRIME: # m / purpose' / coin_type' / account' / script_type' / change / address_index @@ -704,40 +795,6 @@ def xtype_from_derivation(derivation: str) -> str: return 'standard' -# extended pubkeys - -def is_xpubkey(x_pubkey): - return x_pubkey[0:2] == 'ff' - - -def parse_xpubkey(x_pubkey): - assert x_pubkey[0:2] == 'ff' - return BIP32_KeyStore.parse_xpubkey(x_pubkey) - - -def xpubkey_to_address(x_pubkey): - if x_pubkey[0:2] == 'fd': - address = bitcoin.script_to_address(x_pubkey[2:]) - return x_pubkey, address - if x_pubkey[0:2] in ['02', '03', '04']: - pubkey = x_pubkey - elif x_pubkey[0:2] == 'ff': - xpub, s = BIP32_KeyStore.parse_xpubkey(x_pubkey) - pubkey = BIP32_KeyStore.get_pubkey_from_xpub(xpub, s) - elif x_pubkey[0:2] == 'fe': - mpk, s = Old_KeyStore.parse_xpubkey(x_pubkey) - pubkey = Old_KeyStore.get_pubkey_from_mpk(mpk, s[0], s[1]) - else: - raise BitcoinException("Cannot parse pubkey. prefix: {}" - .format(x_pubkey[0:2])) - if pubkey: - address = public_key_to_p2pkh(bfh(pubkey)) - return pubkey, address - -def xpubkey_to_pubkey(x_pubkey): - pubkey, address = xpubkey_to_address(x_pubkey) - return pubkey - hw_keystores = {} def register_keystore(hw_type, constructor): @@ -770,7 +827,7 @@ def load_keystore(storage, name) -> KeyStore: def is_old_mpk(mpk: str) -> bool: try: - int(mpk, 16) + int(mpk, 16) # test if hex string except: return False if len(mpk) != 128: @@ -804,16 +861,18 @@ def is_private_key_list(text, *, allow_spaces_inside_key=True, raise_on_error=Fa raise_on_error=raise_on_error)) -is_mpk = lambda x: is_old_mpk(x) or is_xpub(x) -is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x) -is_master_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x) -is_private_key = lambda x: is_xprv(x) or is_private_key_list(x) -is_bip32_key = lambda x: is_xprv(x) or is_xpub(x) +def is_master_key(x): + return is_old_mpk(x) or is_bip32_key(x) + + +def is_bip32_key(x): + return is_xprv(x) or is_xpub(x) def bip44_derivation(account_id, bip43_purpose=44): coin = constants.net.BIP44_COIN_TYPE - return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) + der = "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) + return normalize_bip32_derivation(der) def purpose48_derivation(account_id: int, xtype: str) -> str: @@ -824,7 +883,8 @@ def purpose48_derivation(account_id: int, xtype: str) -> str: script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype) if script_type_int is None: raise Exception('unknown xtype: {}'.format(xtype)) - return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) + der = "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) + return normalize_bip32_derivation(der) def from_seed(seed, passphrase, is_p2sh=False): @@ -861,14 +921,12 @@ def from_old_mpk(mpk): def from_xpub(xpub): k = BIP32_KeyStore({}) - k.xpub = xpub + k.add_xpub(xpub) return k def from_xprv(xprv): - xpub = bip32.xpub_from_xprv(xprv) k = BIP32_KeyStore({}) - k.xprv = xprv - k.xpub = xpub + k.add_xprv(xprv) return k def from_master_key(text): diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py @@ -32,10 +32,9 @@ import time from . import ecc from .util import bfh, bh2u -from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS from .bitcoin import redeem_script_to_address from .crypto import sha256, sha256d -from .transaction import Transaction +from .transaction import Transaction, PartialTransaction from .logging import Logger from .lnonion import decode_onion_error @@ -528,19 +527,19 @@ class Channel(Logger): ctx = self.make_commitment(subject, point, ctn) return secret, ctx - def get_commitment(self, subject, ctn): + def get_commitment(self, subject, ctn) -> PartialTransaction: secret, ctx = self.get_secret_and_commitment(subject, ctn) return ctx - def get_next_commitment(self, subject: HTLCOwner) -> Transaction: + def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction: ctn = self.get_next_ctn(subject) return self.get_commitment(subject, ctn) - def get_latest_commitment(self, subject: HTLCOwner) -> Transaction: + def get_latest_commitment(self, subject: HTLCOwner) -> PartialTransaction: ctn = self.get_latest_ctn(subject) return self.get_commitment(subject, ctn) - def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> Transaction: + def get_oldest_unrevoked_commitment(self, subject: HTLCOwner) -> PartialTransaction: ctn = self.get_oldest_unrevoked_ctn(subject) return self.get_commitment(subject, ctn) @@ -603,7 +602,7 @@ class Channel(Logger): self.hm.recv_fail(htlc_id) def pending_local_fee(self): - return self.constraints.capacity - sum(x[2] for x in self.get_next_commitment(LOCAL).outputs()) + return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs()) def update_fee(self, feerate: int, from_us: bool): # feerate uses sat/kw @@ -658,7 +657,7 @@ class Channel(Logger): def __str__(self): return str(self.serialize()) - def make_commitment(self, subject, this_point, ctn) -> Transaction: + def make_commitment(self, subject, this_point, ctn) -> PartialTransaction: assert type(subject) is HTLCOwner feerate = self.get_feerate(subject, ctn) other = REMOTE if LOCAL == subject else LOCAL @@ -717,21 +716,20 @@ class Channel(Logger): onchain_fees, htlcs=htlcs) - def get_local_index(self): - return int(self.config[LOCAL].multisig_key.pubkey > self.config[REMOTE].multisig_key.pubkey) - def make_closing_tx(self, local_script: bytes, remote_script: bytes, - fee_sat: int) -> Tuple[bytes, Transaction]: + fee_sat: int) -> Tuple[bytes, PartialTransaction]: """ cooperative close """ - _, outputs = make_commitment_outputs({ + _, outputs = make_commitment_outputs( + fees_per_participant={ LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0, REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0, }, - self.balance(LOCAL), - self.balance(REMOTE), - (TYPE_SCRIPT, bh2u(local_script)), - (TYPE_SCRIPT, bh2u(remote_script)), - [], self.config[LOCAL].dust_limit_sat) + local_amount_msat=self.balance(LOCAL), + remote_amount_msat=self.balance(REMOTE), + local_script=bh2u(local_script), + remote_script=bh2u(remote_script), + htlcs=[], + dust_limit_sat=self.config[LOCAL].dust_limit_sat) closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey, self.config[REMOTE].multisig_key.pubkey, @@ -744,25 +742,23 @@ class Channel(Logger): sig = ecc.sig_string_from_der_sig(der_sig[:-1]) return sig, closing_tx - def signature_fits(self, tx): + def signature_fits(self, tx: PartialTransaction): remote_sig = self.config[LOCAL].current_commitment_signature preimage_hex = tx.serialize_preimage(0) - pre_hash = sha256d(bfh(preimage_hex)) + msg_hash = sha256d(bfh(preimage_hex)) assert remote_sig - res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash) + res = ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, msg_hash) return res def force_close_tx(self): tx = self.get_latest_commitment(LOCAL) assert self.signature_fits(tx) - tx = Transaction(str(tx)) - tx.deserialize(True) tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)}) remote_sig = self.config[LOCAL].current_commitment_signature remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01" - sigs = tx._inputs[0]["signatures"] - none_idx = sigs.index(None) - tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) + tx.add_signature_to_txin(txin_idx=0, + signing_pubkey=self.config[REMOTE].multisig_key.pubkey.hex(), + sig=remote_sig.hex()) assert tx.is_complete() return tx diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -11,7 +11,7 @@ import asyncio import os import time from functools import partial -from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable +from typing import List, Tuple, Dict, TYPE_CHECKING, Optional, Callable, Union import traceback import sys from datetime import datetime @@ -24,7 +24,7 @@ from . import ecc from .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string from . import constants from .util import bh2u, bfh, log_exceptions, list_enabled_bits, ignore_exceptions, chunks, SilentTaskGroup -from .transaction import Transaction, TxOutput +from .transaction import Transaction, TxOutput, PartialTxOutput from .logging import Logger from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage, @@ -48,7 +48,7 @@ from .interface import GracefulDisconnect, NetworkException from .lnrouter import fee_for_edge_msat if TYPE_CHECKING: - from .lnworker import LNWorker + from .lnworker import LNWorker, LNGossip, LNWallet from .lnrouter import RouteEdge @@ -62,7 +62,7 @@ def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[b class Peer(Logger): - def __init__(self, lnworker: 'LNWorker', pubkey:bytes, transport: LNTransportBase): + def __init__(self, lnworker: Union['LNGossip', 'LNWallet'], pubkey:bytes, transport: LNTransportBase): self.initialized = asyncio.Event() self.querying = asyncio.Event() self.transport = transport @@ -483,8 +483,8 @@ class Peer(Logger): push_msat: int, temp_channel_id: bytes) -> Channel: wallet = self.lnworker.wallet # dry run creating funding tx to see if we even have enough funds - funding_tx_test = wallet.mktx([TxOutput(bitcoin.TYPE_ADDRESS, wallet.dummy_address(), funding_sat)], - password, nonlocal_only=True) + funding_tx_test = wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(wallet.dummy_address(), funding_sat)], + password=password, nonlocal_only=True) await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT) feerate = self.lnworker.current_feerate_per_kw() local_config = self.make_local_config(funding_sat, push_msat, LOCAL) @@ -563,8 +563,8 @@ class Peer(Logger): # create funding tx redeem_script = funding_output_script(local_config, remote_config) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) - funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat) - funding_tx = wallet.mktx([funding_output], password, nonlocal_only=True) + funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat) + funding_tx = wallet.mktx(outputs=[funding_output], password=password, nonlocal_only=True) funding_txid = funding_tx.txid() funding_index = funding_tx.outputs().index(funding_output) # remote commitment transaction @@ -691,7 +691,7 @@ class Peer(Logger): outp = funding_tx.outputs()[funding_idx] redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL]) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) - if outp != TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat): + if not (outp.address == funding_address and outp.value == funding_sat): chan.set_state('DISCONNECTED') raise Exception('funding outpoint mismatch') @@ -1485,11 +1485,13 @@ class Peer(Logger): break # TODO: negotiate better our_fee = their_fee - # index of our_sig - i = chan.get_local_index() # add signatures - closing_tx.add_signature_to_txin(0, i, bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) - closing_tx.add_signature_to_txin(0, 1-i, bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) + closing_tx.add_signature_to_txin(txin_idx=0, + signing_pubkey=chan.config[LOCAL].multisig_key.pubkey, + sig=bh2u(der_sig_from_sig_string(our_sig) + b'\x01')) + closing_tx.add_signature_to_txin(txin_idx=0, + signing_pubkey=chan.config[REMOTE].multisig_key.pubkey, + sig=bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) # broadcast await self.network.broadcast_transaction(closing_tx) return closing_tx.txid() diff --git a/electrum/lnsweep.py b/electrum/lnsweep.py @@ -6,7 +6,7 @@ from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Calla from enum import Enum, auto from .util import bfh, bh2u -from .bitcoin import TYPE_ADDRESS, redeem_script_to_address, dust_threshold +from .bitcoin import redeem_script_to_address, dust_threshold from . import ecc from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script, derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey, @@ -15,7 +15,8 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, map_htlcs_to_ctx_output_idxs, Direction) -from .transaction import Transaction, TxOutput, construct_witness +from .transaction import (Transaction, TxOutput, construct_witness, PartialTransaction, PartialTxInput, + PartialTxOutput, TxOutpoint) from .simple_config import SimpleConfig from .logging import get_logger @@ -254,7 +255,7 @@ def create_sweeptxs_for_our_ctx(*, chan: 'Channel', ctx: Transaction, is_revocation=False, config=chan.lnworker.config) # side effect - txs[htlc_tx.prevout(0)] = SweepInfo(name='first-stage-htlc', + txs[htlc_tx.inputs()[0].prevout.to_str()] = SweepInfo(name='first-stage-htlc', csv_delay=0, cltv_expiry=htlc_tx.locktime, gen_tx=lambda: htlc_tx) @@ -336,7 +337,7 @@ def create_sweeptxs_for_their_ctx(*, chan: 'Channel', ctx: Transaction, gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address) if gen_tx: tx = gen_tx() - txs[tx.prevout(0)] = SweepInfo(name='to_local_for_revoked_ctx', + txs[tx.inputs()[0].prevout.to_str()] = SweepInfo(name='to_local_for_revoked_ctx', csv_delay=0, cltv_expiry=0, gen_tx=gen_tx) @@ -433,66 +434,58 @@ def create_htlctx_that_spends_from_our_ctx(chan: 'Channel', our_pcp: bytes, local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey)) txin = htlc_tx.inputs()[0] witness_program = bfh(Transaction.get_preimage_script(txin)) - txin['witness'] = bh2u(make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program)) + txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program) return witness_script, htlc_tx def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep_address: str, preimage: Optional[bytes], output_idx: int, privkey: bytes, is_revocation: bool, - cltv_expiry: int, config: SimpleConfig) -> Optional[Transaction]: + cltv_expiry: int, config: SimpleConfig) -> Optional[PartialTransaction]: assert type(cltv_expiry) is int preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) val = ctx.outputs()[output_idx].value - sweep_inputs = [{ - 'scriptSig': '', - 'type': 'p2wsh', - 'signatures': [], - 'num_sig': 0, - 'prevout_n': output_idx, - 'prevout_hash': ctx.txid(), - 'value': val, - 'coinbase': False, - 'preimage_script': bh2u(witness_script), - }] + prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) + txin = PartialTxInput(prevout=prevout) + txin._trusted_value_sats = val + txin.witness_script = witness_script + txin.script_sig = b'' + sweep_inputs = [txin] tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) outvalue = val - fee if outvalue <= dust_threshold(): return None - sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] - tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry) + sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] + tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry) sig = bfh(tx.sign_txin(0, privkey)) if not is_revocation: witness = construct_witness([sig, preimage, witness_script]) else: revocation_pubkey = privkey_to_pubkey(privkey) witness = construct_witness([sig, revocation_pubkey, witness_script]) - tx.inputs()[0]['witness'] = witness + tx.inputs()[0].witness = bfh(witness) assert tx.is_complete() return tx def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int, our_payment_privkey: ecc.ECPrivkey, - config: SimpleConfig) -> Optional[Transaction]: + config: SimpleConfig) -> Optional[PartialTransaction]: our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True) val = ctx.outputs()[output_idx].value - sweep_inputs = [{ - 'type': 'p2wpkh', - 'x_pubkeys': [our_payment_pubkey], - 'num_sig': 1, - 'prevout_n': output_idx, - 'prevout_hash': ctx.txid(), - 'value': val, - 'coinbase': False, - 'signatures': [None], - }] + prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) + txin = PartialTxInput(prevout=prevout) + txin._trusted_value_sats = val + txin.script_type = 'p2wpkh' + txin.pubkeys = [bfh(our_payment_pubkey)] + txin.num_sig = 1 + sweep_inputs = [txin] tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) outvalue = val - fee if outvalue <= dust_threshold(): return None - sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] - sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs) + sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] + sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs) sweep_tx.set_rbf(True) sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) if not sweep_tx.is_complete(): @@ -502,7 +495,7 @@ def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, out def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_idx: int, witness_script: str, privkey: bytes, is_revocation: bool, config: SimpleConfig, - to_self_delay: int=None) -> Optional[Transaction]: + to_self_delay: int=None) -> Optional[PartialTransaction]: """Create a txn that sweeps the 'to_local' output of a commitment transaction into our wallet. @@ -510,61 +503,51 @@ def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_ is_revocation: tells us which ^ """ val = ctx.outputs()[output_idx].value - sweep_inputs = [{ - 'scriptSig': '', - 'type': 'p2wsh', - 'signatures': [], - 'num_sig': 0, - 'prevout_n': output_idx, - 'prevout_hash': ctx.txid(), - 'value': val, - 'coinbase': False, - 'preimage_script': witness_script, - }] + prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) + txin = PartialTxInput(prevout=prevout) + txin._trusted_value_sats = val + txin.script_sig = b'' + txin.witness_script = bfh(witness_script) + sweep_inputs = [txin] if not is_revocation: assert isinstance(to_self_delay, int) - sweep_inputs[0]['sequence'] = to_self_delay + sweep_inputs[0].nsequence = to_self_delay tx_size_bytes = 121 # approx size of to_local -> p2wpkh fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) outvalue = val - fee if outvalue <= dust_threshold(): return None - sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] - sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) + sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] + sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2) sig = sweep_tx.sign_txin(0, privkey) witness = construct_witness([sig, int(is_revocation), witness_script]) - sweep_tx.inputs()[0]['witness'] = witness + sweep_tx.inputs()[0].witness = bfh(witness) return sweep_tx def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(*, htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str, privkey: bytes, is_revocation: bool, to_self_delay: int, - config: SimpleConfig) -> Optional[Transaction]: + config: SimpleConfig) -> Optional[PartialTransaction]: val = htlc_tx.outputs()[0].value - sweep_inputs = [{ - 'scriptSig': '', - 'type': 'p2wsh', - 'signatures': [], - 'num_sig': 0, - 'prevout_n': 0, - 'prevout_hash': htlc_tx.txid(), - 'value': val, - 'coinbase': False, - 'preimage_script': bh2u(htlctx_witness_script), - }] + prevout = TxOutpoint(txid=bfh(htlc_tx.txid()), out_idx=0) + txin = PartialTxInput(prevout=prevout) + txin._trusted_value_sats = val + txin.script_sig = b'' + txin.witness_script = htlctx_witness_script + sweep_inputs = [txin] if not is_revocation: assert isinstance(to_self_delay, int) - sweep_inputs[0]['sequence'] = to_self_delay + sweep_inputs[0].nsequence = to_self_delay tx_size_bytes = 200 # TODO fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) outvalue = val - fee if outvalue <= dust_threshold(): return None - sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] - tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) + sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)] + tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2) sig = bfh(tx.sign_txin(0, privkey)) witness = construct_witness([sig, int(is_revocation), htlctx_witness_script]) - tx.inputs()[0]['witness'] = witness + tx.inputs()[0].witness = bfh(witness) assert tx.is_complete() return tx diff --git a/electrum/lnutil.py b/electrum/lnutil.py @@ -10,11 +10,11 @@ import re from .util import bfh, bh2u, inv_dict from .crypto import sha256 -from .transaction import Transaction +from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint, + PartialTxOutput, opcodes, TxOutput) from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number from . import ecc, bitcoin, crypto, transaction -from .transaction import opcodes, TxOutput, Transaction -from .bitcoin import push_script, redeem_script_to_address, TYPE_ADDRESS +from .bitcoin import push_script, redeem_script_to_address, address_to_script from . import segwit_addr from .i18n import _ from .lnaddr import lndecode @@ -97,6 +97,7 @@ class ScriptHtlc(NamedTuple): htlc: 'UpdateAddHtlc' +# FIXME duplicate of TxOutpoint in transaction.py?? class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])): def to_str(self): return "{}:{}".format(self.txid, self.output_index) @@ -287,7 +288,7 @@ def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_dela fee = fee // 1000 * 1000 final_amount_sat = (amount_msat - fee) // 1000 assert final_amount_sat > 0, final_amount_sat - output = TxOutput(bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat) + output = PartialTxOutput.from_address_and_value(p2wsh, final_amount_sat) return script, output def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes, @@ -299,29 +300,23 @@ def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes, return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script])) def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int, - amount_msat: int, witness_script: str) -> List[dict]: + amount_msat: int, witness_script: str) -> List[PartialTxInput]: assert type(htlc_output_txid) is str assert type(htlc_output_index) is int assert type(amount_msat) is int assert type(witness_script) is str - c_inputs = [{ - 'scriptSig': '', - 'type': 'p2wsh', - 'signatures': [], - 'num_sig': 0, - 'prevout_n': htlc_output_index, - 'prevout_hash': htlc_output_txid, - 'value': amount_msat // 1000, - 'coinbase': False, - 'sequence': 0x0, - 'preimage_script': witness_script, - }] + txin = PartialTxInput(prevout=TxOutpoint(txid=bfh(htlc_output_txid), out_idx=htlc_output_index), + nsequence=0) + txin.witness_script = bfh(witness_script) + txin.script_sig = b'' + txin._trusted_value_sats = amount_msat // 1000 + c_inputs = [txin] return c_inputs -def make_htlc_tx(*, cltv_expiry: int, inputs, output) -> Transaction: +def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction: assert type(cltv_expiry) is int c_outputs = [output] - tx = Transaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2) + tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2) return tx def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, @@ -437,7 +432,7 @@ def map_htlcs_to_ctx_output_idxs(*, chan: 'Channel', ctx: Transaction, pcp: byte def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner', htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int, - htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, Transaction]: + htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, PartialTransaction]: amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash for_us = subject == LOCAL conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us) @@ -472,19 +467,15 @@ def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTL return witness_script_of_htlc_tx_output, htlc_tx def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, - funding_pos: int, funding_txid: bytes, funding_sat: int): + funding_pos: int, funding_txid: str, funding_sat: int) -> PartialTxInput: pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)]) # commitment tx input - c_input = { - 'type': 'p2wsh', - 'x_pubkeys': pubkeys, - 'signatures': [None, None], - 'num_sig': 2, - 'prevout_n': funding_pos, - 'prevout_hash': funding_txid, - 'value': funding_sat, - 'coinbase': False, - } + prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos) + c_input = PartialTxInput(prevout=prevout) + c_input.script_type = 'p2wsh' + c_input.pubkeys = [bfh(pk) for pk in pubkeys] + c_input.num_sig = 2 + c_input._trusted_value_sats = funding_sat return c_input class HTLCOwner(IntFlag): @@ -504,18 +495,18 @@ RECEIVED = Direction.RECEIVED LOCAL = HTLCOwner.LOCAL REMOTE = HTLCOwner.REMOTE -def make_commitment_outputs(fees_per_participant: Mapping[HTLCOwner, int], local_amount: int, remote_amount: int, - local_tupl, remote_tupl, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[TxOutput], List[TxOutput]]: - to_local_amt = local_amount - fees_per_participant[LOCAL] - to_local = TxOutput(*local_tupl, to_local_amt // 1000) - to_remote_amt = remote_amount - fees_per_participant[REMOTE] - to_remote = TxOutput(*remote_tupl, to_remote_amt // 1000) +def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int, + local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]: + to_local_amt = local_amount_msat - fees_per_participant[LOCAL] + to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000) + to_remote_amt = remote_amount_msat - fees_per_participant[REMOTE] + to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000) non_htlc_outputs = [to_local, to_remote] htlc_outputs = [] for script, htlc in htlcs: - htlc_outputs.append(TxOutput(bitcoin.TYPE_ADDRESS, - bitcoin.redeem_script_to_address('p2wsh', bh2u(script)), - htlc.amount_msat // 1000)) + addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script)) + htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)), + value=htlc.amount_msat // 1000)) # trim outputs c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs)) @@ -533,13 +524,13 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, delayed_pubkey, to_self_delay, funding_txid, funding_pos, funding_sat, local_amount, remote_amount, dust_limit_sat, fees_per_participant, - htlcs: List[ScriptHtlc]) -> Transaction: + htlcs: List[ScriptHtlc]) -> PartialTransaction: c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey, funding_pos, funding_txid, funding_sat) obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint) locktime = (0x20 << 24) + (obs & 0xffffff) sequence = (0x80 << 24) + (obs >> 24) - c_input['sequence'] = sequence + c_input.nsequence = sequence c_inputs = [c_input] @@ -555,13 +546,19 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey, htlcs = list(htlcs) htlcs.sort(key=lambda x: x.htlc.cltv_expiry) - htlc_outputs, c_outputs_filtered = make_commitment_outputs(fees_per_participant, local_amount, remote_amount, - (bitcoin.TYPE_ADDRESS, local_address), (bitcoin.TYPE_ADDRESS, remote_address), htlcs, dust_limit_sat) + htlc_outputs, c_outputs_filtered = make_commitment_outputs( + fees_per_participant=fees_per_participant, + local_amount_msat=local_amount, + remote_amount_msat=remote_amount, + local_script=address_to_script(local_address), + remote_script=address_to_script(remote_address), + htlcs=htlcs, + dust_limit_sat=dust_limit_sat) assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat) # create commitment tx - tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2) + tx = PartialTransaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2) return tx def make_commitment_output_to_local_witness_script( @@ -578,11 +575,9 @@ def make_commitment_output_to_local_address( def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str: return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey)) -def sign_and_get_sig_string(tx, local_config, remote_config): - pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)]) +def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config): tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)}) - sig_index = pubkeys.index(bh2u(local_config.multisig_key.pubkey)) - sig = bytes.fromhex(tx.inputs()[0]["signatures"][sig_index]) + sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey] sig_64 = sig_string_from_der_sig(sig[:-1]) return sig_64 @@ -598,11 +593,11 @@ def get_obscured_ctn(ctn: int, funder: bytes, fundee: bytes) -> int: mask = int.from_bytes(sha256(funder + fundee)[-6:], 'big') return ctn ^ mask -def extract_ctn_from_tx(tx, txin_index: int, funder_payment_basepoint: bytes, +def extract_ctn_from_tx(tx: Transaction, txin_index: int, funder_payment_basepoint: bytes, fundee_payment_basepoint: bytes) -> int: tx.deserialize() locktime = tx.locktime - sequence = tx.inputs()[txin_index]['sequence'] + sequence = tx.inputs()[txin_index].nsequence obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff) return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint) @@ -671,12 +666,12 @@ def get_compressed_pubkey_from_bech32(bech32_pubkey: str) -> bytes: def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, - funding_txid: bytes, funding_pos: int, funding_sat: int, - outputs: List[TxOutput]) -> Transaction: + funding_txid: str, funding_pos: int, funding_sat: int, + outputs: List[PartialTxOutput]) -> PartialTransaction: c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey, funding_pos, funding_txid, funding_sat) - c_input['sequence'] = 0xFFFF_FFFF - tx = Transaction.from_io([c_input], outputs, locktime=0, version=2) + c_input.nsequence = 0xFFFF_FFFF + tx = PartialTransaction.from_io([c_input], outputs, locktime=0, version=2) return tx diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py @@ -77,9 +77,11 @@ class SweepStore(SqlDB): return set([r[0] for r in c.fetchall()]) @sql - def add_sweep_tx(self, funding_outpoint, ctn, prevout, tx): + def add_sweep_tx(self, funding_outpoint, ctn, prevout, tx: Transaction): c = self.conn.cursor() - c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, bfh(str(tx)))) + assert tx.is_complete() + raw_tx = bfh(tx.serialize()) + c.execute("""INSERT INTO sweep_txs (funding_outpoint, ctn, prevout, tx) VALUES (?,?,?,?)""", (funding_outpoint, ctn, prevout, raw_tx)) self.conn.commit() @sql diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -375,7 +375,7 @@ class LNWallet(LNWorker): for ctn in range(watchtower_ctn + 1, current_ctn): sweeptxs = chan.create_sweeptxs(ctn) for tx in sweeptxs: - await watchtower.add_sweep_tx(outpoint, ctn, tx.prevout(0), str(tx)) + await watchtower.add_sweep_tx(outpoint, ctn, tx.inputs()[0].prevout.to_str(), tx.serialize()) def start_network(self, network: 'Network'): self.lnwatcher = LNWatcher(network) diff --git a/electrum/network.py b/electrum/network.py @@ -64,6 +64,7 @@ if TYPE_CHECKING: from .channel_db import ChannelDB from .lnworker import LNGossip from .lnwatcher import WatchTower + from .transaction import Transaction _logger = get_logger(__name__) @@ -887,11 +888,11 @@ class Network(Logger): return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height]) @best_effort_reliable - async def broadcast_transaction(self, tx, *, timeout=None) -> None: + async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None: if timeout is None: timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent) try: - out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout) + out = await self.interface.session.send_request('blockchain.transaction.broadcast', [tx.serialize()], timeout=timeout) # note: both 'out' and exception messages are untrusted input from the server except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError): raise # pass-through diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py @@ -25,7 +25,7 @@ import hashlib import sys import time -from typing import Optional +from typing import Optional, List import asyncio import urllib.parse @@ -42,8 +42,8 @@ from . import bitcoin, ecc, util, transaction, x509, rsakey from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT from .crypto import sha256 -from .bitcoin import TYPE_ADDRESS -from .transaction import TxOutput +from .bitcoin import address_to_script +from .transaction import PartialTxOutput from .network import Network from .logging import get_logger, Logger @@ -128,7 +128,7 @@ class PaymentRequest: return str(self.raw) def parse(self, r): - self.outputs = [] + self.outputs = [] # type: List[PartialTxOutput] if self.error: return self.id = bh2u(sha256(r)[0:16]) @@ -141,12 +141,12 @@ class PaymentRequest: self.details = pb2.PaymentDetails() self.details.ParseFromString(self.data.serialized_payment_details) for o in self.details.outputs: - type_, addr = transaction.get_address_from_output_script(o.script) - if type_ != TYPE_ADDRESS: + addr = transaction.get_address_from_output_script(o.script) + if not addr: # TODO maybe rm restriction but then get_requestor and get_id need changes self.error = "only addresses are allowed as outputs" return - self.outputs.append(TxOutput(type_, addr, o.amount)) + self.outputs.append(PartialTxOutput.from_address_and_value(addr, o.amount)) self.memo = self.details.memo self.payment_url = self.details.payment_url @@ -252,8 +252,9 @@ class PaymentRequest: def get_address(self): o = self.outputs[0] - assert o.type == TYPE_ADDRESS - return o.address + addr = o.address + assert addr + return addr def get_requestor(self): return self.requestor if self.requestor else self.get_address() @@ -278,7 +279,7 @@ class PaymentRequest: paymnt.merchant_data = pay_det.merchant_data paymnt.transactions.append(bfh(raw_tx)) ref_out = paymnt.refund_to.add() - ref_out.script = util.bfh(transaction.Transaction.pay_script(TYPE_ADDRESS, refund_addr)) + ref_out.script = util.bfh(address_to_script(refund_addr)) paymnt.memo = "Paid using Electrum" pm = paymnt.SerializeToString() payurl = urllib.parse.urlparse(pay_det.payment_url) @@ -326,7 +327,7 @@ def make_unsigned_request(req): if amount is None: amount = 0 memo = req['memo'] - script = bfh(Transaction.pay_script(TYPE_ADDRESS, addr)) + script = bfh(address_to_script(addr)) outputs = [(script, amount)] pd = pb2.PaymentDetails() for script, amount in outputs: diff --git a/electrum/plugin.py b/electrum/plugin.py @@ -39,6 +39,7 @@ from .logging import get_logger, Logger if TYPE_CHECKING: from .plugins.hw_wallet import HW_PluginBase + from .keystore import Hardware_KeyStore _logger = get_logger(__name__) @@ -442,20 +443,23 @@ class DeviceMgr(ThreadJob): self.scan_devices() return self.client_lookup(id_) - def client_for_keystore(self, plugin, handler, keystore, force_pair): + def client_for_keystore(self, plugin, handler, keystore: 'Hardware_KeyStore', force_pair): self.logger.info("getting client for keystore") if handler is None: raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing.")) handler.update_status(False) devices = self.scan_devices() xpub = keystore.xpub - derivation = keystore.get_derivation() + derivation = keystore.get_derivation_prefix() + assert derivation is not None client = self.client_by_xpub(plugin, xpub, handler, devices) if client is None and force_pair: info = self.select_device(plugin, handler, keystore, devices) client = self.force_pair_xpub(plugin, handler, info, xpub, derivation, devices) if client: handler.update_status(True) + if client: + keystore.opportunistically_fill_in_missing_info_from_device(client) self.logger.info("end client for keystore") return client diff --git a/electrum/plugins/audio_modem/qt.py b/electrum/plugins/audio_modem/qt.py @@ -4,6 +4,7 @@ import json from io import BytesIO import sys import platform +from typing import TYPE_CHECKING from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton) @@ -12,6 +13,9 @@ from electrum.gui.qt.util import WaitingDialog, EnterButton, WindowModalDialog, from electrum.i18n import _ from electrum.logging import get_logger +if TYPE_CHECKING: + from electrum.gui.qt.transaction_dialog import TxDialog + _logger = get_logger(__name__) @@ -71,12 +75,12 @@ class Plugin(BasePlugin): return bool(d.exec_()) @hook - def transaction_dialog(self, dialog): + def transaction_dialog(self, dialog: 'TxDialog'): b = QPushButton() b.setIcon(read_QIcon("speaker.png")) def handler(): - blob = json.dumps(dialog.tx.as_dict()) + blob = dialog.tx.serialize() self._send(parent=dialog, blob=blob) b.clicked.connect(handler) dialog.sharing_buttons.insert(-1, b) diff --git a/electrum/plugins/coldcard/basic_psbt.py b/electrum/plugins/coldcard/basic_psbt.py @@ -1,313 +0,0 @@ -# -# 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 -import 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 - diff --git a/electrum/plugins/coldcard/build_psbt.py b/electrum/plugins/coldcard/build_psbt.py @@ -1,397 +0,0 @@ -# -# build_psbt.py - create a PSBT from (unsigned) transaction and keystore data. -# -import io -import struct -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, Abstract_Wallet -from electrum.keystore import xpubkey_to_pubkey, Xpub -from electrum.util import bfh, bh2u -from electrum.crypto import hash_160, sha256 -from electrum.bitcoin import DecodeBase58Check -from electrum.i18n import _ - -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, PSBT_OUT_WITNESS_SCRIPT) -from .basic_psbt import BasicPSBT - - -_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 - -def packed_xfp_path_for_keystore(ks, int_path=[]): - # Return XFP + common prefix path for keystore, as binary ready for PSBT - derv = getattr(ks, 'derivation', 'm') - return packed_xfp_path(xfp_for_keystore(ks), derv[2:] or 'm', int_path=int_path) - -# 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: Abstract_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_for_keystore(ks) - - # 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) - output_info = tx.output_info.get(o.address) - if not output_info.is_change: - continue - chg_path = output_info.address_index - assert chg_path[0] == 1 and len(chg_path) == 2, f"unexpected change path: {chg_path}" - 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()): - ks_prefix = packed_xfp_path_for_keystore(ks) - - write_kv(PSBT_GLOBAL_XPUB, ks_prefix, 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 Transaction.is_segwit_input(txin): - # 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_for_keystore(ks, 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(pubkey) - 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) - if output_info.is_change: - pubkeys = [bfh(i) for i in wallet.get_public_keys(o.address)] - - # Add redeem/witness script? - if type(wallet) is Multisig_Wallet: - # always need a redeem script for multisig cases - scr = bfh(multisig_script([bh2u(i) for i in sorted(pubkeys)], wallet.m)) - - if output_info.script_type == 'p2wsh-p2sh': - write_kv(PSBT_OUT_WITNESS_SCRIPT, scr) - write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x20' + sha256(scr)) - elif output_info.script_type == 'p2wsh': - write_kv(PSBT_OUT_WITNESS_SCRIPT, scr) - elif output_info.script_type == 'p2sh': - write_kv(PSBT_OUT_REDEEM_SCRIPT, scr) - else: - raise ValueError(output_info.script_type) - - elif output_info.script_type == 'p2wpkh-p2sh': - # need a redeem script when P2SH is used to wrap p2wpkh - assert len(pubkeys) == 1 - pa = hash_160(pubkeys[0]) - write_kv(PSBT_OUT_REDEEM_SCRIPT, b'\x00\x14' + pa) - - # Document change output's bip32 derivation(s) - for pubkey in pubkeys: - sk = subkeys[pubkey] - write_kv(PSBT_OUT_BIP32_DERIVATION, sk, pubkey) - - 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: Abstract_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 - diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py @@ -2,16 +2,18 @@ # Coldcard Electrum plugin main code. # # -from struct import pack, unpack -import os, sys, time, io +import os, time, io import traceback +from typing import TYPE_CHECKING +import struct +from electrum import bip32 from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes from electrum.i18n import _ from electrum.plugin import Device, hook from electrum.keystore import Hardware_KeyStore -from electrum.transaction import Transaction, multisig_script -from electrum.wallet import Standard_Wallet, Multisig_Wallet +from electrum.transaction import PartialTransaction +from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet from electrum.util import bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported from electrum.logging import get_logger @@ -19,9 +21,9 @@ from electrum.logging import get_logger from ..hw_wallet import HW_PluginBase from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available -from .basic_psbt import BasicPSBT -from .build_psbt import (build_psbt, xfp2str, unpacked_xfp_path, - merge_sigs_from_psbt, xfp_for_keystore) +if TYPE_CHECKING: + from electrum.keystore import Xpub + _logger = get_logger(__name__) @@ -86,7 +88,7 @@ class CKCCClient: return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint), self.label()) - def verify_connection(self, expected_xfp, expected_xpub=None): + def verify_connection(self, expected_xfp: int, expected_xpub=None): ex = (expected_xfp, expected_xpub) if self._expected_device == ex: @@ -213,7 +215,7 @@ class CKCCClient: # poll device... if user has approved, will get tuple: (addr, sig) else None return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None) - def sign_transaction_start(self, raw_psbt, finalize=True): + def sign_transaction_start(self, raw_psbt: bytes, *, finalize: bool = False): # Multiple steps to sign: # - upload binary # - start signing UX @@ -242,6 +244,8 @@ class Coldcard_KeyStore(Hardware_KeyStore): hw_type = 'coldcard' device = 'Coldcard' + plugin: 'ColdcardPlugin' + def __init__(self, d): Hardware_KeyStore.__init__(self, d) # Errors and other user interaction is done through the wallet's @@ -250,39 +254,22 @@ class Coldcard_KeyStore(Hardware_KeyStore): self.force_watching_only = False self.ux_busy = False - # for multisig I need to know what wallet this keystore is part of - # will be set by link_wallet - self.my_wallet = None - - # Seems like only the derivation path and resulting **derived** xpub is stored in - # the wallet file... however, we need to know at least the fingerprint of the master - # xpub to verify against MiTM, and also so we can put the right value into the subkey paths - # of PSBT files that might be generated offline. - # - save the fingerprint of the master xpub, as "xfp" - # - it's a LE32 int, but hex BE32 is more natural way to view it + # we need to know at least the fingerprint of the master xpub to verify against MiTM # - device reports these value during encryption setup process # - full xpub value now optional lab = d['label'] - if hasattr(lab, 'xfp'): - # initial setup - self.ckcc_xfp = lab.xfp - self.ckcc_xpub = getattr(lab, 'xpub', None) - else: - # wallet load: fatal if missing, we need them! - self.ckcc_xfp = d['ckcc_xfp'] - self.ckcc_xpub = d.get('ckcc_xpub', None) + self.ckcc_xpub = getattr(lab, 'xpub', None) or d.get('ckcc_xpub', None) def dump(self): # our additions to the stored data about keystore -- only during creation? d = Hardware_KeyStore.dump(self) - - d['ckcc_xfp'] = self.ckcc_xfp d['ckcc_xpub'] = self.ckcc_xpub - return d - def get_derivation(self): - return self.derivation + def get_xfp_int(self) -> int: + xfp = self.get_root_fingerprint() + assert xfp is not None + return xfp_int_from_xfp_bytes(bfh(xfp)) def get_client(self): # called when user tries to do something like view address, sign somthing. @@ -290,7 +277,8 @@ class Coldcard_KeyStore(Hardware_KeyStore): # - will fail if indicated device can't produce the xpub (at derivation) expected rv = self.plugin.get_client(self) if rv: - rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub) + xfp_int = self.get_xfp_int() + rv.verify_connection(xfp_int, self.ckcc_xpub) return rv @@ -332,7 +320,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): return b'' client = self.get_client() - path = self.get_derivation() + ("/%d/%d" % sequence) + path = self.get_derivation_prefix() + ("/%d/%d" % sequence) try: cl = self.get_client() try: @@ -372,28 +360,23 @@ class Coldcard_KeyStore(Hardware_KeyStore): return b'' @wrap_busy - def sign_transaction(self, tx: Transaction, password): - # Build a PSBT in memory, upload it for signing. + def sign_transaction(self, tx, password): + # Upload PSBT for signing. # - we can also work offline (without paired device present) if tx.is_complete(): return - assert self.my_wallet, "Not clear which wallet associated with this Coldcard" - client = self.get_client() - assert client.dev.master_fingerprint == self.ckcc_xfp + assert client.dev.master_fingerprint == self.get_xfp_int() - # makes PSBT required - raw_psbt = build_psbt(tx, self.my_wallet) - - cc_finalize = not (type(self.my_wallet) is Multisig_Wallet) + raw_psbt = tx.serialize_as_bytes() try: try: self.handler.show_message("Authorize Transaction...") - client.sign_transaction_start(raw_psbt, cc_finalize) + client.sign_transaction_start(raw_psbt) while 1: # How to kill some time, without locking UI? @@ -420,18 +403,11 @@ class Coldcard_KeyStore(Hardware_KeyStore): self.give_error(e, True) return - if cc_finalize: - # We trust the coldcard to re-serialize final transaction ready to go - tx.update(bh2u(raw_resp)) - else: - # apply partial signatures back into txn - psbt = BasicPSBT() - psbt.parse(raw_resp, client.label()) - - merge_sigs_from_psbt(tx, psbt) - - # caller's logic looks at tx now and if it's sufficiently signed, - # will send it if that's the user's intent. + tx2 = PartialTransaction.from_raw_psbt(raw_resp) + # apply partial signatures back into txn + tx.combine_with_other_psbt(tx2) + # caller's logic looks at tx now and if it's sufficiently signed, + # will send it if that's the user's intent. @staticmethod def _encode_txin_type(txin_type): @@ -447,7 +423,7 @@ class Coldcard_KeyStore(Hardware_KeyStore): @wrap_busy def show_address(self, sequence, txin_type): client = self.get_client() - address_path = self.get_derivation()[2:] + "/%d/%d"%sequence + address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence addr_fmt = self._encode_txin_type(txin_type) try: try: @@ -573,7 +549,7 @@ class ColdcardPlugin(HW_PluginBase): xpub = client.get_xpub(derivation, xtype) return xpub - def get_client(self, keystore, force_pair=True): + def get_client(self, keystore, force_pair=True) -> 'CKCCClient': # Acquire a connection to the hardware device (via USB) devmgr = self.device_manager() handler = keystore.handler @@ -586,9 +562,10 @@ class ColdcardPlugin(HW_PluginBase): return client @staticmethod - def export_ms_wallet(wallet, fp, name): + def export_ms_wallet(wallet: Multisig_Wallet, fp, name): # Build the text file Coldcard needs to understand the multisig wallet # it is participating in. All involved Coldcards can share same file. + assert isinstance(wallet, Multisig_Wallet) print('# Exported from Electrum', file=fp) print(f'Name: {name:.20s}', file=fp) @@ -597,12 +574,12 @@ class ColdcardPlugin(HW_PluginBase): xpubs = [] derivs = set() - for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): - xfp = xfp_for_keystore(ks) - dd = getattr(ks, 'derivation', 'm') - - xpubs.append( (xfp2str(xfp), xp, dd) ) - derivs.add(dd) + for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): + fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[], only_der_suffix=False) + fp_hex = fp_bytes.hex().upper() + der_prefix_str = bip32.convert_bip32_intpath_to_strpath(der_full) + xpubs.append( (fp_hex, xpub, der_prefix_str) ) + derivs.add(der_prefix_str) # Derivation doesn't matter too much to the Coldcard, since it # uses key path data from PSBT or USB request as needed. However, @@ -613,14 +590,14 @@ class ColdcardPlugin(HW_PluginBase): print('', file=fp) assert len(xpubs) == wallet.n - for xfp, xp, dd in xpubs: + for xfp, xpub, der_prefix in xpubs: if derivs: # show as a comment if unclear - print(f'# derivation: {dd}', file=fp) + print(f'# derivation: {der_prefix}', file=fp) - print(f'{xfp}: {xp}\n', file=fp) + print(f'{xfp}: {xpub}\n', file=fp) - def show_address(self, wallet, address, keystore=None): + def show_address(self, wallet, address, keystore: 'Coldcard_KeyStore' = None): if keystore is None: keystore = wallet.get_keystore() if not self.show_address_helper(wallet, address, keystore): @@ -633,50 +610,36 @@ class ColdcardPlugin(HW_PluginBase): sequence = wallet.get_address_index(address) keystore.show_address(sequence, txin_type) elif type(wallet) is Multisig_Wallet: + assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE # More involved for P2SH/P2WSH addresses: need M, and all public keys, and their # derivation paths. Must construct script, and track fingerprints+paths for # all those keys - pubkeys = wallet.get_public_keys(address) - - xfps = [] - for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): - path = "%s/%d/%d" % (getattr(ks, 'derivation', 'm'), - *wallet.get_address_index(address)) - - # need master XFP for each co-signers - ks_xfp = xfp_for_keystore(ks) - xfps.append(unpacked_xfp_path(ks_xfp, path)) + pubkey_deriv_info = wallet.get_public_keys_with_deriv_info(address) + pubkeys = sorted([pk for pk in list(pubkey_deriv_info)]) + xfp_paths = [] + for pubkey_hex in pubkey_deriv_info: + ks, der_suffix = pubkey_deriv_info[pubkey_hex] + fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, only_der_suffix=False) + xfp_int = xfp_int_from_xfp_bytes(fp_bytes) + xfp_paths.append([xfp_int] + list(der_full)) - # put into BIP45 (sorted) order - pkx = list(sorted(zip(pubkeys, xfps))) + script = bfh(wallet.pubkeys_to_scriptcode(pubkeys)) - script = bfh(multisig_script([pk for pk,xfp in pkx], wallet.m)) - - keystore.show_p2sh_address(wallet.m, script, [xfp for pk,xfp in pkx], txin_type) + keystore.show_p2sh_address(wallet.m, script, xfp_paths, txin_type) else: keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) return - @classmethod - def link_wallet(cls, wallet): - # PROBLEM: wallet.sign_transaction() does not pass in the wallet to the individual - # keystores, and we need to know about our co-signers at that time. - # FIXME the keystore needs a reference to the wallet object because - # it constructs a PSBT from an electrum tx object inside keystore.sign_transaction. - # instead keystore.sign_transaction's API should be changed such that its input - # *is* a PSBT and not an electrum tx object - for ks in wallet.get_keystores(): - if type(ks) == Coldcard_KeyStore: - if not ks.my_wallet: - ks.my_wallet = wallet - - @hook - def load_wallet(self, wallet, window): - # make sure hook in superclass also runs: - if hasattr(super(), 'load_wallet'): - super().load_wallet(wallet, window) - self.link_wallet(wallet) + +def xfp_int_from_xfp_bytes(fp_bytes: bytes) -> int: + return int.from_bytes(fp_bytes, byteorder="little", signed=False) + + +def xfp2str(xfp: int) -> str: + # 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 struct.pack('<I', xfp).hex().lower() # EOF diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py @@ -1,25 +1,22 @@ import time, os from functools import partial +import copy from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout -from PyQt5.QtWidgets import QFileDialog + +from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons +from electrum.gui.qt.transaction_dialog import TxDialog from electrum.i18n import _ from electrum.plugin import hook -from electrum.wallet import Standard_Wallet, Multisig_Wallet -from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons -from electrum.transaction import Transaction +from electrum.wallet import Multisig_Wallet +from electrum.transaction import PartialTransaction from .coldcard import ColdcardPlugin, xfp2str from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available -from binascii import a2b_hex -from base64 import b64encode, b64decode - -from .basic_psbt import BasicPSBT -from .build_psbt import build_psbt, merge_sigs_from_psbt, recover_tx_from_psbt CC_DEBUG = False @@ -73,135 +70,29 @@ class Plugin(ColdcardPlugin, QtPluginBase): ColdcardPlugin.export_ms_wallet(wallet, f, basename) main_window.show_message(_("Wallet setup file exported successfully")) - @only_hook_if_libraries_available @hook - def transaction_dialog(self, dia): - # see gui/qt/transaction_dialog.py - + def transaction_dialog(self, dia: TxDialog): # if not a Coldcard wallet, hide feature if not any(type(ks) == self.keystore_class for ks in dia.wallet.get_keystores()): return - # - add a new button, near "export" - btn = QPushButton(_("Save PSBT")) - btn.clicked.connect(lambda unused: self.export_psbt(dia)) - if dia.tx.is_complete(): - # but disable it for signed transactions (nothing to do if already signed) - btn.setDisabled(True) - - dia.sharing_buttons.append(btn) - - def export_psbt(self, dia): - # Called from hook in transaction dialog - tx = dia.tx - - if tx.is_complete(): - # if they sign while dialog is open, it can transition from unsigned to signed, - # which we don't support here, so do nothing - return - - # convert to PSBT - build_psbt(tx, dia.wallet) + def gettx_for_coldcard_export() -> PartialTransaction: + if not isinstance(dia.tx, PartialTransaction): + raise Exception("Can only export partial transactions for coinjoins.") + tx = copy.deepcopy(dia.tx) + tx.add_info_from_wallet(dia.wallet, include_xpubs_and_full_paths=True) + return tx - name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt'))\ - .replace(' ', '-').replace('.json', '') - fileName = dia.main_window.getSaveFileName(_("Select where to save the PSBT file"), - name, "*.psbt") - if fileName: - with open(fileName, "wb+") as f: - f.write(tx.raw_psbt) - dia.show_message(_("Transaction exported successfully")) - dia.saved = True + # add a new "export" option + if isinstance(dia.tx, PartialTransaction): + export_submenu = dia.export_actions_menu.addMenu(_("For {}; include xpubs").format(self.device)) + dia.add_export_actions_to_menu(export_submenu, gettx=gettx_for_coldcard_export) def show_settings_dialog(self, window, keystore): # When they click on the icon for CC we come here. # - doesn't matter if device not connected, continue CKCCSettingsDialog(window, self, keystore).exec_() - @hook - def init_menubar_tools(self, main_window, tools_menu): - # add some PSBT-related tools to the "Load Transaction" menu. - rt = main_window.raw_transaction_menu - wallet = main_window.wallet - rt.addAction(_("From &PSBT File or Files"), lambda: self.psbt_combiner(main_window, wallet)) - - def psbt_combiner(self, window, wallet): - title = _("Select the PSBT file to load or PSBT files to combine") - directory = '' - fnames, __ = QFileDialog.getOpenFileNames(window, title, directory, "PSBT Files (*.psbt)") - - psbts = [] - for fn in fnames: - try: - with open(fn, "rb") as f: - raw = f.read() - - psbt = BasicPSBT() - psbt.parse(raw, fn) - - psbts.append(psbt) - except (AssertionError, ValueError, IOError, os.error) as reason: - window.show_critical(_("Electrum was unable to open your PSBT file") + "\n" + str(reason), title=_("Unable to read file")) - return - - warn = [] - if not psbts: return # user picked nothing - - # Consistency checks and warnings. - try: - first = psbts[0] - for p in psbts: - fn = os.path.split(p.filename)[1] - - assert (p.txn == first.txn), \ - "All must relate to the same unsigned transaction." - - for idx, inp in enumerate(p.inputs): - if not inp.part_sigs: - warn.append(fn + ':\n ' + _("No partial signatures found for input #%d") % idx) - - assert first.inputs[idx].redeem_script == inp.redeem_script, "Mismatched redeem scripts" - assert first.inputs[idx].witness_script == inp.witness_script, "Mismatched witness" - - except AssertionError as exc: - # Fatal errors stop here. - window.show_critical(str(exc), - title=_("Unable to combine PSBT files, check: ")+p.filename) - return - - if warn: - # Lots of potential warnings... - window.show_warning('\n\n'.join(warn), title=_("PSBT warnings")) - - # Construct an Electrum transaction object from data in first PSBT file. - try: - tx = recover_tx_from_psbt(first, wallet) - except BaseException as exc: - if CC_DEBUG: - from PyQt5.QtCore import pyqtRemoveInputHook; pyqtRemoveInputHook() - import pdb; pdb.post_mortem() - window.show_critical(str(exc), title=_("Unable to understand PSBT file")) - return - - # Combine the signatures from all the PSBTS (may do nothing if unsigned PSBTs) - for p in psbts: - try: - merge_sigs_from_psbt(tx, p) - except BaseException as exc: - if CC_DEBUG: - from PyQt5.QtCore import pyqtRemoveInputHook; pyqtRemoveInputHook() - import pdb; pdb.post_mortem() - window.show_critical("Unable to merge signatures: " + str(exc), - title=_("Unable to combine PSBT file: ") + p.filename) - return - - # Display result, might not be complete yet, but hopefully it's ready to transmit! - if len(psbts) == 1: - desc = _("From PSBT file: ") + fn - else: - desc = _("Combined from %d PSBT files") % len(psbts) - - window.show_transaction(tx, tx_desc=desc) class Coldcard_Handler(QtHandlerBase): setup_signal = pyqtSignal() @@ -307,7 +198,7 @@ class CKCCSettingsDialog(WindowModalDialog): def show_placeholders(self, unclear_arg): # device missing, so hide lots of detail. - self.xfp.setText('<tt>%s' % xfp2str(self.keystore.ckcc_xfp)) + self.xfp.setText('<tt>%s' % self.keystore.get_root_fingerprint()) self.serial.setText('(not connected)') self.fw_version.setText('') self.fw_built.setText('') diff --git a/electrum/plugins/cosigner_pool/qt.py b/electrum/plugins/cosigner_pool/qt.py @@ -25,23 +25,25 @@ import time from xmlrpc.client import ServerProxy +from typing import TYPE_CHECKING, Union, List, Tuple from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import QPushButton from electrum import util, keystore, ecc, crypto from electrum import transaction +from electrum.transaction import Transaction, PartialTransaction, tx_from_any from electrum.bip32 import BIP32Node from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.wallet import Multisig_Wallet from electrum.util import bh2u, bfh -from electrum.gui.qt.transaction_dialog import show_transaction +from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog from electrum.gui.qt.util import WaitingDialog -import sys -import traceback +if TYPE_CHECKING: + from electrum.gui.qt.main_window import ElectrumWindow server = ServerProxy('https://cosigner.electrum.org/', allow_none=True) @@ -97,8 +99,8 @@ class Plugin(BasePlugin): self.listener = None self.obj = QReceiveSignalObject() self.obj.cosigner_receive_signal.connect(self.on_receive) - self.keys = [] - self.cosigner_list = [] + self.keys = [] # type: List[Tuple[str, str, ElectrumWindow]] + self.cosigner_list = [] # type: List[Tuple[ElectrumWindow, str, bytes, str]] @hook def init_qt(self, gui): @@ -116,10 +118,11 @@ class Plugin(BasePlugin): def is_available(self): return True - def update(self, window): + def update(self, window: 'ElectrumWindow'): wallet = window.wallet if type(wallet) != Multisig_Wallet: return + assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE if self.listener is None: self.logger.info("starting listener") self.listener = Listener(self) @@ -131,7 +134,7 @@ class Plugin(BasePlugin): self.keys = [] self.cosigner_list = [] for key, keystore in wallet.keystores.items(): - xpub = keystore.get_master_public_key() + xpub = keystore.get_master_public_key() # type: str pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True) _hash = bh2u(crypto.sha256d(pubkey)) if not keystore.is_watching_only(): @@ -142,14 +145,14 @@ class Plugin(BasePlugin): self.listener.set_keyhashes([t[1] for t in self.keys]) @hook - def transaction_dialog(self, d): + def transaction_dialog(self, d: 'TxDialog'): d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) b.clicked.connect(lambda: self.do_send(d.tx)) d.buttons.insert(0, b) self.transaction_dialog_update(d) @hook - def transaction_dialog_update(self, d): + def transaction_dialog_update(self, d: 'TxDialog'): if d.tx.is_complete() or d.wallet.can_sign(d.tx): d.cosigner_send_button.hide() return @@ -160,17 +163,14 @@ class Plugin(BasePlugin): else: d.cosigner_send_button.hide() - def cosigner_can_sign(self, tx, cosigner_xpub): - from electrum.keystore import is_xpubkey, parse_xpubkey - xpub_set = set([]) - for txin in tx.inputs(): - for x_pubkey in txin['x_pubkeys']: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - xpub_set.add(xpub) - return cosigner_xpub in xpub_set - - def do_send(self, tx): + def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool: + if not isinstance(tx, PartialTransaction): + return False + if tx.is_complete(): + return False + return cosigner_xpub in {bip32node.to_xpub() for bip32node in tx.xpubs} + + def do_send(self, tx: Union[Transaction, PartialTransaction]): def on_success(result): window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + _("Open your cosigner wallet to retrieve it.")) @@ -184,7 +184,7 @@ class Plugin(BasePlugin): if not self.cosigner_can_sign(tx, xpub): continue # construct message - raw_tx_bytes = bfh(str(tx)) + raw_tx_bytes = tx.serialize_as_bytes() public_key = ecc.ECPubkey(K) message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') # send message @@ -223,12 +223,12 @@ class Plugin(BasePlugin): return try: privkey = BIP32Node.from_xkey(xprv).eckey - message = bh2u(privkey.decrypt_message(message)) + message = privkey.decrypt_message(message) except Exception as e: self.logger.exception('') window.show_error(_('Error decrypting message') + ':\n' + repr(e)) return self.listener.clear(keyhash) - tx = transaction.Transaction(message) - show_transaction(tx, window, prompt_if_unsaved=True) + tx = tx_from_any(message) + show_transaction(tx, parent=window, prompt_if_unsaved=True) diff --git a/electrum/plugins/digitalbitbox/digitalbitbox.py b/electrum/plugins/digitalbitbox/digitalbitbox.py @@ -14,20 +14,21 @@ import re import struct import sys import time +import copy from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh, is_address) -from electrum.bip32 import BIP32Node +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation from electrum import ecc from electrum.ecc import msg_magic from electrum.wallet import Standard_Wallet from electrum import constants -from electrum.transaction import Transaction +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore from ..hw_wallet import HW_PluginBase -from electrum.util import to_string, UserCancelled, UserFacingException +from electrum.util import to_string, UserCancelled, UserFacingException, bfh from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from electrum.network import Network from electrum.logging import get_logger @@ -449,21 +450,13 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): hw_type = 'digitalbitbox' device = 'DigitalBitbox' + plugin: 'DigitalBitboxPlugin' def __init__(self, d): Hardware_KeyStore.__init__(self, d) self.force_watching_only = False self.maxInputs = 14 # maximum inputs per single sign command - - def get_derivation(self): - return str(self.derivation) - - - def is_p2pkh(self): - return self.derivation.startswith("m/44'/") - - def give_error(self, message, clear_client = False): if clear_client: self.client = None @@ -478,7 +471,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): sig = None try: message = message.encode('utf8') - inputPath = self.get_derivation() + "/%d/%d" % sequence + inputPath = self.get_derivation_prefix() + "/%d/%d" % sequence msg_hash = sha256d(msg_magic(message)) inputHash = to_hexstr(msg_hash) hasharray = [] @@ -540,58 +533,50 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): try: p2pkhTransaction = True - derivations = self.get_tx_derivations(tx) inputhasharray = [] hasharray = [] pubkeyarray = [] # Build hasharray from inputs for i, txin in enumerate(tx.inputs()): - if txin['type'] == 'coinbase': + if txin.is_coinbase(): self.give_error("Coinbase not supported") # should never happen - if txin['type'] != 'p2pkh': + if txin.script_type != 'p2pkh': p2pkhTransaction = False - for x_pubkey in txin['x_pubkeys']: - if x_pubkey in derivations: - index = derivations.get(x_pubkey) - inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1]) - inputHash = sha256d(binascii.unhexlify(tx.serialize_preimage(i))) - hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} - hasharray.append(hasharray_i) - inputhasharray.append(inputHash) - break - else: - self.give_error("No matching x_key for sign_transaction") # should never happen + my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin) + if not inputPath: + self.give_error("No matching pubkey for sign_transaction") # should never happen + inputPath = convert_bip32_intpath_to_strpath(inputPath) + inputHash = sha256d(bfh(tx.serialize_preimage(i))) + hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} + hasharray.append(hasharray_i) + inputhasharray.append(inputHash) # Build pubkeyarray from outputs - for o in tx.outputs(): - assert o.type == TYPE_ADDRESS - info = tx.output_info.get(o.address) - if info is not None: - if info.is_change: - index = info.address_index - changePath = self.get_derivation() + "/%d/%d" % index - changePubkey = self.derive_pubkey(index[0], index[1]) - pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} - pubkeyarray.append(pubkeyarray_i) + for txout in tx.outputs(): + assert txout.address + if txout.is_change: + changePubkey, changePath = self.find_my_pubkey_in_txinout(txout) + assert changePath + changePath = convert_bip32_intpath_to_strpath(changePath) + changePubkey = changePubkey.hex() + pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} + pubkeyarray.append(pubkeyarray_i) # Special serialization of the unsigned transaction for # the mobile verification app. # At the moment, verification only works for p2pkh transactions. if p2pkhTransaction: - class CustomTXSerialization(Transaction): - @classmethod - def input_script(self, txin, estimate_size=False): - if txin['type'] == 'p2pkh': - return Transaction.get_preimage_script(txin) - if txin['type'] == 'p2sh': - # Multisig verification has partial support, but is disabled. This is the - # expected serialization though, so we leave it here until we activate it. - return '00' + push_script(Transaction.get_preimage_script(txin)) - raise Exception("unsupported type %s" % txin['type']) - tx_dbb_serialized = CustomTXSerialization(tx.serialize()).serialize_to_network() + tx_copy = copy.deepcopy(tx) + # monkey-patch method of tx_copy instance to change serialization + def input_script(self, txin: PartialTxInput, *, estimate_size=False): + if txin.script_type == 'p2pkh': + return Transaction.get_preimage_script(txin) + raise Exception("unsupported type %s" % txin.script_type) + tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction) + tx_dbb_serialized = tx_copy.serialize_to_network() else: # We only need this for the signing echo / verification. tx_dbb_serialized = None @@ -656,12 +641,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): if len(dbb_signatures) != len(tx.inputs()): raise Exception("Incorrect number of transactions signed.") # Should never occur for i, txin in enumerate(tx.inputs()): - num = txin['num_sig'] - for pubkey in txin['pubkeys']: - signatures = list(filter(None, txin['signatures'])) - if len(signatures) == num: - break # txin is complete - ii = txin['pubkeys'].index(pubkey) + for pubkey_bytes in txin.pubkeys: + if txin.is_complete(): + break signed = dbb_signatures[i] if 'recid' in signed: # firmware > v2.1.1 @@ -673,20 +655,19 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore): elif 'pubkey' in signed: # firmware <= v2.1.1 pk = signed['pubkey'] - if pk != pubkey: + if pk != pubkey_bytes.hex(): continue sig_r = int(signed['sig'][:64], 16) sig_s = int(signed['sig'][64:], 16) sig = ecc.der_sig_from_r_and_s(sig_r, sig_s) sig = to_hexstr(sig) + '01' - tx.add_signature_to_txin(i, ii, sig) + tx.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_bytes.hex(), sig=sig) except UserCancelled: raise except BaseException as e: self.give_error(e, True) else: - _logger.info("Transaction is_complete {tx.is_complete()}") - tx.raw = tx.serialize() + _logger.info(f"Transaction is_complete {tx.is_complete()}") class DigitalBitboxPlugin(HW_PluginBase): @@ -760,6 +741,8 @@ class DigitalBitboxPlugin(HW_PluginBase): def get_xpub(self, device_id, derivation, xtype, wizard): if xtype not in self.SUPPORTED_XTYPES: raise ScriptTypeNotSupported(_('This type of script is not supported with {}.').format(self.device)) + if is_all_public_derivation(derivation): + raise Exception(f"The {self.device} does not reveal xpubs corresponding to non-hardened paths. (path: {derivation})") devmgr = self.device_manager() client = devmgr.client_by_id(device_id) client.handler = self.create_handler(wizard) @@ -788,11 +771,11 @@ class DigitalBitboxPlugin(HW_PluginBase): if not self.is_mobile_paired(): keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device)) return - if not keystore.is_p2pkh(): + if wallet.get_txin_type(address) != 'p2pkh': keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device)) return change, index = wallet.get_address_index(address) - keypath = '%s/%d/%d' % (keystore.derivation, change, index) + keypath = '%s/%d/%d' % (keystore.get_derivation_prefix(), change, index) xpub = self.get_client(keystore)._get_xpub(keypath) verify_request_payload = { "type": 'p2pkh', diff --git a/electrum/plugins/digitalbitbox/qt.py b/electrum/plugins/digitalbitbox/qt.py @@ -2,7 +2,7 @@ from functools import partial from electrum.i18n import _ from electrum.plugin import hook -from electrum.wallet import Standard_Wallet +from electrum.wallet import Standard_Wallet, Abstract_Wallet from ..hw_wallet.qt import QtHandlerBase, QtPluginBase from ..hw_wallet.plugin import only_hook_if_libraries_available @@ -18,7 +18,7 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase): @only_hook_if_libraries_available @hook - def receive_menu(self, menu, addrs, wallet): + def receive_menu(self, menu, addrs, wallet: Abstract_Wallet): if type(wallet) is not Standard_Wallet: return @@ -29,12 +29,12 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase): if not self.is_mobile_paired(): return - if not keystore.is_p2pkh(): - return - if len(addrs) == 1: + addr = addrs[0] + if wallet.get_txin_type(addr) != 'p2pkh': + return def show_address(): - keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore)) + keystore.thread.add(partial(self.show_address, wallet, addr, keystore)) menu.addAction(_("Show on {}").format(self.device), show_address) diff --git a/electrum/plugins/greenaddress_instant/qt.py b/electrum/plugins/greenaddress_instant/qt.py @@ -36,6 +36,7 @@ from electrum.network import Network if TYPE_CHECKING: from aiohttp import ClientResponse + from electrum.gui.qt.transaction_dialog import TxDialog class Plugin(BasePlugin): @@ -43,13 +44,13 @@ class Plugin(BasePlugin): button_label = _("Verify GA instant") @hook - def transaction_dialog(self, d): + def transaction_dialog(self, d: 'TxDialog'): d.verify_button = QPushButton(self.button_label) d.verify_button.clicked.connect(lambda: self.do_verify(d)) d.buttons.insert(0, d.verify_button) self.transaction_dialog_update(d) - def get_my_addr(self, d): + def get_my_addr(self, d: 'TxDialog'): """Returns the address for given tx which can be used to request instant confirmation verification from GreenAddress""" for o in d.tx.outputs(): @@ -58,13 +59,13 @@ class Plugin(BasePlugin): return None @hook - def transaction_dialog_update(self, d): + def transaction_dialog_update(self, d: 'TxDialog'): if d.tx.is_complete() and self.get_my_addr(d): d.verify_button.show() else: d.verify_button.hide() - def do_verify(self, d): + def do_verify(self, d: 'TxDialog'): tx = d.tx wallet = d.wallet window = d.main_window diff --git a/electrum/plugins/hw_wallet/plugin.py b/electrum/plugins/hw_wallet/plugin.py @@ -24,11 +24,18 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +from typing import TYPE_CHECKING, Dict, List, Union, Tuple, Sequence + from electrum.plugin import BasePlugin, hook from electrum.i18n import _ from electrum.bitcoin import is_address, TYPE_SCRIPT, opcodes from electrum.util import bfh, versiontuple, UserFacingException -from electrum.transaction import TxOutput, Transaction +from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.bip32 import BIP32Node + +if TYPE_CHECKING: + from electrum.wallet import Abstract_Wallet + from electrum.keystore import Hardware_KeyStore class HW_PluginBase(BasePlugin): @@ -65,7 +72,10 @@ class HW_PluginBase(BasePlugin): """ raise NotImplementedError() - def show_address(self, wallet, address, keystore=None): + def get_client(self, keystore: 'Hardware_KeyStore', force_pair: bool = True): + raise NotImplementedError() + + def show_address(self, wallet: 'Abstract_Wallet', address, keystore: 'Hardware_KeyStore' = None): pass # implemented in child classes def show_address_helper(self, wallet, address, keystore=None): @@ -132,20 +142,12 @@ class HW_PluginBase(BasePlugin): return self._ignore_outdated_fw -def is_any_tx_output_on_change_branch(tx: Transaction) -> bool: - if not tx.output_info: - return False - for o in tx.outputs(): - info = tx.output_info.get(o.address) - if info is not None: - return info.is_change - return False +def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool: + return any([txout.is_change for txout in tx.outputs()]) def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: - if output.type != TYPE_SCRIPT: - raise Exception("Unexpected output type: {}".format(output.type)) - script = bfh(output.address) + script = output.scriptpubkey if not (script[0] == opcodes.OP_RETURN and script[1] == len(script) - 2 and script[1] <= 75): raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported.")) @@ -154,6 +156,25 @@ def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: return script[2:] +def get_xpubs_and_der_suffixes_from_txinout(tx: PartialTransaction, + txinout: Union[PartialTxInput, PartialTxOutput]) \ + -> List[Tuple[str, List[int]]]: + xfp_to_xpub_map = {xfp: bip32node for bip32node, (xfp, path) + in tx.xpubs.items()} # type: Dict[bytes, BIP32Node] + xfps = [txinout.bip32_paths[pubkey][0] for pubkey in txinout.pubkeys] + try: + xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps] + except KeyError as e: + raise Exception(f"Partial transaction is missing global xpub for " + f"fingerprint ({str(e)}) in input/output") from e + xpubs_and_deriv_suffixes = [] + for bip32node, pubkey in zip(xpubs, txinout.pubkeys): + xfp, path = txinout.bip32_paths[pubkey] + der_suffix = list(path)[bip32node.depth:] + xpubs_and_deriv_suffixes.append((bip32node.to_xpub(), der_suffix)) + return xpubs_and_deriv_suffixes + + def only_hook_if_libraries_available(func): # note: this decorator must wrap @hook, not the other way around, # as 'hook' uses the name of the function it wraps diff --git a/electrum/plugins/keepkey/keepkey.py b/electrum/plugins/keepkey/keepkey.py @@ -1,19 +1,23 @@ from binascii import hexlify, unhexlify import traceback import sys +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING from electrum.util import bfh, bh2u, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bip32 import BIP32Node from electrum import constants from electrum.i18n import _ -from electrum.transaction import deserialize, Transaction -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data +from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, + get_xpubs_and_der_suffixes_from_txinout) +if TYPE_CHECKING: + from .client import KeepKeyClient # TREZOR initialization methods TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) @@ -23,8 +27,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): hw_type = 'keepkey' device = 'KeepKey' - def get_derivation(self): - return self.derivation + plugin: 'KeepKeyPlugin' def get_client(self, force_pair=True): return self.plugin.get_client(self, force_pair) @@ -34,7 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore): def sign_message(self, sequence, message, password): client = self.get_client() - address_path = self.get_derivation() + "/%d/%d"%sequence + address_path = self.get_derivation_prefix() + "/%d/%d"%sequence address_n = client.expand_path(address_path) msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) return msg_sig.signature @@ -44,22 +47,13 @@ class KeepKey_KeyStore(Hardware_KeyStore): return # previous transactions used as inputs prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} for txin in tx.inputs(): - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - tx_hash = txin['prevout_hash'] - if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - prev_tx[tx_hash] = txin['prev_tx'] - for x_pubkey in x_pubkeys: - if not is_xpubkey(x_pubkey): - continue - xpub, s = parse_xpubkey(x_pubkey) - if xpub == self.get_master_public_key(): - xpub_path[xpub] = self.get_derivation() + tx_hash = txin.prevout.txid.hex() + if txin.utxo is None and not Transaction.is_segwit_input(txin): + raise UserFacingException(_('Missing previous tx for legacy input.')) + prev_tx[tx_hash] = txin.utxo - self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) + self.plugin.sign_transaction(self, tx, prev_tx) class KeepKeyPlugin(HW_PluginBase): @@ -164,7 +158,7 @@ class KeepKeyPlugin(HW_PluginBase): return client - def get_client(self, keystore, force_pair=True): + def get_client(self, keystore, force_pair=True) -> Optional['KeepKeyClient']: devmgr = self.device_manager() handler = keystore.handler with devmgr.hid_lock: @@ -306,12 +300,11 @@ class KeepKeyPlugin(HW_PluginBase): return self.types.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) - def sign_transaction(self, keystore, tx, prev_tx, xpub_path): + def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): self.prev_tx = prev_tx - self.xpub_path = xpub_path client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True) - outputs = self.tx_outputs(keystore.get_derivation(), tx) + inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore) + outputs = self.tx_outputs(tx, keystore=keystore) signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] signatures = [(bh2u(x) + '01') for x in signatures] @@ -326,137 +319,118 @@ class KeepKeyPlugin(HW_PluginBase): if not client.atleast_version(1, 3): keystore.handler.show_error(_("Your device firmware is too old")) return - change, index = wallet.get_address_index(address) - derivation = keystore.derivation - address_path = "%s/%d/%d"%(derivation, change, index) + deriv_suffix = wallet.get_address_index(address) + derivation = keystore.get_derivation_prefix() + address_path = "%s/%d/%d"%(derivation, *deriv_suffix) address_n = client.expand_path(address_path) + script_type = self.get_keepkey_input_script_type(wallet.txin_type) + + # prepare multisig, if available: xpubs = wallet.get_master_public_keys() - if len(xpubs) == 1: - script_type = self.get_keepkey_input_script_type(wallet.txin_type) - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) - else: - def f(xpub): - return self._make_node_path(xpub, [change, index]) + if len(xpubs) > 1: pubkeys = wallet.get_public_keys(address) # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - pubkeys = list(map(f, sorted_xpubs)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * wallet.n, - m=wallet.m, - ) - script_type = self.get_keepkey_input_script_type(wallet.txin_type) - client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) - - def tx_inputs(self, tx, for_sig=False): + sorted_pairs = sorted(zip(pubkeys, xpubs)) + multisig = self._make_multisig( + wallet.m, + [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs]) + else: + multisig = None + + client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) + + def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'KeepKey_KeyStore' = None): inputs = [] for txin in tx.inputs(): txinputtype = self.types.TxInputType() - if txin['type'] == 'coinbase': + if txin.is_coinbase(): prev_hash = b"\x00"*32 prev_index = 0xffffffff # signed int -1 else: if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = parse_xpubkey(x_pubkey) - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - txinputtype.script_type = self.get_keepkey_input_script_type(txin['type']) + assert isinstance(tx, PartialTransaction) + assert isinstance(txin, PartialTxInput) + assert keystore + if len(txin.pubkeys) > 1: + xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) + multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) else: - def f(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - return self._make_node_path(xpub, s) - pubkeys = list(map(f, x_pubkeys)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')), - m=txin.get('num_sig'), - ) - script_type = self.get_keepkey_input_script_type(txin['type']) - txinputtype = self.types.TxInputType( - script_type=script_type, - multisig=multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype.address_n.extend(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - if 'value' in txin: - txinputtype.amount = txin['value'] + multisig = None + script_type = self.get_keepkey_input_script_type(txin.script_type) + txinputtype = self.types.TxInputType( + script_type=script_type, + multisig=multisig) + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) + if full_path: + txinputtype.address_n.extend(full_path) + + prev_hash = txin.prevout.txid + prev_index = txin.prevout.out_idx + + if txin.value_sats() is not None: + txinputtype.amount = txin.value_sats() txinputtype.prev_hash = prev_hash txinputtype.prev_index = prev_index - if txin.get('scriptSig') is not None: - script_sig = bfh(txin['scriptSig']) - txinputtype.script_sig = script_sig + if txin.script_sig is not None: + txinputtype.script_sig = txin.script_sig - txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) + txinputtype.sequence = txin.nsequence inputs.append(txinputtype) return inputs - def tx_outputs(self, derivation, tx: Transaction): + def _make_multisig(self, m, xpubs): + if len(xpubs) == 1: + return None + pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] + return self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * len(pubkeys), + m=m) + + def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'): def create_output_by_derivation(): - script_type = self.get_keepkey_output_script_type(info.script_type) - if len(xpubs) == 1: - address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) - txoutputtype = self.types.TxOutputType( - amount=amount, - script_type=script_type, - address_n=address_n, - ) + script_type = self.get_keepkey_output_script_type(txout.script_type) + if len(txout.pubkeys) > 1: + xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) + multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) else: - address_n = self.client_class.expand_path("/%d/%d" % index) - pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs] - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * len(pubkeys), - m=m) - txoutputtype = self.types.TxOutputType( - multisig=multisig, - amount=amount, - address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), - script_type=script_type) + multisig = None + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) + assert full_path + txoutputtype = self.types.TxOutputType( + multisig=multisig, + amount=txout.value, + address_n=full_path, + script_type=script_type) return txoutputtype def create_output_by_address(): txoutputtype = self.types.TxOutputType() - txoutputtype.amount = amount - if _type == TYPE_SCRIPT: - txoutputtype.script_type = self.types.PAYTOOPRETURN - txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) - elif _type == TYPE_ADDRESS: + txoutputtype.amount = txout.value + if address: txoutputtype.script_type = self.types.PAYTOADDRESS txoutputtype.address = address + else: + txoutputtype.script_type = self.types.PAYTOOPRETURN + txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout) return txoutputtype outputs = [] has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for o in tx.outputs(): - _type, address, amount = o.type, o.address, o.value + for txout in tx.outputs(): + address = txout.address use_create_by_derivation = False - info = tx.output_info.get(address) - if info is not None and not has_change: - index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig + if txout.is_mine and not has_change: # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed - if info.is_change == any_output_on_change_branch: + if txout.is_change == any_output_on_change_branch: use_create_by_derivation = True has_change = True @@ -468,20 +442,20 @@ class KeepKeyPlugin(HW_PluginBase): return outputs - def electrum_tx_to_txtype(self, tx): + def electrum_tx_to_txtype(self, tx: Optional[Transaction]): t = self.types.TransactionType() if tx is None: # probably for segwit input and we don't need this prev txn return t - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] + tx.deserialize() + t.version = tx.version + t.lock_time = tx.locktime inputs = self.tx_inputs(tx) t.inputs.extend(inputs) - for vout in d['outputs']: + for out in tx.outputs(): o = t.bin_outputs.add() - o.amount = vout['value'] - o.script_pubkey = bfh(vout['scriptPubKey']) + o.amount = out.value + o.script_pubkey = out.scriptpubkey return t # This function is called from the TREZOR libraries (via tx_api) diff --git a/electrum/plugins/ledger/ledger.py b/electrum/plugins/ledger/ledger.py @@ -4,11 +4,13 @@ import sys import traceback from electrum import ecc -from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int, is_segwit_script_type -from electrum.bip32 import BIP32Node +from electrum import bip32 +from electrum.crypto import hash_160 +from electrum.bitcoin import int_to_hex, var_int, is_segwit_script_type +from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath from electrum.i18n import _ from electrum.keystore import Hardware_KeyStore -from electrum.transaction import Transaction +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput from electrum.wallet import Standard_Wallet from electrum.util import bfh, bh2u, versiontuple, UserFacingException from electrum.base_wizard import ScriptTypeNotSupported @@ -78,9 +80,6 @@ class Ledger_Client(): def label(self): return "" - def i4b(self, x): - return pack('>I', x) - def has_usable_connection_with_device(self): try: self.dongleObject.getFirmwareVersion() @@ -101,29 +100,27 @@ class Ledger_Client(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) if xtype in ['p2wpkh-p2sh', 'p2wsh-p2sh'] and not self.supports_segwit(): raise UserFacingException(MSG_NEEDS_FW_UPDATE_SEGWIT) - splitPath = bip32_path.split('/') - if splitPath[0] == 'm': - splitPath = splitPath[1:] - bip32_path = bip32_path[2:] - fingerprint = 0 - if len(splitPath) > 1: - prevPath = "/".join(splitPath[0:len(splitPath) - 1]) + bip32_path = bip32.normalize_bip32_derivation(bip32_path) + bip32_intpath = bip32.convert_bip32_path_to_list_of_uint32(bip32_path) + bip32_path = bip32_path[2:] # cut off "m/" + if len(bip32_intpath) >= 1: + prevPath = bip32.convert_bip32_intpath_to_strpath(bip32_intpath[:-1])[2:] nodeData = self.dongleObject.getWalletPublicKey(prevPath) publicKey = compress_public_key(nodeData['publicKey']) - h = hashlib.new('ripemd160') - h.update(hashlib.sha256(publicKey).digest()) - fingerprint = unpack(">I", h.digest()[0:4])[0] + fingerprint_bytes = hash_160(publicKey)[0:4] + childnum_bytes = bip32_intpath[-1].to_bytes(length=4, byteorder="big") + else: + fingerprint_bytes = bytes(4) + childnum_bytes = bytes(4) nodeData = self.dongleObject.getWalletPublicKey(bip32_path) publicKey = compress_public_key(nodeData['publicKey']) - depth = len(splitPath) - lastChild = splitPath[len(splitPath) - 1].split('\'') - childnum = int(lastChild[0]) if len(lastChild) == 1 else 0x80000000 | int(lastChild[0]) + depth = len(bip32_intpath) return BIP32Node(xtype=xtype, eckey=ecc.ECPubkey(publicKey), chaincode=nodeData['chainCode'], depth=depth, - fingerprint=self.i4b(fingerprint), - child_number=self.i4b(childnum)).to_xpub() + fingerprint=fingerprint_bytes, + child_number=childnum_bytes).to_xpub() def has_detached_pin_support(self, client): try: @@ -217,6 +214,8 @@ class Ledger_KeyStore(Hardware_KeyStore): hw_type = 'ledger' device = 'Ledger' + plugin: 'LedgerPlugin' + def __init__(self, d): Hardware_KeyStore.__init__(self, d) # Errors and other user interaction is done through the wallet's @@ -231,9 +230,6 @@ class Ledger_KeyStore(Hardware_KeyStore): obj['cfg'] = self.cfg return obj - def get_derivation(self): - return self.derivation - def get_client(self): return self.plugin.get_client(self).dongleObject @@ -260,13 +256,6 @@ class Ledger_KeyStore(Hardware_KeyStore): self.signing = False return wrapper - def address_id_stripped(self, address): - # Strip the leading "m/" - change, index = self.get_address_index(address) - derivation = self.derivation - address_path = "%s/%d/%d"%(derivation, change, index) - return address_path[2:] - def decrypt_message(self, pubkey, message, password): raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) @@ -277,7 +266,7 @@ class Ledger_KeyStore(Hardware_KeyStore): message_hash = hashlib.sha256(message).hexdigest().upper() # prompt for the PIN before displaying the dialog if necessary client = self.get_client() - address_path = self.get_derivation()[2:] + "/%d/%d"%sequence + address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash) try: info = self.get_client().signMessagePrepare(address_path, message) @@ -318,16 +307,13 @@ class Ledger_KeyStore(Hardware_KeyStore): @test_pin_unlocked @set_and_unset_signing - def sign_transaction(self, tx: Transaction, password): + def sign_transaction(self, tx, password): if tx.is_complete(): return - client = self.get_client() inputs = [] inputsPaths = [] - pubKeys = [] chipInputs = [] redeemScripts = [] - signatures = [] changePath = "" output = None p2shTransaction = False @@ -336,60 +322,52 @@ class Ledger_KeyStore(Hardware_KeyStore): self.get_client() # prompt for the PIN before displaying the dialog if necessary # Fetch inputs of the transaction to sign - derivations = self.get_tx_derivations(tx) for txin in tx.inputs(): - if txin['type'] == 'coinbase': + if txin.is_coinbase(): self.give_error("Coinbase not supported") # should never happen - if txin['type'] in ['p2sh']: + if txin.script_type in ['p2sh']: p2shTransaction = True - if txin['type'] in ['p2wpkh-p2sh', 'p2wsh-p2sh']: + if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']: if not self.get_client_electrum().supports_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True - if txin['type'] in ['p2wpkh', 'p2wsh']: + if txin.script_type in ['p2wpkh', 'p2wsh']: if not self.get_client_electrum().supports_native_segwit(): self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) segwitTransaction = True - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - for i, x_pubkey in enumerate(x_pubkeys): - if x_pubkey in derivations: - signingPos = i - s = derivations.get(x_pubkey) - hwAddress = "%s/%d/%d" % (self.get_derivation()[2:], s[0], s[1]) - break - else: - self.give_error("No matching x_key for sign_transaction") # should never happen + my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin) + if not full_path: + self.give_error("No matching pubkey for sign_transaction") # should never happen + full_path = convert_bip32_intpath_to_strpath(full_path)[2:] redeemScript = Transaction.get_preimage_script(txin) - txin_prev_tx = txin.get('prev_tx') + txin_prev_tx = txin.utxo if txin_prev_tx is None and not Transaction.is_segwit_input(txin): - raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None + raise UserFacingException(_('Missing previous tx for legacy input.')) + txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None inputs.append([txin_prev_tx_raw, - txin['prevout_n'], + txin.prevout.out_idx, redeemScript, - txin['prevout_hash'], - signingPos, - txin.get('sequence', 0xffffffff - 1), - txin.get('value')]) - inputsPaths.append(hwAddress) - pubKeys.append(pubkeys) + txin.prevout.txid.hex(), + my_pubkey, + txin.nsequence, + txin.value_sats()]) + inputsPaths.append(full_path) # Sanity check if p2shTransaction: for txin in tx.inputs(): - if txin['type'] != 'p2sh': + if txin.script_type != 'p2sh': self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen txOutput = var_int(len(tx.outputs())) for o in tx.outputs(): - output_type, addr, amount = o.type, o.address, o.value - txOutput += int_to_hex(amount, 8) - script = tx.pay_script(output_type, addr) + txOutput += int_to_hex(o.value, 8) + script = o.scriptpubkey.hex() txOutput += var_int(len(script)//2) txOutput += script txOutput = bfh(txOutput) @@ -403,21 +381,21 @@ class Ledger_KeyStore(Hardware_KeyStore): self.give_error("Transaction with more than 2 outputs not supported") has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for o in tx.outputs(): - assert o.type == TYPE_ADDRESS - info = tx.output_info.get(o.address) - if (info is not None) and len(tx.outputs()) > 1 \ + for txout in tx.outputs(): + assert txout.address + if txout.is_mine and len(tx.outputs()) > 1 \ and not has_change: - index = info.address_index # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed - if info.is_change == any_output_on_change_branch: - changePath = self.get_derivation()[2:] + "/%d/%d"%index + if txout.is_change == any_output_on_change_branch: + my_pubkey, changePath = self.find_my_pubkey_in_txinout(txout) + assert changePath + changePath = convert_bip32_intpath_to_strpath(changePath)[2:] has_change = True else: - output = o.address + output = txout.address else: - output = o.address + output = txout.address self.handler.show_message(_("Confirm Transaction on your Ledger device...")) try: @@ -467,7 +445,10 @@ class Ledger_KeyStore(Hardware_KeyStore): singleInput, redeemScripts[inputIndex], version=tx.version) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ - signatures.append(inputSignature) + my_pubkey = inputs[inputIndex][4] + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey.hex(), + sig=inputSignature.hex()) inputIndex = inputIndex + 1 else: while inputIndex < len(inputs): @@ -488,7 +469,10 @@ class Ledger_KeyStore(Hardware_KeyStore): # Sign input with the provided PIN inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature[0] = 0x30 # force for 1.4.9+ - signatures.append(inputSignature) + my_pubkey = inputs[inputIndex][4] + tx.add_signature_to_txin(txin_idx=inputIndex, + signing_pubkey=my_pubkey.hex(), + sig=inputSignature.hex()) inputIndex = inputIndex + 1 firstTransaction = False except UserWarning: @@ -508,16 +492,11 @@ class Ledger_KeyStore(Hardware_KeyStore): finally: self.handler.finished() - for i, txin in enumerate(tx.inputs()): - signingPos = inputs[i][4] - tx.add_signature_to_txin(i, signingPos, bh2u(signatures[i])) - tx.raw = tx.serialize() - @test_pin_unlocked @set_and_unset_signing def show_address(self, sequence, txin_type): client = self.get_client() - address_path = self.get_derivation()[2:] + "/%d/%d"%sequence + address_path = self.get_derivation_prefix()[2:] + "/%d/%d"%sequence self.handler.show_message(_("Showing address ...")) segwit = is_segwit_script_type(txin_type) segwitNative = txin_type == 'p2wpkh' diff --git a/electrum/plugins/safe_t/safe_t.py b/electrum/plugins/safe_t/safe_t.py @@ -1,6 +1,7 @@ from binascii import hexlify, unhexlify import traceback import sys +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT @@ -8,13 +9,16 @@ from electrum.bip32 import BIP32Node from electrum import constants from electrum.i18n import _ from electrum.plugin import Device -from electrum.transaction import deserialize, Transaction -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported from ..hw_wallet import HW_PluginBase -from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data +from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, + get_xpubs_and_der_suffixes_from_txinout) +if TYPE_CHECKING: + from .client import SafeTClient # Safe-T mini initialization methods TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) @@ -24,8 +28,7 @@ class SafeTKeyStore(Hardware_KeyStore): hw_type = 'safe_t' device = 'Safe-T mini' - def get_derivation(self): - return self.derivation + plugin: 'SafeTPlugin' def get_client(self, force_pair=True): return self.plugin.get_client(self, force_pair) @@ -35,7 +38,7 @@ class SafeTKeyStore(Hardware_KeyStore): def sign_message(self, sequence, message, password): client = self.get_client() - address_path = self.get_derivation() + "/%d/%d"%sequence + address_path = self.get_derivation_prefix() + "/%d/%d"%sequence address_n = client.expand_path(address_path) msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) return msg_sig.signature @@ -45,22 +48,13 @@ class SafeTKeyStore(Hardware_KeyStore): return # previous transactions used as inputs prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} for txin in tx.inputs(): - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - tx_hash = txin['prevout_hash'] - if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - prev_tx[tx_hash] = txin['prev_tx'] - for x_pubkey in x_pubkeys: - if not is_xpubkey(x_pubkey): - continue - xpub, s = parse_xpubkey(x_pubkey) - if xpub == self.get_master_public_key(): - xpub_path[xpub] = self.get_derivation() + tx_hash = txin.prevout.txid.hex() + if txin.utxo is None and not Transaction.is_segwit_input(txin): + raise UserFacingException(_('Missing previous tx for legacy input.')) + prev_tx[tx_hash] = txin.utxo - self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) + self.plugin.sign_transaction(self, tx, prev_tx) class SafeTPlugin(HW_PluginBase): @@ -148,7 +142,7 @@ class SafeTPlugin(HW_PluginBase): return client - def get_client(self, keystore, force_pair=True): + def get_client(self, keystore, force_pair=True) -> Optional['SafeTClient']: devmgr = self.device_manager() handler = keystore.handler with devmgr.hid_lock: @@ -302,12 +296,11 @@ class SafeTPlugin(HW_PluginBase): return self.types.OutputScriptType.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) - def sign_transaction(self, keystore, tx, prev_tx, xpub_path): + def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): self.prev_tx = prev_tx - self.xpub_path = xpub_path client = self.get_client(keystore) - inputs = self.tx_inputs(tx, True) - outputs = self.tx_outputs(keystore.get_derivation(), tx) + inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore) + outputs = self.tx_outputs(tx, keystore=keystore) signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, lock_time=tx.locktime, version=tx.version)[0] signatures = [(bh2u(x) + '01') for x in signatures] @@ -322,139 +315,120 @@ class SafeTPlugin(HW_PluginBase): if not client.atleast_version(1, 0): keystore.handler.show_error(_("Your device firmware is too old")) return - change, index = wallet.get_address_index(address) - derivation = keystore.derivation - address_path = "%s/%d/%d"%(derivation, change, index) + deriv_suffix = wallet.get_address_index(address) + derivation = keystore.get_derivation_prefix() + address_path = "%s/%d/%d"%(derivation, *deriv_suffix) address_n = client.expand_path(address_path) + script_type = self.get_safet_input_script_type(wallet.txin_type) + + # prepare multisig, if available: xpubs = wallet.get_master_public_keys() - if len(xpubs) == 1: - script_type = self.get_safet_input_script_type(wallet.txin_type) - client.get_address(self.get_coin_name(), address_n, True, script_type=script_type) - else: - def f(xpub): - return self._make_node_path(xpub, [change, index]) + if len(xpubs) > 1: pubkeys = wallet.get_public_keys(address) # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - pubkeys = list(map(f, sorted_xpubs)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * wallet.n, - m=wallet.m, - ) - script_type = self.get_safet_input_script_type(wallet.txin_type) - client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) - - def tx_inputs(self, tx, for_sig=False): + sorted_pairs = sorted(zip(pubkeys, xpubs)) + multisig = self._make_multisig( + wallet.m, + [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs]) + else: + multisig = None + + client.get_address(self.get_coin_name(), address_n, True, multisig=multisig, script_type=script_type) + + def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'SafeTKeyStore' = None): inputs = [] for txin in tx.inputs(): txinputtype = self.types.TxInputType() - if txin['type'] == 'coinbase': + if txin.is_coinbase(): prev_hash = b"\x00"*32 prev_index = 0xffffffff # signed int -1 else: if for_sig: - x_pubkeys = txin['x_pubkeys'] - if len(x_pubkeys) == 1: - x_pubkey = x_pubkeys[0] - xpub, s = parse_xpubkey(x_pubkey) - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - txinputtype.script_type = self.get_safet_input_script_type(txin['type']) + assert isinstance(tx, PartialTransaction) + assert isinstance(txin, PartialTxInput) + assert keystore + if len(txin.pubkeys) > 1: + xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) + multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) else: - def f(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - return self._make_node_path(xpub, s) - pubkeys = list(map(f, x_pubkeys)) - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), - m=txin.get('num_sig'), - ) - script_type = self.get_safet_input_script_type(txin['type']) - txinputtype = self.types.TxInputType( - script_type=script_type, - multisig=multisig - ) - # find which key is mine - for x_pubkey in x_pubkeys: - if is_xpubkey(x_pubkey): - xpub, s = parse_xpubkey(x_pubkey) - if xpub in self.xpub_path: - xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) - txinputtype._extend_address_n(xpub_n + s) - break - - prev_hash = unhexlify(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - if 'value' in txin: - txinputtype.amount = txin['value'] + multisig = None + script_type = self.get_safet_input_script_type(txin.script_type) + txinputtype = self.types.TxInputType( + script_type=script_type, + multisig=multisig) + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) + if full_path: + txinputtype._extend_address_n(full_path) + + prev_hash = txin.prevout.txid + prev_index = txin.prevout.out_idx + + if txin.value_sats() is not None: + txinputtype.amount = txin.value_sats() txinputtype.prev_hash = prev_hash txinputtype.prev_index = prev_index - if txin.get('scriptSig') is not None: - script_sig = bfh(txin['scriptSig']) - txinputtype.script_sig = script_sig + if txin.script_sig is not None: + txinputtype.script_sig = txin.script_sig - txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) + txinputtype.sequence = txin.nsequence inputs.append(txinputtype) return inputs - def tx_outputs(self, derivation, tx: Transaction): + def _make_multisig(self, m, xpubs): + if len(xpubs) == 1: + return None + pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] + return self.types.MultisigRedeemScriptType( + pubkeys=pubkeys, + signatures=[b''] * len(pubkeys), + m=m) + + def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'): def create_output_by_derivation(): - script_type = self.get_safet_output_script_type(info.script_type) - if len(xpubs) == 1: - address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) - txoutputtype = self.types.TxOutputType( - amount=amount, - script_type=script_type, - address_n=address_n, - ) + script_type = self.get_safet_output_script_type(txout.script_type) + if len(txout.pubkeys) > 1: + xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) + multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) else: - address_n = self.client_class.expand_path("/%d/%d" % index) - pubkeys = [self._make_node_path(xpub, address_n) for xpub in xpubs] - multisig = self.types.MultisigRedeemScriptType( - pubkeys=pubkeys, - signatures=[b''] * len(pubkeys), - m=m) - txoutputtype = self.types.TxOutputType( - multisig=multisig, - amount=amount, - address_n=self.client_class.expand_path(derivation + "/%d/%d" % index), - script_type=script_type) + multisig = None + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) + assert full_path + txoutputtype = self.types.TxOutputType( + multisig=multisig, + amount=txout.value, + address_n=full_path, + script_type=script_type) return txoutputtype def create_output_by_address(): txoutputtype = self.types.TxOutputType() - txoutputtype.amount = amount - if _type == TYPE_SCRIPT: - txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN - txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) - elif _type == TYPE_ADDRESS: + txoutputtype.amount = txout.value + if address: txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS txoutputtype.address = address + else: + txoutputtype.script_type = self.types.OutputScriptType.PAYTOOPRETURN + txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout) return txoutputtype outputs = [] has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for o in tx.outputs(): - _type, address, amount = o.type, o.address, o.value + for txout in tx.outputs(): + address = txout.address use_create_by_derivation = False - info = tx.output_info.get(address) - if info is not None and not has_change: - index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig + if txout.is_mine and not has_change: # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed # note: ^ restriction can be removed once we require fw # that has https://github.com/trezor/trezor-mcu/pull/306 - if info.is_change == any_output_on_change_branch: + if txout.is_change == any_output_on_change_branch: use_create_by_derivation = True has_change = True @@ -466,20 +440,20 @@ class SafeTPlugin(HW_PluginBase): return outputs - def electrum_tx_to_txtype(self, tx): + def electrum_tx_to_txtype(self, tx: Optional[Transaction]): t = self.types.TransactionType() if tx is None: # probably for segwit input and we don't need this prev txn return t - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] + tx.deserialize() + t.version = tx.version + t.lock_time = tx.locktime inputs = self.tx_inputs(tx) t._extend_inputs(inputs) - for vout in d['outputs']: + for out in tx.outputs(): o = t._add_bin_outputs() - o.amount = vout['value'] - o.script_pubkey = bfh(vout['scriptPubKey']) + o.amount = out.value + o.script_pubkey = out.scriptpubkey return t # This function is called from the TREZOR libraries (via tx_api) diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py @@ -1,6 +1,6 @@ import traceback import sys -from typing import NamedTuple, Any +from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING from electrum.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT @@ -8,14 +8,15 @@ from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as pa from electrum import constants from electrum.i18n import _ from electrum.plugin import Device -from electrum.transaction import deserialize, Transaction -from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey +from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput +from electrum.keystore import Hardware_KeyStore from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from electrum.logging import get_logger from ..hw_wallet import HW_PluginBase from ..hw_wallet.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data, - LibraryFoundButUnusable, OutdatedHwFirmwareException) + LibraryFoundButUnusable, OutdatedHwFirmwareException, + get_xpubs_and_der_suffixes_from_txinout) _logger = get_logger(__name__) @@ -53,8 +54,7 @@ class TrezorKeyStore(Hardware_KeyStore): hw_type = 'trezor' device = TREZOR_PRODUCT_KEY - def get_derivation(self): - return self.derivation + plugin: 'TrezorPlugin' def get_client(self, force_pair=True): return self.plugin.get_client(self, force_pair) @@ -64,7 +64,7 @@ class TrezorKeyStore(Hardware_KeyStore): def sign_message(self, sequence, message, password): client = self.get_client() - address_path = self.get_derivation() + "/%d/%d"%sequence + address_path = self.get_derivation_prefix() + "/%d/%d"%sequence msg_sig = client.sign_message(address_path, message) return msg_sig.signature @@ -73,22 +73,13 @@ class TrezorKeyStore(Hardware_KeyStore): return # previous transactions used as inputs prev_tx = {} - # path of the xpubs that are involved - xpub_path = {} for txin in tx.inputs(): - pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) - tx_hash = txin['prevout_hash'] - if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): - raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) - prev_tx[tx_hash] = txin['prev_tx'] - for x_pubkey in x_pubkeys: - if not is_xpubkey(x_pubkey): - continue - xpub, s = parse_xpubkey(x_pubkey) - if xpub == self.get_master_public_key(): - xpub_path[xpub] = self.get_derivation() + tx_hash = txin.prevout.txid.hex() + if txin.utxo is None and not Transaction.is_segwit_input(txin): + raise UserFacingException(_('Missing previous tx for legacy input.')) + prev_tx[tx_hash] = txin.utxo - self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) + self.plugin.sign_transaction(self, tx, prev_tx) class TrezorInitSettings(NamedTuple): @@ -172,7 +163,7 @@ class TrezorPlugin(HW_PluginBase): # note that this call can still raise! return TrezorClientBase(transport, handler, self) - def get_client(self, keystore, force_pair=True): + def get_client(self, keystore, force_pair=True) -> Optional['TrezorClientBase']: devmgr = self.device_manager() handler = keystore.handler with devmgr.hid_lock: @@ -327,11 +318,11 @@ class TrezorPlugin(HW_PluginBase): return OutputScriptType.PAYTOMULTISIG raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) - def sign_transaction(self, keystore, tx, prev_tx, xpub_path): - prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() } + def sign_transaction(self, keystore, tx: PartialTransaction, prev_tx): + prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() } client = self.get_client(keystore) - inputs = self.tx_inputs(tx, xpub_path, True) - outputs = self.tx_outputs(keystore.get_derivation(), tx) + inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore) + outputs = self.tx_outputs(tx, keystore=keystore) details = SignTx(lock_time=tx.locktime, version=tx.version) signatures, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, details=details, prev_txes=prev_tx) signatures = [(bh2u(x) + '01') for x in signatures] @@ -343,7 +334,7 @@ class TrezorPlugin(HW_PluginBase): if not self.show_address_helper(wallet, address, keystore): return deriv_suffix = wallet.get_address_index(address) - derivation = keystore.derivation + derivation = keystore.get_derivation_prefix() address_path = "%s/%d/%d"%(derivation, *deriv_suffix) script_type = self.get_trezor_input_script_type(wallet.txin_type) @@ -355,111 +346,107 @@ class TrezorPlugin(HW_PluginBase): sorted_pairs = sorted(zip(pubkeys, xpubs)) multisig = self._make_multisig( wallet.m, - [(xpub, deriv_suffix) for _, xpub in sorted_pairs]) + [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs]) else: multisig = None client = self.get_client(keystore) client.show_address(address_path, script_type, multisig) - def tx_inputs(self, tx, xpub_path, for_sig=False): + def tx_inputs(self, tx: Transaction, *, for_sig=False, keystore: 'TrezorKeyStore' = None): inputs = [] for txin in tx.inputs(): txinputtype = TxInputType() - if txin['type'] == 'coinbase': + if txin.is_coinbase(): prev_hash = b"\x00"*32 prev_index = 0xffffffff # signed int -1 else: if for_sig: - x_pubkeys = txin['x_pubkeys'] - xpubs = [parse_xpubkey(x) for x in x_pubkeys] - multisig = self._make_multisig(txin.get('num_sig'), xpubs, txin.get('signatures')) - script_type = self.get_trezor_input_script_type(txin['type']) + assert isinstance(tx, PartialTransaction) + assert isinstance(txin, PartialTxInput) + assert keystore + if len(txin.pubkeys) > 1: + xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) + multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) + else: + multisig = None + script_type = self.get_trezor_input_script_type(txin.script_type) txinputtype = TxInputType( script_type=script_type, multisig=multisig) - # find which key is mine - for xpub, deriv in xpubs: - if xpub in xpub_path: - xpub_n = parse_path(xpub_path[xpub]) - txinputtype.address_n = xpub_n + deriv - break - - prev_hash = bfh(txin['prevout_hash']) - prev_index = txin['prevout_n'] - - if 'value' in txin: - txinputtype.amount = txin['value'] + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) + if full_path: + txinputtype.address_n = full_path + + prev_hash = txin.prevout.txid + prev_index = txin.prevout.out_idx + + if txin.value_sats() is not None: + txinputtype.amount = txin.value_sats() txinputtype.prev_hash = prev_hash txinputtype.prev_index = prev_index - if txin.get('scriptSig') is not None: - script_sig = bfh(txin['scriptSig']) - txinputtype.script_sig = script_sig + if txin.script_sig is not None: + txinputtype.script_sig = txin.script_sig - txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) + txinputtype.sequence = txin.nsequence inputs.append(txinputtype) return inputs - def _make_multisig(self, m, xpubs, signatures=None): + def _make_multisig(self, m, xpubs): if len(xpubs) == 1: return None - pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] - if signatures is None: - signatures = [b''] * len(pubkeys) - elif len(signatures) != len(pubkeys): - raise RuntimeError('Mismatched number of signatures') - else: - signatures = [bfh(x)[:-1] if x else b'' for x in signatures] - return MultisigRedeemScriptType( pubkeys=pubkeys, - signatures=signatures, + signatures=[b''] * len(pubkeys), m=m) - def tx_outputs(self, derivation, tx: Transaction): + def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'): def create_output_by_derivation(): - script_type = self.get_trezor_output_script_type(info.script_type) - deriv = parse_path("/%d/%d" % index) - multisig = self._make_multisig(m, [(xpub, deriv) for xpub in xpubs]) + script_type = self.get_trezor_output_script_type(txout.script_type) + if len(txout.pubkeys) > 1: + xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) + multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) + else: + multisig = None + my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) + assert full_path txoutputtype = TxOutputType( multisig=multisig, - amount=amount, - address_n=parse_path(derivation + "/%d/%d" % index), + amount=txout.value, + address_n=full_path, script_type=script_type) return txoutputtype def create_output_by_address(): txoutputtype = TxOutputType() - txoutputtype.amount = amount - if _type == TYPE_SCRIPT: - txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN - txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o) - elif _type == TYPE_ADDRESS: + txoutputtype.amount = txout.value + if address: txoutputtype.script_type = OutputScriptType.PAYTOADDRESS txoutputtype.address = address + else: + txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN + txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout) return txoutputtype outputs = [] has_change = False any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) - for o in tx.outputs(): - _type, address, amount = o.type, o.address, o.value + for txout in tx.outputs(): + address = txout.address use_create_by_derivation = False - info = tx.output_info.get(address) - if info is not None and not has_change: - index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig + if txout.is_mine and not has_change: # prioritise hiding outputs on the 'change' branch from user # because no more than one change address allowed # note: ^ restriction can be removed once we require fw # that has https://github.com/trezor/trezor-mcu/pull/306 - if info.is_change == any_output_on_change_branch: + if txout.is_change == any_output_on_change_branch: use_create_by_derivation = True has_change = True @@ -471,17 +458,17 @@ class TrezorPlugin(HW_PluginBase): return outputs - def electrum_tx_to_txtype(self, tx, xpub_path): + def electrum_tx_to_txtype(self, tx: Optional[Transaction]): t = TransactionType() if tx is None: # probably for segwit input and we don't need this prev txn return t - d = deserialize(tx.raw) - t.version = d['version'] - t.lock_time = d['lockTime'] - t.inputs = self.tx_inputs(tx, xpub_path) + tx.deserialize() + t.version = tx.version + t.lock_time = tx.locktime + t.inputs = self.tx_inputs(tx) t.bin_outputs = [ - TxOutputBinType(amount=vout['value'], script_pubkey=bfh(vout['scriptPubKey'])) - for vout in d['outputs'] + TxOutputBinType(amount=o.value, script_pubkey=o.scriptpubkey) + for o in tx.outputs() ] return t diff --git a/electrum/plugins/trustedcoin/cmdline.py b/electrum/plugins/trustedcoin/cmdline.py @@ -30,7 +30,7 @@ from .trustedcoin import TrustedCoinPlugin class Plugin(TrustedCoinPlugin): - def prompt_user_for_otp(self, wallet, tx): + def prompt_user_for_otp(self, wallet, tx): # FIXME this is broken if not isinstance(wallet, self.wallet_class): return if not wallet.can_sign_without_server(): diff --git a/electrum/plugins/trustedcoin/legacy_tx_format.py b/electrum/plugins/trustedcoin/legacy_tx_format.py @@ -0,0 +1,106 @@ +# Copyright (C) 2018 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +import copy +from typing import Union + +from electrum import bitcoin +from electrum.bitcoin import push_script, int_to_hex, var_int +from electrum.transaction import (Transaction, PartialTransaction, PartialTxInput, + multisig_script, construct_witness) +from electrum.keystore import BIP32_KeyStore +from electrum.wallet import Multisig_Wallet + + +ELECTRUM_PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff' +PARTIAL_FORMAT_VERSION = b'\x00' +NO_SIGNATURE = b'\xff' + + +def get_xpubkey(keystore: BIP32_KeyStore, c, i) -> str: + def encode_path_int(path_int) -> str: + if path_int < 0xffff: + hex = bitcoin.int_to_hex(path_int, 2) + else: + hex = 'ffff' + bitcoin.int_to_hex(path_int, 4) + return hex + + s = ''.join(map(encode_path_int, (c, i))) + return 'ff' + bitcoin.DecodeBase58Check(keystore.xpub).hex() + s + + +def serialize_tx_in_legacy_format(tx: PartialTransaction, *, wallet: Multisig_Wallet) -> str: + assert isinstance(tx, PartialTransaction) + + # copy tx so we don't mutate the input arg + # monkey-patch method of tx instance to change serialization + tx = copy.deepcopy(tx) + + def get_siglist(txin: 'PartialTxInput', *, estimate_size=False): + if txin.prevout.is_coinbase(): + return [], [] + if estimate_size: + try: + pubkey_size = len(txin.pubkeys[0]) + except IndexError: + pubkey_size = 33 # guess it is compressed + num_pubkeys = max(1, len(txin.pubkeys)) + pk_list = ["00" * pubkey_size] * num_pubkeys + # we assume that signature will be 0x48 bytes long + num_sig = max(txin.num_sig, num_pubkeys) + sig_list = [ "00" * 0x48 ] * num_sig + else: + pk_list = ["" for pk in txin.pubkeys] + for ks in wallet.get_keystores(): + my_pubkey, full_path = ks.find_my_pubkey_in_txinout(txin) + x_pubkey = get_xpubkey(ks, full_path[-2], full_path[-1]) + pubkey_index = txin.pubkeys.index(my_pubkey) + pk_list[pubkey_index] = x_pubkey + assert all(pk_list) + sig_list = [txin.part_sigs.get(pubkey, NO_SIGNATURE).hex() for pubkey in txin.pubkeys] + return pk_list, sig_list + + def input_script(self, txin: PartialTxInput, *, estimate_size=False) -> str: + assert estimate_size is False + pubkeys, sig_list = get_siglist(txin, estimate_size=estimate_size) + script = ''.join(push_script(x) for x in sig_list) + if txin.script_type == 'p2sh': + # put op_0 before script + script = '00' + script + redeem_script = multisig_script(pubkeys, txin.num_sig) + script += push_script(redeem_script) + return script + elif txin.script_type == 'p2wsh': + return '' + raise Exception(f"unexpected type {txin.script_type}") + tx.input_script = input_script.__get__(tx, PartialTransaction) + + def serialize_witness(self, txin: PartialTxInput, *, estimate_size=False): + assert estimate_size is False + if txin.witness is not None: + return txin.witness.hex() + if txin.prevout.is_coinbase(): + return '' + assert isinstance(txin, PartialTxInput) + if not self.is_segwit_input(txin): + return '00' + pubkeys, sig_list = get_siglist(txin, estimate_size=estimate_size) + if txin.script_type == 'p2wsh': + witness_script = multisig_script(pubkeys, txin.num_sig) + witness = construct_witness([0] + sig_list + [witness_script]) + else: + raise Exception(f"unexpected type {txin.script_type}") + if txin.is_complete() or estimate_size: + partial_format_witness_prefix = '' + else: + input_value = int_to_hex(txin.value_sats(), 8) + witness_version = int_to_hex(0, 2) + partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version + return partial_format_witness_prefix + witness + tx.serialize_witness = serialize_witness.__get__(tx, PartialTransaction) + + buf = ELECTRUM_PARTIAL_TXN_HEADER_MAGIC.hex() + buf += PARTIAL_FORMAT_VERSION.hex() + buf += tx.serialize_to_network() + return buf diff --git a/electrum/plugins/trustedcoin/trustedcoin.py b/electrum/plugins/trustedcoin/trustedcoin.py @@ -29,7 +29,7 @@ import base64 import time import hashlib from collections import defaultdict -from typing import Dict, Union +from typing import Dict, Union, Sequence, List from urllib.parse import urljoin from urllib.parse import quote @@ -39,7 +39,7 @@ from electrum import ecc, constants, keystore, version, bip32, bitcoin from electrum.bitcoin import TYPE_ADDRESS from electrum.bip32 import BIP32Node, xpub_type from electrum.crypto import sha256 -from electrum.transaction import TxOutput +from electrum.transaction import PartialTxOutput, PartialTxInput, PartialTransaction, Transaction from electrum.mnemonic import Mnemonic, seed_type, is_any_2fa_seed_type from electrum.wallet import Multisig_Wallet, Deterministic_Wallet from electrum.i18n import _ @@ -50,6 +50,8 @@ from electrum.network import Network from electrum.base_wizard import BaseWizard, WizardWalletPasswordSetting from electrum.logging import Logger +from .legacy_tx_format import serialize_tx_in_legacy_format + def get_signing_xpub(xtype): if not constants.net.TESTNET: @@ -259,6 +261,8 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER class Wallet_2fa(Multisig_Wallet): + plugin: 'TrustedCoinPlugin' + wallet_type = '2fa' def __init__(self, storage, *, config): @@ -314,34 +318,35 @@ class Wallet_2fa(Multisig_Wallet): raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n)) return price - def make_unsigned_transaction(self, coins, outputs, fixed_fee=None, - change_addr=None, is_sweep=False): + def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput], + outputs: List[PartialTxOutput], fee=None, + change_addr: str = None, is_sweep=False) -> PartialTransaction: mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction( - self, coins, o, fixed_fee, change_addr) - fee = self.extra_fee() if not is_sweep else 0 - if fee: + self, coins=coins, outputs=o, fee=fee, change_addr=change_addr) + extra_fee = self.extra_fee() if not is_sweep else 0 + if extra_fee: address = self.billing_info['billing_address_segwit'] - fee_output = TxOutput(TYPE_ADDRESS, address, fee) + fee_output = PartialTxOutput.from_address_and_value(address, extra_fee) try: tx = mk_tx(outputs + [fee_output]) except NotEnoughFunds: # TrustedCoin won't charge if the total inputs is # lower than their fee tx = mk_tx(outputs) - if tx.input_value() >= fee: + if tx.input_value() >= extra_fee: raise self.logger.info("not charging for this tx") else: tx = mk_tx(outputs) return tx - def on_otp(self, tx, otp): + def on_otp(self, tx: PartialTransaction, otp): if not otp: self.logger.info("sign_transaction: no auth code") return otp = int(otp) long_user_id, short_id = self.get_user_id() - raw_tx = tx.serialize() + raw_tx = serialize_tx_in_legacy_format(tx, wallet=self) try: r = server.sign(short_id, raw_tx, otp) except TrustedCoinException as e: @@ -350,8 +355,9 @@ class Wallet_2fa(Multisig_Wallet): else: raise if r: - raw_tx = r.get('transaction') - tx.update(raw_tx) + received_raw_tx = r.get('transaction') + received_tx = Transaction(received_raw_tx) + tx.combine_with_other_psbt(received_tx) self.logger.info(f"twofactor: is complete {tx.is_complete()}") # reset billing_info self.billing_info = None @@ -457,15 +463,16 @@ class TrustedCoinPlugin(BasePlugin): self.logger.info("twofactor: xpub3 not needed") return def wrapper(tx): + assert tx self.prompt_user_for_otp(wallet, tx, on_success, on_failure) return wrapper @hook - def get_tx_extra_fee(self, wallet, tx): + def get_tx_extra_fee(self, wallet, tx: Transaction): if type(wallet) != Wallet_2fa: return for o in tx.outputs(): - if o.type == TYPE_ADDRESS and wallet.is_billing_address(o.address): + if wallet.is_billing_address(o.address): return o.address, o.value def finish_requesting(func): diff --git a/electrum/scripts/bip70.py b/electrum/scripts/bip70.py @@ -7,6 +7,7 @@ import tlslite from electrum.transaction import Transaction from electrum import paymentrequest from electrum import paymentrequest_pb2 as pb2 +from electrum.bitcoin import address_to_script chain_file = 'mychain.pem' cert_file = 'mycert.pem' @@ -26,7 +27,7 @@ certificates.certificate.extend(map(lambda x: str(x.bytes), chain.x509List)) with open(cert_file, 'r') as f: rsakey = tlslite.utils.python_rsakey.Python_RSAKey.parsePEM(f.read()) -script = Transaction.pay_script('address', address).decode('hex') +script = address_to_script(address) pr_string = paymentrequest.make_payment_request(amount, script, memo, rsakey) diff --git a/electrum/segwit_addr.py b/electrum/segwit_addr.py @@ -103,6 +103,8 @@ def convertbits(data, frombits, tobits, pad=True): def decode(hrp, addr): """Decode a segwit address.""" + if addr is None: + return (None, None) hrpgot, data = bech32_decode(addr) if hrpgot != hrp: return (None, None) diff --git a/electrum/synchronizer.py b/electrum/synchronizer.py @@ -209,7 +209,7 @@ class Synchronizer(SynchronizerBase): async def _get_transaction(self, tx_hash, *, allow_server_not_finding_tx=False): self._requests_sent += 1 try: - result = await self.network.get_transaction(tx_hash) + raw_tx = await self.network.get_transaction(tx_hash) except UntrustedServerReturnedError as e: # most likely, "No such mempool or blockchain transaction" if allow_server_not_finding_tx: @@ -219,7 +219,7 @@ class Synchronizer(SynchronizerBase): raise finally: self._requests_answered += 1 - tx = Transaction(result) + tx = Transaction(raw_tx) try: tx.deserialize() # see if raises except Exception as e: @@ -233,7 +233,7 @@ class Synchronizer(SynchronizerBase): raise SynchronizerFailure(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})") tx_height = self.requested_tx.pop(tx_hash) self.wallet.receive_tx_callback(tx_hash, tx, tx_height) - self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(tx.raw)}") + self.logger.info(f"received tx {tx_hash} height: {tx_height} bytes: {len(raw_tx)}") # callbacks self.wallet.network.trigger_callback('new_transaction', self.wallet, tx) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh @@ -147,7 +147,7 @@ if [[ $1 == "breach" ]]; then echo "alice pays" $alice lnpay $request sleep 2 - ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"') + ctx=$($alice get_channel_ctx $channel) request=$($bob add_lightning_request 0.01 -m "blah2") echo "alice pays again" $alice lnpay $request @@ -224,7 +224,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then echo "SETTLE_DELAY did not work, $settled != 0" exit 1 fi - ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"') + ctx=$($alice get_channel_ctx $channel) sleep 5 settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') if [[ "$settled" != "1" ]]; then @@ -251,7 +251,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then echo "alice pays bob" invoice=$($bob add_lightning_request 0.05 -m "test") $alice lnpay $invoice --timeout=1 || true - ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"') + ctx=$($alice get_channel_ctx $channel) settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') if [[ "$settled" != "0" ]]; then echo "SETTLE_DELAY did not work, $settled != 0" diff --git a/electrum/tests/test_bitcoin.py b/electrum/tests/test_bitcoin.py @@ -12,7 +12,7 @@ from electrum.bitcoin import (public_key_to_p2pkh, address_from_private_key, from electrum.bip32 import (BIP32Node, convert_bip32_intpath_to_strpath, xpub_from_xprv, xpub_type, is_xprv, is_bip32_derivation, is_xpub, convert_bip32_path_to_list_of_uint32, - normalize_bip32_derivation) + normalize_bip32_derivation, is_all_public_derivation) from electrum.crypto import sha256d, SUPPORTED_PW_HASH_VERSIONS from electrum import ecc, crypto, constants from electrum.ecc import number_to_string, string_to_number @@ -494,6 +494,14 @@ class Test_xprv_xpub(ElectrumTestCase): self.assertEqual("m/0/2/1'", normalize_bip32_derivation("m/0/2/-1/")) self.assertEqual("m/0/1'/1'/5'", normalize_bip32_derivation("m/0//-1/1'///5h")) + def test_is_all_public_derivation(self): + self.assertFalse(is_all_public_derivation("m/0/1'/1'")) + self.assertFalse(is_all_public_derivation("m/0/2/1'")) + self.assertFalse(is_all_public_derivation("m/0/1'/1'/5")) + self.assertTrue(is_all_public_derivation("m")) + self.assertTrue(is_all_public_derivation("m/0")) + self.assertTrue(is_all_public_derivation("m/75/22/3")) + def test_xtype_from_derivation(self): self.assertEqual('standard', xtype_from_derivation("m/44'")) self.assertEqual('standard', xtype_from_derivation("m/44'/")) diff --git a/electrum/tests/test_commands.py b/electrum/tests/test_commands.py @@ -159,3 +159,24 @@ class TestCommandsTestnet(TestCaseForTestnet): for xkey1, xtype1 in xprvs: for xkey2, xtype2 in xprvs: self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) + + def test_serialize(self): + cmds = Commands(config=self.config) + jsontx = { + "inputs": [ + { + "prevout_hash": "9d221a69ca3997cbeaf5624d723e7dc5f829b1023078c177d37bdae95f37c539", + "prevout_n": 1, + "value": 1000000, + "privkey": "p2wpkh:cVDXzzQg6RoCTfiKpe8MBvmm5d5cJc6JLuFApsFDKwWa6F5TVHpD" + } + ], + "outputs": [ + { + "address": "tb1q4s8z6g5jqzllkgt8a4har94wl8tg0k9m8kv5zd", + "value": 990000 + } + ] + } + self.assertEqual("0200000000010139c5375fe9da7bd377c1783002b129f8c57d3e724d62f5eacb9739ca691a229d0100000000feffffff01301b0f0000000000160014ac0e2d229200bffb2167ed6fd196aef9d687d8bb02483045022100fa88a9e7930b2af269fd0a5cb7fbbc3d0a05606f3ac6ea8a40686ebf02fdd85802203dd19603b4ee8fdb81d40185572027686f70ea299c6a3e22bc2545e1396398b20121021f110909ded653828a254515b58498a6bafc96799fb0851554463ed44ca7d9da00000000", + cmds._run('serialize', (jsontx,))) diff --git a/electrum/tests/test_lnchannel.py b/electrum/tests/test_lnchannel.py @@ -170,7 +170,7 @@ class TestFee(ElectrumTestCase): """ def test_fee(self): alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000) - self.assertIn(9999817, [x[2] for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) + self.assertIn(9999817, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) class TestChannel(ElectrumTestCase): maxDiff = 999 diff --git a/electrum/tests/test_lnutil.py b/electrum/tests/test_lnutil.py @@ -9,7 +9,7 @@ from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_see get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError, ScriptHtlc, extract_nodeid, calc_onchain_fees, UpdateAddHtlc) from electrum.util import bh2u, bfh -from electrum.transaction import Transaction +from electrum.transaction import Transaction, PartialTransaction from . import ElectrumTestCase @@ -570,7 +570,7 @@ class TestLNUtil(ElectrumTestCase): localhtlcsig=bfh(local_sig), payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout witness_script=htlc) - our_htlc_tx._inputs[0]['witness'] = bh2u(our_htlc_tx_witness) + our_htlc_tx._inputs[0].witness = our_htlc_tx_witness return str(our_htlc_tx) def test_commitment_tx_with_one_output(self): @@ -669,7 +669,7 @@ class TestLNUtil(ElectrumTestCase): ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) - def sign_and_insert_remote_sig(self, tx, remote_pubkey, remote_signature, pubkey, privkey): + def sign_and_insert_remote_sig(self, tx: PartialTransaction, remote_pubkey, remote_signature, pubkey, privkey): assert type(remote_pubkey) is bytes assert len(remote_pubkey) == 33 assert type(remote_signature) is str @@ -678,10 +678,7 @@ class TestLNUtil(ElectrumTestCase): assert len(pubkey) == 33 assert len(privkey) == 33 tx.sign({bh2u(pubkey): (privkey[:-1], True)}) - pubkeys, _x_pubkeys = tx.get_sorted_pubkeys(tx.inputs()[0]) - index_of_pubkey = pubkeys.index(bh2u(remote_pubkey)) - tx._inputs[0]["signatures"][index_of_pubkey] = remote_signature + "01" - tx.raw = None + tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + "01") def test_get_compressed_pubkey_from_bech32(self): self.assertEqual(b'\x03\x84\xef\x87\xd9d\xa2\xaaa7=\xff\xb8\xfe=t8[}>;\n\x13\xa8e\x8eo:\xf5Mi\xb5H', diff --git a/electrum/tests/test_psbt.py b/electrum/tests/test_psbt.py @@ -0,0 +1,269 @@ +from pprint import pprint +import unittest + +from electrum import constants +from electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream, + SerializationError, PSBTInputConsistencyFailure) + +from . import ElectrumTestCase, TestCaseForTestnet + + +class TestValidPSBT(TestCaseForTestnet): + # test cases from BIP-0174 + + def test_valid_psbt_001(self): + # Case: PSBT with one P2PKH input. Outputs are empty + tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab300000000000000')) + tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAAAA') + for tx in (tx1, tx2): + self.assertEqual(1, len(tx.inputs())) + self.assertFalse(tx.inputs()[0].is_complete()) + + def test_valid_psbt_002(self): + # Case: PSBT with one P2PKH input and one P2SH-P2WPKH input. First input is signed and finalized. Outputs are empty + tx1 = tx_from_any(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac000000000001076a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa882920001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000')) + tx2 = tx_from_any('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA') + for tx in (tx1, tx2): + self.assertEqual(2, len(tx.inputs())) + self.assertTrue(tx.inputs()[0].is_complete()) + self.assertFalse(tx.inputs()[1].is_complete()) + + def test_valid_psbt_003(self): + # Case: PSBT with one P2PKH input which has a non-final scriptSig and has a sighash type specified. Outputs are empty + tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001030401000000000000')) + tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==') + for tx in (tx1, tx2): + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(1, tx.inputs()[0].sighash) + self.assertFalse(tx.inputs()[0].is_complete()) + + def test_valid_psbt_004(self): + # Case: PSBT with one P2PKH input and one P2SH-P2WPKH input both with non-final scriptSigs. P2SH-P2WPKH input's redeemScript is available. Outputs filled. + tx1 = tx_from_any(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000100df0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e13000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000')) + tx2 = tx_from_any('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=') + for tx in (tx1, tx2): + self.assertEqual(2, len(tx.inputs())) + self.assertFalse(tx.inputs()[0].is_complete()) + self.assertFalse(tx.inputs()[1].is_complete()) + self.assertTrue(tx.inputs()[1].redeem_script is not None) + + def test_valid_psbt_005(self): + # Case: PSBT with one P2SH-P2WSH input of a 2-of-2 multisig, redeemScript, witnessScript, and keypaths are available. Contains one signature. + tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=') + for tx in (tx1, tx2): + self.assertEqual(1, len(tx.inputs())) + self.assertFalse(tx.inputs()[0].is_complete()) + self.assertTrue(tx.inputs()[0].redeem_script is not None) + self.assertTrue(tx.inputs()[0].witness_script is not None) + self.assertEqual(2, len(tx.inputs()[0].bip32_paths)) + self.assertEqual(1, len(tx.inputs()[0].part_sigs)) + + def test_valid_psbt_006(self): + # Case: PSBT with one P2WSH input of a 2-of-2 multisig. witnessScript, keypaths, and global xpubs are available. Contains no signatures. Outputs filled. + tx1 = tx_from_any(bytes.fromhex('70736274ff01005202000000019dfc6628c26c5899fe1bd3dc338665bfd55d7ada10f6220973df2d386dec12760100000000ffffffff01f03dcd1d000000001600147b3a00bfdc14d27795c2b74901d09da6ef133579000000004f01043587cf02da3fd0088000000097048b1ad0445b1ec8275517727c87b4e4ebc18a203ffa0f94c01566bd38e9000351b743887ee1d40dc32a6043724f2d6459b3b5a4d73daec8fbae0472f3bc43e20cd90c6a4fae000080000000804f01043587cf02da3fd00880000001b90452427139cd78c2cff2444be353cd58605e3e513285e528b407fae3f6173503d30a5e97c8adbc557dac2ad9a7e39c1722ebac69e668b6f2667cc1d671c83cab0cd90c6a4fae000080010000800001012b0065cd1d000000002200202c5486126c4978079a814e13715d65f36459e4d6ccaded266d0508645bafa6320105475221029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c88712103372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b52ae2206029da12cdb5b235692b91536afefe5c91c3ab9473d8e43b533836ab456299c887110d90c6a4fae0000800000008000000000220603372b34234ed7cf9c1fea5d05d441557927be9542b162eb02e1ab2ce80224c00b10d90c6a4fae0000800100008000000000002202039eff1f547a1d5f92dfa2ba7af6ac971a4bd03ba4a734b03156a256b8ad3a1ef910ede45cc500000080000000800100008000')) + tx2 = tx_from_any('cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA') + for tx in (tx1, tx2): + self.assertEqual(1, len(tx.inputs())) + self.assertFalse(tx.inputs()[0].is_complete()) + self.assertTrue(tx.inputs()[0].witness_script is not None) + self.assertEqual(2, len(tx.inputs()[0].bip32_paths)) + self.assertEqual(2, len(tx.xpubs)) + self.assertEqual(0, len(tx.inputs()[0].part_sigs)) + + def test_valid_psbt_007(self): + # Case: PSBT with unknown types in the inputs. + tx1 = tx_from_any(bytes.fromhex('70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0000')) + tx2 = tx_from_any('cHNidP8BAD8CAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAA2oBAAAAAAAACg8BAgMEBQYHCAkPAQIDBAUGBwgJCgsMDQ4PAAA=') + for tx in (tx1, tx2): + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(1, len(tx.inputs()[0]._unknown)) + + def test_valid_psbt_008(self): + # Case: PSBT with `PSBT_GLOBAL_XPUB`. + constants.set_mainnet() + try: + tx1 = tx_from_any(bytes.fromhex('70736274ff01009d0100000002710ea76ab45c5cb6438e607e59cc037626981805ae9e0dfd9089012abb0be5350100000000ffffffff190994d6a8b3c8c82ccbcfb2fba4106aa06639b872a8d447465c0d42588d6d670000000000ffffffff0200e1f505000000001976a914b6bc2c0ee5655a843d79afedd0ccc3f7dd64340988ac605af405000000001600141188ef8e4ce0449eaac8fb141cbf5a1176e6a088000000004f010488b21e039e530cac800000003dbc8a5c9769f031b17e77fea1518603221a18fd18f2b9a54c6c8c1ac75cbc3502f230584b155d1c7f1cd45120a653c48d650b431b67c5b2c13f27d7142037c1691027569c503100008000000080000000800001011f00e1f5050000000016001433b982f91b28f160c920b4ab95e58ce50dda3a4a220203309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c47304402202d704ced830c56a909344bd742b6852dccd103e963bae92d38e75254d2bb424502202d86c437195df46c0ceda084f2a291c3da2d64070f76bf9b90b195e7ef28f77201220603309680f33c7de38ea6a47cd4ecd66f1f5a49747c6ffb8808ed09039243e3ad5c1827569c5031000080000000800000008000000000010000000001011f00e1f50500000000160014388fb944307eb77ef45197d0b0b245e079f011de220202c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b11047304402204cb1fb5f869c942e0e26100576125439179ae88dca8a9dc3ba08f7953988faa60220521f49ca791c27d70e273c9b14616985909361e25be274ea200d7e08827e514d01220602c777161f73d0b7c72b9ee7bde650293d13f095bc7656ad1f525da5fd2e10b1101827569c5031000080000000800000008000000000000000000000220202d20ca502ee289686d21815bd43a80637b0698e1fbcdbe4caed445f6c1a0a90ef1827569c50310000800000008000000080000000000400000000')) + tx2 = tx_from_any('cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA') + for tx in (tx1, tx2): + self.assertEqual(1, len(tx.xpubs)) + finally: + constants.set_testnet() + + +class TestInvalidPSBT(TestCaseForTestnet): + # test cases from BIP-0174 + + def test_invalid_psbt_001(self): + # Case: Network transaction, not PSBT format + with self.assertRaises(BadHeaderMagic): + tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('0200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf6000000006a473044022070b2245123e6bf474d60c5b50c043d4c691a5d2435f09a34a7662a9dc251790a022001329ca9dacf280bdf30740ec0390422422c81cb45839457aeb76fc12edd95b3012102657d118d3357b8e0f4c2cd46db7b39f6d9c38d9a70abcb9b2de5dc8dbfe4ce31feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300')) + with self.assertRaises(BadHeaderMagic): + tx2 = PartialTransaction.from_raw_psbt('AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAABqRzBEAiBwsiRRI+a/R01gxbUMBD1MaRpdJDXwmjSnZiqdwlF5CgIgATKcqdrPKAvfMHQOwDkEIkIsgctFg5RXrrdvwS7dlbMBIQJlfRGNM1e44PTCzUbbezn22cONmnCry5st5dyNv+TOMf7///8C09/1BQAAAAAZdqkU0MWZA8W6woaHYOkP1SGkZlqnZSCIrADh9QUAAAAAF6kUNUXm4zuDLEcFDyTT7rk8nAOUi8eHsy4TAA==') + + def test_invalid_psbt_002(self): + # Case: PSBT missing outputs + with self.assertRaises(UnexpectedEndOfStream): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000000')) + with self.assertRaises(UnexpectedEndOfStream): + tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==') + + def test_invalid_psbt_003(self): + # Case: PSBT where one input has a filled scriptSig in the unsigned tx + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100fd0a010200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be4000000006a47304402204759661797c01b036b25928948686218347d89864b719e1f7fcf57d1e511658702205309eabf56aa4d8891ffd111fdf1336f3a29da866d7f8486d75546ceedaf93190121035cdc61fc7ba971c0b501a646a2a83b102cb43881217ca682dc86e2d73fa88292feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac00000000000001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb82308000000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAP0KAQIAAAACqwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QAAAAAakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpL+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAABASAA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHhwEEFgAUhdE1N/LiZUBaNNuvqePdoB+4IwgAAAA=') + + def test_invalid_psbt_004(self): + # Case: PSBT where inputs and outputs are provided but without an unsigned tx + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8AAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAA==') + + def test_invalid_psbt_005(self): + # Case: PSBT with duplicate keys in an input + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100750200000001268171371edff285e937adeea4b37b78000c0566cbb3ad64641713ca42171bf60000000000feffffff02d3dff505000000001976a914d0c59903c5bac2868760e90fd521a4665aa7652088ac00e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787b32e1300000100fda5010100000000010289a3c71eab4d20e0371bbba4cc698fa295c9463afa2e397f8533ccb62f9567e50100000017160014be18d152a9b012039daf3da7de4f53349eecb985ffffffff86f8aa43a71dff1448893a530a7237ef6b4608bbb2dd2d0171e63aec6a4890b40100000017160014fe3e9ef1a745e974d902c4355943abcb34bd5353ffffffff0200c2eb0b000000001976a91485cff1097fd9e008bb34af709c62197b38978a4888ac72fef84e2c00000017a914339725ba21efd62ac753a9bcd067d6c7a6a39d05870247304402202712be22e0270f394f568311dc7ca9a68970b8025fdd3b240229f07f8a5f3a240220018b38d7dcd314e734c9276bd6fb40f673325bc4baa144c800d2f2f02db2765c012103d2e15674941bad4a996372cb87e1856d3652606d98562fe39c5e9e7e413f210502483045022100d12b852d85dcd961d2f5f4ab660654df6eedcc794c0c33ce5cc309ffb5fce58d022067338a8e0e1725c197fb1a88af59f51e44e4255b20167c8684031c05d1f2592a01210223b72beef0965d10be0778efecd61fcac6f79a4ea169393380734464f84f2ab30000000001003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a010000000000000000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQA/AgAAAAH//////////////////////////////////////////wAAAAAA/////wEAAAAAAAAAAANqAQAAAAAAAAAA') + + def test_invalid_psbt_006(self): + # Case: PSBT With invalid global transaction typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff020001550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8CAAFVAgAAAAEnmiMjpd+1H8RfIg+liw/BPh4zQnkqhdfjbNYzO1y8OQAAAAAA/////wGgWuoLAAAAABl2qRT/6cAGEJfMO2NvLLBGD6T8Qn0rRYisAAAAAAABASCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA') + + def test_invalid_psbt_007(self): + # Case: PSBT With invalid input witness utxo typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac000000000002010020955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAIBACCVXuoLAAAAABepFGNFIA9o0YnhrcDfHE0W6o8UwNvrhyICA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GRjBDAiAEJLWO/6qmlOFVnqXJO7/UqJBkIkBVzfBwtncUaUQtBwIfXI6w/qZRbWC4rLM61k7eYOh4W/s6qUuZvfhhUduamgEBBCIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA') + + def test_invalid_psbt_008(self): + # Case: PSBT With invalid pubkey length for input partial signature typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87210203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd46304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIQIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYwQwIgBCS1jv+qppThVZ6lyTu/1KiQZCJAVc3wcLZ3FGlELQcCH1yOsP6mUW1guKyzOtZO3mDoeFv7OqlLmb34YVHbmpoBAQQiACB3H9GK1FlmbdSfPVZOPbxC9MhHdONgraFoFqjtSI1WgQEFR1IhA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GIQPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvVKuIgYDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYQtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==') + + def test_invalid_psbt_009(self): + # Case: PSBT With invalid redeemscript typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a01020400220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQIEACIAIHcf0YrUWWZt1J89Vk49vEL0yEd042CtoWgWqO1IjVaBAQVHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA') + + def test_invalid_psbt_010(self): + # Case: PSBT With invalid witnessscript typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d568102050047522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae220603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4610b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoECBQBHUiEDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUYhA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9Uq4iBgOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RhC0prpnAAAAgAAAAIAEAACAIgYD3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg70QtKa6ZwAAAIAAAACABQAAgAAA') + + def test_invalid_psbt_011(self): + # Case: PSBT With invalid bip32 typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100550200000001279a2323a5dfb51fc45f220fa58b0fc13e1e3342792a85d7e36cd6333b5cbc390000000000ffffffff01a05aea0b000000001976a914ffe9c0061097cc3b636f2cb0460fa4fc427d2b4588ac0000000000010120955eea0b0000000017a9146345200f68d189e1adc0df1c4d16ea8f14c0dbeb87220203b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd4646304302200424b58effaaa694e1559ea5c93bbfd4a89064224055cdf070b6771469442d07021f5c8eb0fea6516d60b8acb33ad64ede60e8785bfb3aa94b99bdf86151db9a9a010104220020771fd18ad459666dd49f3d564e3dbc42f4c84774e360ada16816a8ed488d5681010547522103b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd462103de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd52ae210603b1341ccba7683b6af4f1238cd6e97e7167d569fac47f1e48d47541844355bd10b4a6ba67000000800000008004000080220603de55d1e1dac805e3f8a58c1fbf9b94c02f3dbaafe127fefca4995f26f82083bd10b4a6ba670000008000000080050000800000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriEGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb0QtKa6ZwAAAIAAAACABAAAgCIGA95V0eHayAXj+KWMH7+blMAvPbqv4Sf+/KSZXyb4IIO9ELSmumcAAACAAAAAgAUAAIAAAA==') + + def test_invalid_psbt_012(self): + # Case: PSBT With invalid non-witness utxo typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f0000000000020000bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAIAALsCAAAAAarXOTEBi9JfhK5AC2iEi+CdtwbqwqwYKYur7nGrZW+LAAAAAEhHMEQCIFj2/HxqM+GzFUjUgcgmwBW9MBNarULNZ3kNq2bSrSQ7AiBKHO0mBMZzW2OT5bQWkd14sA8MWUL7n3UYVvqpOBV9ugH+////AoDw+gIAAAAAF6kUD7lGNCFpa4LIM68kHHjBfdveSTSH0PIKJwEAAAAXqRQpynT4oI+BmZQoGFyXtdhS5AY/YYdlAAAAAQfaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') + + def test_invalid_psbt_013(self): + # Case: PSBT With invalid final scriptsig typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000020700da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAACBwDaAEcwRAIgdAGK1BgAl7hzMjwAFXILNoTMgSOJEEjn282bVa1nnJkCIHPTabdA4+tT3O+jOCPIBwUUylWn3ZVE8VfBZ5EyYRGMAUgwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gFHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4AAQEgAMLrCwAAAAAXqRS39fr0Dj1ApaRZsds1NfK3L6kh6IcBByMiACCMI1MXN0O1ld+0oHtyuo5C43l9p06H/n2ddJfjsgKJAwEI2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') + + def test_invalid_psbt_014(self): + # Case: PSBT With invalid final script witness typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903020800da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAggA2gQARzBEAiBi63pVYQenxz9FrEq1od3fb3B1+xJ1lpp/OD7/94S8sgIgDAXbt0cNvy8IVX3TVscyXB7TCRPpls04QJRdsSIo2l8BRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBR1IhAwidwQx6xttU+RMpr2FzM9s4jOrQwjH3IzedG5kDCwLcIQI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8Oc1KuACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') + + def test_invalid_psbt_015(self): + # Case: PSBT With invalid pubkey in output BIP 32 derivation paths typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00210203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca58710d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABB9oARzBEAiB0AYrUGACXuHMyPAAVcgs2hMyBI4kQSOfbzZtVrWecmQIgc9Npt0Dj61Pc76M4I8gHBRTKVafdlUTxV8FnkTJhEYwBSDBFAiEA9hA4swjcHahlo0hSdG8BV3KTQgjG0kRUOTzZm98iF3cCIAVuZ1pnWm0KArhbFOXikHTYolqbV2C+ooFvZhkQoAbqAUdSIQKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfyEC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtdSrgABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEHIyIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQjaBABHMEQCIGLrelVhB6fHP0WsSrWh3d9vcHX7EnWWmn84Pv/3hLyyAiAMBdu3Rw2/LwhVfdNWxzJcHtMJE+mWzThAlF2xIijaXwFHMEQCIGX0W6WZi1mif/4ae+0BavHx+Q1Us6qPdFCqX1aiUQO9AiB/ckcDrR7blmgLKEtW1P/LiPf7dZ6rvgiqMPKbhROD0gFHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4AIQIDqaTDf1mW06ol26xrVwrwZQOUSSlCRgs1R1PtnuylhxDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA') + + def test_invalid_psbt_016(self): + # Case: PSBT With invalid input sighash type typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c0203000100000000010016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a65010125512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wCAwABAAAAAAEAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A') + + def test_invalid_psbt_017(self): + # Case: PSBT With invalid output redeemScript typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c0002000016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a65010125512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAgAAFgAUYunpgv/zTdgjlhAxawkM0qO3R8sAAQAiACCHa62DLx0WgBXtQSMqnqZaGBXZ7xPA74dZ9ktbKyeKZQEBJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A') + + def test_invalid_psbt_018(self): + # Case: PSBT With invalid output witnessScript typed key + with self.assertRaises(SerializationError): + tx1 = tx_from_any(bytes.fromhex('70736274ff0100730200000001301ae986e516a1ec8ac5b4bc6573d32f83b465e23ad76167d68b38e730b4dbdb0000000000ffffffff02747b01000000000017a91403aa17ae882b5d0d54b25d63104e4ffece7b9ea2876043993b0000000017a914b921b1ba6f722e4bfa83b6557a3139986a42ec8387000000000001011f00ca9a3b00000000160014d2d94b64ae08587eefc8eeb187c601e939f9037c00010016001462e9e982fff34dd8239610316b090cd2a3b747cb000100220020876bad832f1d168015ed41232a9ea65a1815d9ef13c0ef8759f64b5b2b278a6521010025512103b7ce23a01c5b4bf00a642537cdfabb315b668332867478ef51309d2bd57f8a8751ae00')) + with self.assertRaises(SerializationError): + tx2 = tx_from_any('cHNidP8BAHMCAAAAATAa6YblFqHsisW0vGVz0y+DtGXiOtdhZ9aLOOcwtNvbAAAAAAD/////AnR7AQAAAAAAF6kUA6oXrogrXQ1Usl1jEE5P/s57nqKHYEOZOwAAAAAXqRS5IbG6b3IuS/qDtlV6MTmYakLsg4cAAAAAAAEBHwDKmjsAAAAAFgAU0tlLZK4IWH7vyO6xh8YB6Tn5A3wAAQAWABRi6emC//NN2COWEDFrCQzSo7dHywABACIAIIdrrYMvHRaAFe1BIyqeploYFdnvE8Dvh1n2S1srJ4plIQEAJVEhA7fOI6AcW0vwCmQlN836uzFbZoMyhnR471EwnSvVf4qHUa4A') + + +class TestPSBTSignerChecks(TestCaseForTestnet): + # test cases from BIP-0174 + + @unittest.skip("the check this test is testing is intentionally disabled in transaction.py") + def test_psbt_fails_signer_checks_001(self): + # Case: A Witness UTXO is provided for a non-witness input + with self.assertRaises(PSBTInputConsistencyFailure): + tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff0100a00200000002ab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40000000000feffffffab0949a08c5af7c49b8212f417e2f15ab3f5c33dcf153821a8139f877a5b7be40100000000feffffff02603bea0b000000001976a914768a40bbd740cbe81d988e71de2a4d5c71396b1d88ac8e240000000000001976a9146f4620b553fa095e721b9ee0efe9fa039cca459788ac0000000000010122d3dff505000000001976a914d48ed3110b94014cb114bd32d6f4d066dc74256b88ac0001012000e1f5050000000017a9143545e6e33b832c47050f24d3eeb93c9c03948bc787010416001485d13537f2e265405a34dbafa9e3dda01fb8230800220202ead596687ca806043edc3de116cdf29d5e9257c196cd055cf698c8d02bf24e9910b4a6ba670000008000000080020000800022020394f62be9df19952c5587768aeb7698061ad2c4a25c894f47d8c162b4d7213d0510b4a6ba6700000080010000800200008000')) + for txin in tx1.inputs(): + txin.validate_data(for_signing=True) + with self.assertRaises(PSBTInputConsistencyFailure): + tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEBItPf9QUAAAAAGXapFNSO0xELlAFMsRS9Mtb00GbcdCVriKwAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=') + for txin in tx2.inputs(): + txin.validate_data(for_signing=True) + + def test_psbt_fails_signer_checks_002(self): + # Case: redeemScript with non-witness UTXO does not match the scriptPubKey + with self.assertRaises(PSBTInputConsistencyFailure): + tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752af2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(PSBTInputConsistencyFailure): + tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq8iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') + + def test_psbt_fails_signer_checks_003(self): + # Case: redeemScript with witness UTXO does not match the scriptPubKey + with self.assertRaises(PSBTInputConsistencyFailure): + tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028900010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(PSBTInputConsistencyFailure): + tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQABBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSriIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') + + def test_psbt_fails_signer_checks_004(self): + # Case: witnessScript with witness UTXO does not match the redeemScript + with self.assertRaises(PSBTInputConsistencyFailure): + tx1 = PartialTransaction.from_raw_psbt(bytes.fromhex('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ad2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000')) + with self.assertRaises(PSBTInputConsistencyFailure): + tx2 = PartialTransaction.from_raw_psbt('cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAAiAgLath/0mhTban0CsM0fu3j8SxgxK1tOVNrk26L7/vU210gwRQIhAPYQOLMI3B2oZaNIUnRvAVdyk0IIxtJEVDk82ZvfIhd3AiAFbmdaZ1ptCgK4WxTl4pB02KJam1dgvqKBb2YZEKAG6gEBAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohyICAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zRzBEAiBl9FulmYtZon/+GnvtAWrx8fkNVLOqj3RQql9WolEDvQIgf3JHA60e25ZoCyhLVtT/y4j3+3Weq74IqjDym4UTg9IBAQMEAQAAAAEEIgAgjCNTFzdDtZXftKB7crqOQuN5fadOh/59nXSX47ICiQMBBUdSIQMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3CECOt2QTz1tz1nduQaw3uI1Kbf/ue1Q5ehhUZJoYCIfDnNSrSIGAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zENkMak8AAACAAAAAgAMAAIAiBgMIncEMesbbVPkTKa9hczPbOIzq0MIx9yM3nRuZAwsC3BDZDGpPAAAAgAAAAIACAACAACICA6mkw39ZltOqJdusa1cK8GUDlEkpQkYLNUdT7Z7spYdxENkMak8AAACAAAAAgAQAAIAAIgICf2OZdX0u/1WhNq0CxoSxg4tlVuXxtrNCgqlLa1AFEJYQ2QxqTwAAAIAAAACABQAAgAA=') + + +class TestPSBTComplexChecks(TestCaseForTestnet): + # test cases from BIP-0174 + + def test_psbt_combiner_unknown_fields(self): + tx1 = tx_from_any("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f00") + tx2 = tx_from_any("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00") + tx1.combine_with_other_psbt(tx2) + self.assertEqual("70736274ff01003f0200000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff010000000000000000036a0100000000000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f000a0f0102030405060708090f0102030405060708090a0b0c0d0e0f0a0f0102030405060708100f0102030405060708090a0b0c0d0e0f00", + tx1.serialize_as_bytes().hex()) diff --git a/electrum/tests/test_transaction.py b/electrum/tests/test_transaction.py @@ -1,13 +1,19 @@ from electrum import transaction -from electrum.transaction import TxOutputForUI, tx_from_str +from electrum.transaction import convert_tx_str_to_hex, tx_from_any, Transaction, PartialTransaction from electrum.bitcoin import TYPE_ADDRESS -from electrum.keystore import xpubkey_to_address from electrum.util import bh2u, bfh +from electrum import keystore +from electrum import bip32 +from electrum.mnemonic import seed_type +from electrum.simple_config import SimpleConfig + + +from electrum.plugins.trustedcoin import trustedcoin +from electrum.plugins.trustedcoin.legacy_tx_format import serialize_tx_in_legacy_format from . import ElectrumTestCase, TestCaseForTestnet from .test_bitcoin import needs_test_with_all_ecc_implementations -unsigned_blob = '45505446ff0001000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000" @@ -58,80 +64,35 @@ class TestBCDataStream(ElectrumTestCase): class TestTransaction(ElectrumTestCase): @needs_test_with_all_ecc_implementations - def test_tx_unsigned(self): - expected = { - 'inputs': [{ - 'type': 'p2pkh', - 'address': '1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD', - 'num_sig': 1, - 'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a', - 'prevout_n': 0, - 'pubkeys': ['02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'], - 'scriptSig': '01ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000', - 'sequence': 4294967295, - 'signatures': [None], - 'x_pubkeys': ['ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000']}], - 'lockTime': 0, - 'outputs': [{ - 'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', - 'prevout_n': 0, - 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', - 'type': TYPE_ADDRESS, - 'value': 1000000}], - 'partial': True, - 'segwit_ser': False, - 'version': 1, - } - tx = transaction.Transaction(unsigned_blob) - self.assertEqual(tx.deserialize(), expected) - self.assertEqual(tx.deserialize(), None) - - self.assertEqual(tx.as_dict(), {'hex': unsigned_blob, 'complete': False, 'final': True}) - self.assertEqual(tx.get_outputs_for_UI(), [TxOutputForUI('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', 1000000)]) - - self.assertTrue(tx.has_address('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs')) - self.assertTrue(tx.has_address('1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD')) - self.assertFalse(tx.has_address('1CQj15y1N7LDHp7wTt28eoD1QhHgFgxECH')) - - self.assertEqual(tx.serialize(), unsigned_blob) - + def test_tx_update_signatures(self): + tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA") + tx.inputs()[0].script_type = 'p2pkh' + tx.inputs()[0].pubkeys = [bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')] + tx.inputs()[0].num_sig = 1 tx.update_signatures(signed_blob_signatures) - self.assertEqual(tx.raw, signed_blob) - - tx.update(unsigned_blob) - tx.raw = None - blob = str(tx) - self.assertEqual(transaction.deserialize(blob), expected) + self.assertEqual(tx.serialize(), signed_blob) @needs_test_with_all_ecc_implementations - def test_tx_signed(self): - expected = { - 'inputs': [{'address': None, - 'num_sig': 0, - 'prevout_hash': '3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a', - 'prevout_n': 0, - 'scriptSig': '493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6', - 'sequence': 4294967295, - 'type': 'unknown'}], - 'lockTime': 0, - 'outputs': [{ - 'address': '14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', - 'prevout_n': 0, - 'scriptPubKey': '76a914230ac37834073a42146f11ef8414ae929feaafc388ac', - 'type': TYPE_ADDRESS, - 'value': 1000000}], - 'partial': False, - 'segwit_ser': False, - 'version': 1 - } + def test_tx_deserialize_for_signed_network_tx(self): tx = transaction.Transaction(signed_blob) - self.assertEqual(tx.deserialize(), expected) - self.assertEqual(tx.deserialize(), None) - self.assertEqual(tx.as_dict(), {'hex': signed_blob, 'complete': True, 'final': True}) + tx.deserialize() + self.assertEqual(1, tx.version) + self.assertEqual(0, tx.locktime) + self.assertEqual(1, len(tx.inputs())) + self.assertEqual(4294967295, tx.inputs()[0].nsequence) + self.assertEqual(bfh('493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6'), + tx.inputs()[0].script_sig) + self.assertEqual(None, tx.inputs()[0].witness) + self.assertEqual('3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0', tx.inputs()[0].prevout.to_str()) + self.assertEqual(1, len(tx.outputs())) + self.assertEqual(bfh('76a914230ac37834073a42146f11ef8414ae929feaafc388ac'), tx.outputs()[0].scriptpubkey) + self.assertEqual('14CHYaaByjJZpx4oHBpfDMdqhTyXnZ3kVs', tx.outputs()[0].address) + self.assertEqual(1000000, tx.outputs()[0].value) self.assertEqual(tx.serialize(), signed_blob) - tx.update_signatures(signed_blob_signatures) + def test_estimated_tx_size(self): + tx = transaction.Transaction(signed_blob) self.assertEqual(tx.estimated_total_size(), 193) self.assertEqual(tx.estimated_base_size(), 193) @@ -156,73 +117,84 @@ class TestTransaction(ElectrumTestCase): self.assertEqual(tx.estimated_weight(), 561) self.assertEqual(tx.estimated_size(), 141) - def test_errors(self): - with self.assertRaises(TypeError): - transaction.Transaction.pay_script(output_type=None, addr='') - - with self.assertRaises(BaseException): - xpubkey_to_address('') - - def test_parse_xpub(self): - res = xpubkey_to_address('fe4e13b0f311a55b8a5db9a32e959da9f011b131019d4cebe6141b9e2c93edcbfc0954c358b062a9f94111548e50bde5847a3096b8b7872dcffadb0e9579b9017b01000200') - self.assertEqual(res, ('04ee98d63800824486a1cf5b4376f2f574d86e0a3009a6448105703453f3368e8e1d8d090aaecdd626a45cc49876709a3bbb6dc96a4311b3cac03e225df5f63dfc', '19h943e4diLc68GXW7G75QNe2KWuMu7BaJ')) - def test_version_field(self): tx = transaction.Transaction(v2_blob) self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe") - def test_tx_from_str(self): - # json dict - self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600', - tx_from_str("""{ - "complete": true, - "final": false, - "hex": "020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600" - } - """) - ) + def test_convert_tx_str_to_hex(self): # raw hex self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600', - tx_from_str('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600')) + convert_tx_str_to_hex('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600')) # base43 self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600', - tx_from_str('64XF-8+PM6*4IYN-QWW$B2QLNW+:C8-$I$-+T:L.6DKXTSWSFFONDP1J/MOS3SPK0-SYVW38U9.3+A1/*2HTHQTJGP79LVEK-IITQJ1H.C/X$NSOV$8DWR6JAFWXD*LX4-EN0.BDOF+PPYPH16$NM1H.-MAA$V1SCP0Q.6Y5FR822S6K-.5K5F.Z4Q:0SDRG-4GEBLAO4W9Z*H-$1-KDYAFOGF675W0:CK5M1LT92IG:3X60P3GKPM:X2$SP5A7*LT9$-TTEG0/DRZYV$7B4ADL9CVS5O7YG.J64HLZ24MVKO/-GV:V.T/L$D3VQ:MR8--44HK8W')) + convert_tx_str_to_hex('64XF-8+PM6*4IYN-QWW$B2QLNW+:C8-$I$-+T:L.6DKXTSWSFFONDP1J/MOS3SPK0-SYVW38U9.3+A1/*2HTHQTJGP79LVEK-IITQJ1H.C/X$NSOV$8DWR6JAFWXD*LX4-EN0.BDOF+PPYPH16$NM1H.-MAA$V1SCP0Q.6Y5FR822S6K-.5K5F.Z4Q:0SDRG-4GEBLAO4W9Z*H-$1-KDYAFOGF675W0:CK5M1LT92IG:3X60P3GKPM:X2$SP5A7*LT9$-TTEG0/DRZYV$7B4ADL9CVS5O7YG.J64HLZ24MVKO/-GV:V.T/L$D3VQ:MR8--44HK8W')) def test_get_address_from_output_script(self): # the inverse of this test is in test_bitcoin: test_address_to_script addr_from_script = lambda script: transaction.get_address_from_output_script(bfh(script)) - ADDR = transaction.TYPE_ADDRESS - PUBKEY = transaction.TYPE_PUBKEY - SCRIPT = transaction.TYPE_SCRIPT # bech32 native segwit # test vectors from BIP-0173 - self.assertEqual((ADDR, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6')) - self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) - self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e')) - self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323')) + self.assertEqual('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6')) + self.assertEqual('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx', addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) + self.assertEqual('bc1sw50qa3jx3s', addr_from_script('6002751e')) + self.assertEqual('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj', addr_from_script('5210751e76e8199196d454941c45d1b3a323')) # almost but not quite - self.assertEqual((SCRIPT, '0013751e76e8199196d454941c45d1b3a323f1433b'), addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b')) + self.assertEqual(None, addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b')) # base58 p2pkh - self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) - self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) + self.assertEqual('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG', addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) + self.assertEqual('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv', addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) # almost but not quite - self.assertEqual((SCRIPT, '76a9130000000000000000000000000000000000000088ac'), addr_from_script('76a9130000000000000000000000000000000000000088ac')) + self.assertEqual(None, addr_from_script('76a9130000000000000000000000000000000000000088ac')) # base58 p2sh - self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) - self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) + self.assertEqual('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT', addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) + self.assertEqual('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji', addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) # almost but not quite - self.assertEqual((SCRIPT, 'a912f47c8954e421031ad04ecd8e7752c947920687'), addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687')) + self.assertEqual(None, addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687')) # p2pk - self.assertEqual((PUBKEY, '0289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8b'), addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) - self.assertEqual((PUBKEY, '045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120'), addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac')) + self.assertEqual(None, addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) + self.assertEqual(None, addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac')) # almost but not quite - self.assertEqual((SCRIPT, '200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'), addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac')) - self.assertEqual((SCRIPT, '210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'), addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) - + self.assertEqual(None, addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac')) + self.assertEqual(None, addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) + + def test_tx_serialize_methods_for_psbt(self): + raw_hex = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000" + raw_base64 = "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAAAAEAuwIAAAABqtc5MQGL0l+ErkALaISL4J23BurCrBgpi6vucatlb4sAAAAASEcwRAIgWPb8fGoz4bMVSNSByCbAFb0wE1qtQs1neQ2rZtKtJDsCIEoc7SYExnNbY5PltBaR3XiwDwxZQvufdRhW+qk4FX26Af7///8CgPD6AgAAAAAXqRQPuUY0IWlrgsgzryQceMF9295JNIfQ8gonAQAAABepFCnKdPigj4GZlCgYXJe12FLkBj9hh2UAAAABAwQBAAAAAQRHUiEClYO/Oa4KYJdHrRma3dY0+mEIVZ1sXNObTCGD8auW4H8hAtq2H/SaFNtqfQKwzR+7ePxLGDErW05U2uTbovv+9TbXUq4iBgKVg785rgpgl0etGZrd1jT6YQhVnWxc05tMIYPxq5bgfxDZDGpPAAAAgAAAAIAAAACAIgYC2rYf9JoU22p9ArDNH7t4/EsYMStbTlTa5Nui+/71NtcQ2QxqTwAAAIAAAACAAQAAgAABASAAwusLAAAAABepFLf1+vQOPUClpFmx2zU18rcvqSHohwEDBAEAAAABBCIAIIwjUxc3Q7WV37Sge3K6jkLjeX2nTof+fZ10l+OyAokDAQVHUiEDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwhAjrdkE89bc9Z3bkGsN7iNSm3/7ntUOXoYVGSaGAiHw5zUq4iBgI63ZBPPW3PWd25BrDe4jUpt/+57VDl6GFRkmhgIh8OcxDZDGpPAAAAgAAAAIADAACAIgYDCJ3BDHrG21T5EymvYXMz2ziM6tDCMfcjN50bmQMLAtwQ2QxqTwAAAIAAAACAAgAAgAAiAgOppMN/WZbTqiXbrGtXCvBlA5RJKUJGCzVHU+2e7KWHcRDZDGpPAAAAgAAAAIAEAACAACICAn9jmXV9Lv9VoTatAsaEsYOLZVbl8bazQoKpS2tQBRCWENkMak8AAACAAAAAgAUAAIAA" + partial_tx = tx_from_any(raw_hex) + self.assertEqual(PartialTransaction, type(partial_tx)) + self.assertEqual(raw_base64, + partial_tx.serialize()) + self.assertEqual(raw_hex, + partial_tx.serialize_as_bytes().hex()) + self.assertEqual(raw_base64, + partial_tx._serialize_as_base64()) + + def test_tx_serialize_methods_for_network_tx(self): + raw_hex = "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" + tx = tx_from_any(raw_hex) + self.assertEqual(Transaction, type(tx)) + self.assertEqual(raw_hex, + tx.serialize()) + self.assertEqual(raw_hex, + tx.serialize_as_bytes().hex()) + + def test_tx_serialize_methods_for_psbt_that_is_ready_to_be_finalized(self): + raw_hex_psbt = "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000" + raw_hex_network_tx = "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" + partial_tx = tx_from_any(raw_hex_psbt) + self.assertEqual(PartialTransaction, type(partial_tx)) + self.assertEqual(raw_hex_network_tx, + partial_tx.serialize()) + self.assertEqual(raw_hex_network_tx, + partial_tx.serialize_as_bytes().hex()) + # note: the diff between the following, and raw_hex_psbt, is that we added + # an extra FINAL_SCRIPTWITNESS field in finalize_psbt() + self.assertEqual("70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae010801000001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000", + partial_tx.serialize_as_bytes(force_psbt=True).hex()) ##### @@ -811,45 +783,54 @@ class TestTransaction(ElectrumTestCase): # txns from Bitcoin Core ends <--- -class TestTransactionTestnet(TestCaseForTestnet): - - def _run_naive_tests_on_tx(self, raw_tx, txid): - tx = transaction.Transaction(raw_tx) - self.assertEqual(txid, tx.txid()) - self.assertEqual(raw_tx, tx.serialize()) - self.assertTrue(tx.estimated_size() >= 0) - -# partial txns using our partial format ---> - # NOTE: our partial format contains xpubs, and xpubs have version bytes, - # and version bytes encode the network as well; so these are network-sensitive! - - def test_txid_partial_segwit_p2wpkh(self): - raw_tx = '45505446ff000100000000010115a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff02f6fd1200000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600140f9de573bc679d040e763d13f0250bd03e625f6ffeffffffff9095ab000000000000000201ff53ff045f1cf6014af5fa07800000002fa3f450ba41799b9b62642979505817783a9b6c656dc11cd0bb4fa362096808026adc616c25a4d0a877d1741eb1db9cef65c15118bd7d5f31bf65f319edda81840100c8000f391400' - txid = '63ff7e99d85d8e33f683e6ec84574bdf8f5111078a5fe900893e019f9a7f95c3' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_partial_segwit_p2wpkh_p2sh_simple(self): - raw_tx = '45505446ff0001000000000101d0d23a6fbddb21cc664cb81cca96715baa4d6dbe5b7b9bcc6632f1005a7b0b840100000017160014a78a91261e71a681b6312cd184b14503a21f856afdffffff0134410f000000000017a914d6514ca17ecc31952c990daf96e307fbc58529cd87feffffffff40420f000000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c30000020011391400' - txid = '2739f2e7fde9b8ec73fce4aee53722cc7683312d1321ded073284c51fadf44df' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_partial_segwit_p2wpkh_p2sh_mixed_outputs(self): - raw_tx = '45505446ff00010000000001011dcac788f24b84d771b60c44e1f9b6b83429e50f06e1472d47241922164013b00100000017160014801d28ca6e2bde551112031b6cb75de34f10851ffdffffff0440420f00000000001600140f9de573bc679d040e763d13f0250bd03e625f6fc0c62d000000000017a9142899f6484e477233ce60072fc185ef4c1f2c654487809698000000000017a914d40f85ba3c8fa0f3615bcfa5d6603e36dfc613ef87712d19040000000017a914e38c0cffde769cb65e72cda1c234052ae8d2254187feffffffff6ad1ee040000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c301000c000f391400' - txid = 'ba5c88e07a4025a39ad3b85247cbd4f556a70d6312b18e04513c7cec9d45d6ac' - self._run_naive_tests_on_tx(raw_tx, txid) - - def test_txid_partial_issue_5366(self): - raw_tx = '45505446ff000200000000010127523d70642dabd999fb43191ff6763f5b04150ba4cf38d2cfb53edf6a40ac4f0100000000fdffffff013286010000000000160014e79c7ac0b390a9caf52dc002e1095a5fbc042a18feffffffffa08601000000000000000201ff57ff045f1cf60157e9eb7a8000000038fa0b3a9c155ff3390ca0d639783d97af3b3bf66ebb69a31dfe8317fae0a7fe0324bc048fc0002253dfec9d6299711d708175f950ecee8e09db3518a5685741830000ffffcf01010043281700' - txid = 'a0c159616073dc7a4a482092dab4e8516c83dddb769b65919f23f6df63d33eb8' - self._run_naive_tests_on_tx(raw_tx, txid) - -# end partial txns <--- - - -class NetworkMock(object): - - def __init__(self, unspent): - self.unspent = unspent - - def synchronous_send(self, arg): - return self.unspent +class TestLegacyPartialTxFormat(TestCaseForTestnet): + + def setUp(self): + super().setUp() + self.config = SimpleConfig({'electrum_path': self.electrum_path}) + + def test_trustedcoin_legacy_2fa_psbt_to_legacy_partial_tx(self): + from .test_wallet_vertical import WalletIntegrityHelper + seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove' + self.assertEqual(seed_type(seed_words), '2fa') + + xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '') + ks1 = keystore.from_xprv(xprv1) + ks2 = keystore.from_xprv(xprv2) + long_user_id, short_id = trustedcoin.get_user_id( + {'x1/': {'xpub': xpub1}, + 'x2/': {'xpub': xpub2}}) + xtype = bip32.xpub_type(xpub1) + xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id) + ks3 = keystore.from_xpub(xpub3) + + wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config) + + tx = tx_from_any('cHNidP8BAJQCAAAAAcqqxrXrkW4wZ9AiT5QvszHOHc+0Axz7R555Qdz5XkCYAQAAAAD9////A6CGAQAAAAAAFgAU+fBLRlKk9v89xVEm2xJ0kG1wcvNMCwMAAAAAABepFPKffLiXEB3Gmv1Y35uy5bTUM59Nh0ANAwAAAAAAGXapFPriyJZefiOenIisUU3nDewLDxYIiKwSKxgATwEENYfPAAAAAAAAAAAAnOMnCVq57ruCJ7c38H6PtmrwS48+kcQJPEh70w/ofCQCDSEN062A0pw2JKkYltX2G3th8zLexPfEVDGu74BeD6cEcH3xxE8BBDWHzwGCB4l2gAAAAJOfYJjOAH6kksFOokIboP3+8Gwhlzlxhl5uY7zokvfcAmGy8e8txy0wkx69/TgZFOMe1aZc2g1HCwrRQ9M9+Ph7CIIHiXYAAACATwEENYfPAYIHiXaAAAABb6EovcClpG/Hrxr9IF22IHGR1MQFG27b0GQTzcCxot8Dak5MvnvEZt1lN4TIazd0m+w+goApzqNMFWkJVv1hV28IggeJdgEAAIAAAQDfAgAAAAGcKHw7enlMh6IibIkEeKQlL5pUR2wKv6GC1NTd6KY8ggEAAABqRzBEAiBNHsG9H5z10eHHsIOe4kFdvnZK38E7Jx+Cmru14SdQ/gIgWngNYj/F8qHAhkdlU+BgY5ktAL2MeIUoqIJKXudcFRMBIQNU856KX8nmKx8+nbIRwjpRAvyMWroJGz+F6ADwzYv/GP3///8CnJ0HAAAAAAAZdqkUsQVAb+BbDci+RMeDa6WBLb9nTOiIrCChBwAAAAAAF6kULBYX0k+TbkDRSw3ylOy3u6rXUzeHEisYACICA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiSDBFAiEA0Dw1yyk7Adp74Ndxztr6iR7V1wpnfPzNaWcTVva+vtwCIAsqV2xM0cZCSAdWzh/WYKyvC6UmGTowmeH4HN0BrSCTAQEEaVIhAgkfC02KswAWpdHAiCSeAog/rYFg8G+lNYithZhlCj5iIQNfL4JjuzYI1sxO4DvUy41lxNcK9xBJ8F+/7kl4gyof0iED/kLatYcY6gQT98jeaTze7iLOGbHcNMC73XpIJFRlxaJTriIGA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiEIIHiXYAAACAAAAAAAAAAAAiBgIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YhCCB4l2AQAAgAAAAAAAAAAAIgYDXy+CY7s2CNbMTuA71MuNZcTXCvcQSfBfv+5JeIMqH9IMcH3xxAAAAAAAAAAAAAABAGlSIQIqtnn9ouM3xAq7wIID09cdKpb9u/OMkI97kuU3wcTv/yEDTNFHFnZ1xmKGRQzFaUAT9DeDk2NdeWSrilc8w9BKjU4hA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/U64iAgNM0UcWdnXGYoZFDMVpQBP0N4OTY115ZKuKVzzD0EqNThCCB4l2AAAAgAEAAAAAAAAAIgICKrZ5/aLjN8QKu8CCA9PXHSqW/bvzjJCPe5LlN8HE7/8QggeJdgEAAIABAAAAAAAAACICA1joWoIBRYfeqDPrX/uT45hWkO5Lph7zLVsorVqhYXN/DHB98cQBAAAAAAAAAAAA') + tx.add_info_from_wallet(wallet) + raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet) + self.assertEqual('45505446ff000200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fd53010001ff01ff483045022100d03c35cb293b01da7be0d771cedafa891ed5d70a677cfccd69671356f6bebedc02200b2a576c4cd1c642480756ce1fd660acaf0ba526193a3099e1f81cdd01ad2093014d0201524c53ff043587cf0182078976800000016fa128bdc0a5a46fc7af1afd205db6207191d4c4051b6edbd06413cdc0b1a2df036a4e4cbe7bc466dd653784c86b37749bec3e828029cea34c15690956fd61576f000000004c53ff043587cf0000000000000000009ce327095ab9eebb8227b737f07e8fb66af04b8f3e91c4093c487bd30fe87c24020d210dd3ad80d29c3624a91896d5f61b7b61f332dec4f7c45431aeef805e0fa7000000004c53ff043587cf018207897680000000939f6098ce007ea492c14ea2421ba0fdfef06c21973971865e6e63bce892f7dc0261b2f1ef2dc72d30931ebdfd381914e31ed5a65cda0d470b0ad143d33df8f87b0000000053aefdffffff03a086010000000000160014f9f04b4652a4f6ff3dc55126db1274906d7072f34c0b03000000000017a914f29f7cb897101dc69afd58df9bb2e5b4d4339f4d87400d0300000000001976a914fae2c8965e7e239e9c88ac514de70dec0b0f160888ac122b1800', + raw_tx) + + def test_trustedcoin_segwit_2fa_psbt_to_legacy_partial_tx(self): + from .test_wallet_vertical import WalletIntegrityHelper + seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise' + self.assertEqual(seed_type(seed_words), '2fa_segwit') + + xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '') + ks1 = keystore.from_xprv(xprv1) + ks2 = keystore.from_xprv(xprv2) + long_user_id, short_id = trustedcoin.get_user_id( + {'x1/': {'xpub': xpub1}, + 'x2/': {'xpub': xpub2}}) + xtype = bip32.xpub_type(xpub1) + xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id) + ks3 = keystore.from_xpub(xpub3) + + wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config) + + tx = tx_from_any('70736274ff01009f020000000187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788ac132b18004f0102575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80044bf212094f010257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee08d24cd591010000804f010257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c08d24cd591000000800001012b20a1070000000000220020a948d7fa6abbb97e31779ae54383012b413d53821c7fd394900f6b443c61deee22020307a3c41d07ed976d65e213e823d02840937475e709b41253e85e970e3cb1667447304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c001010569522102a9dcb570e8280c741f09032c158095b7aa3b0ce401ada030f2d47b999f020606210307a3c41d07ed976d65e213e823d02840937475e709b41253e85e970e3cb166742103521b0a45e042f08ccd03af47fd88bb207b5414e0e30bb8799fca311a06323a1953ae22060307a3c41d07ed976d65e213e823d02840937475e709b41253e85e970e3cb1667410d24cd591000000800000000001000000220603521b0a45e042f08ccd03af47fd88bb207b5414e0e30bb8799fca311a06323a1910d24cd591010000800000000001000000220602a9dcb570e8280c741f09032c158095b7aa3b0ce401ada030f2d47b999f0206060c4bf212090000000001000000000001016952210205829f9522577122ca9ca9beb67f94cf2fe0ad0d17e052a110701cf4128d339c21022cca282cfdd9cc1f387d3098661d68bc9c8a39ac6bd72c30db579a7857d0859b2102d90e3c8973844b9cc0b38494c48515a319eec3ac2489f96f6f55e6aa6912a60853ae220202d90e3c8973844b9cc0b38494c48515a319eec3ac2489f96f6f55e6aa6912a60810d24cd5910000008001000000000000002202022cca282cfdd9cc1f387d3098661d68bc9c8a39ac6bd72c30db579a7857d0859b10d24cd59101000080010000000000000022020205829f9522577122ca9ca9beb67f94cf2fe0ad0d17e052a110701cf4128d339c0c4bf2120901000000000000000000') + tx.add_info_from_wallet(wallet) + raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet) + self.assertEqual('45505446ff000200000000010187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788acfeffffffff20a10700000000000000050001ff47304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c00101fffd0201524c53ff02575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80000001004c53ff0257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c000001004c53ff0257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee0000010053ae132b1800', + raw_tx) diff --git a/electrum/tests/test_wallet.py b/electrum/tests/test_wallet.py @@ -222,7 +222,7 @@ class TestCreateRestoreWallet(WalletTestCase): addr0 = wallet.get_receiving_addresses()[0] self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', addr0) self.assertEqual('p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', - wallet.export_private_key(addr0, password=None)[0]) + wallet.export_private_key(addr0, password=None)) self.assertEqual(2, len(wallet.get_receiving_addresses())) # also test addr deletion wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c') diff --git a/electrum/tests/test_wallet_vertical.py b/electrum/tests/test_wallet_vertical.py @@ -11,7 +11,7 @@ from electrum import SimpleConfig from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT from electrum.wallet import sweep, Multisig_Wallet, Standard_Wallet, Imported_Wallet, restore_wallet_from_text from electrum.util import bfh, bh2u -from electrum.transaction import TxOutput +from electrum.transaction import TxOutput, Transaction, PartialTransaction, PartialTxOutput, PartialTxInput, tx_from_any from electrum.mnemonic import seed_type from electrum.plugins.trustedcoin import trustedcoin @@ -573,14 +573,14 @@ class TestWalletSending(TestCaseForTestnet): wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 250000)] + outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 250000)] tx = wallet1.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet1.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1.is_mine(wallet1.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('010000000001010392c1940e2ec9f2372919ca3887327fe5b98b866022cc79bab5cbed5a53d2ad0000000000feffffff0290d00300000000001976a914ea7804a2c266063572cc009a63dc25dcc0e9d9b588ac285e0b0000000000160014690b59a8140602fb23cc2904ece9cc4daf361052024730440220608a5339ca894592da82119e1e4a1d09335d70a552c683687223b8ed724465e902201b3f0feccf391b1b6257e4b18970ae57d7ca060af2dae519b3690baad2b2a34e0121030faee9b4a25b7db82023ca989192712cdd4cb53d3d9338591c7909e581ae1c0c00000000', @@ -593,14 +593,14 @@ class TestWalletSending(TestCaseForTestnet): wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1.get_receiving_address(), 100000)] + outputs = [PartialTxOutput.from_address_and_value(wallet1.get_receiving_address(), 100000)] tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('0100000001e228327e4c0bb80661d258d625f516307e7c127c7f3e2b476a22e89b4dae063c000000006b483045022100d3895b31e7c9766987c6f53794c7394f534f4acecefda5479d963236f9703d0b022026dd4e40700ceb788f136faf54bf85b966648dc7c2a608d8110604f2d22d59070121030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cffeffffff02a0860100000000001600148a28bddb7f61864bdcf58b2ad13d5aeb3abc3c4268360200000000001976a914ca4c60999c46c2108326590b125aefd476dcb11888ac00000000', @@ -648,17 +648,20 @@ class TestWalletSending(TestCaseForTestnet): wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 370000)] + outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 370000)] tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) - tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007501000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb20100000000feffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000000100e0010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f28483045022100f9ce5616683e613ae14b98d56436454b003348a8172e2ed598018e3d206e57d7022030c65c6551e839f9e9409812be624dbb4e36bd4152c9ed9b0988c10fd8201d1401010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000000100695221022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be21024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98582102b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a253ae2202022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be0cdb69242701000000000000002202024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98580c0036e9ac0100000000000000220202b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a20c48adc7a0010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) wallet1b.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('01000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb201000000fdfe0000483045022100f9ce5616683e613ae14b98d56436454b003348a8172e2ed598018e3d206e57d7022030c65c6551e839f9e9409812be624dbb4e36bd4152c9ed9b0988c10fd8201d1401483045022100d5cb94d4d1dcf01bb9e9280e8178a7e9ada3ad14378ca543afcc9f5667b27cb2022018e76b74800a21934e73b226b34cbbe45c877fba64693da8a20d3cb330f2eafd014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefeffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000', @@ -671,14 +674,14 @@ class TestWalletSending(TestCaseForTestnet): wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] + outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('01000000015df26ee0f55487ca29727c50dbf0ce2227d3e3eb44621219ff1c2e40d0bdf326000000008b483045022100bd9f61ba82507d3a28922fb8be129e14699dfa54ddd03cc9494f696d38ac4121022071afca6fad5bc5c09b0a675e6444be3e97dbbdbc283764ee5f4e27a032d933d80141045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25edfeffffff02a08601000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887280b0400000000001976a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac00000000', @@ -743,10 +746,13 @@ class TestWalletSending(TestCaseForTestnet): wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2a.get_receiving_address(), 165000)] + outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)] tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) txid = tx.txid() - tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa483045022100ea2fbd3d8681cfafdcae1bdaaa64f92fb9872fb8f6bf03a2b7effcf7390b66c8022021a79eff7975479934f958f3766d6ac61d708c79b785e398b3bcd84b1039e9b50101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa0cb5c37672000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e0c043b02bf0000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9410c3b1da3ef000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a0c043b02bf0100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd50c3b1da3ef0100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a9800cb5c3767201000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertEqual(txid, tx.txid()) self.assertFalse(tx.is_complete()) wallet1b.sign_transaction(tx, password=None) @@ -754,8 +760,8 @@ class TestWalletSending(TestCaseForTestnet): self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('01000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400483045022100ea2fbd3d8681cfafdcae1bdaaa64f92fb9872fb8f6bf03a2b7effcf7390b66c8022021a79eff7975479934f958f3766d6ac61d708c79b785e398b3bcd84b1039e9b501483045022100dbc4f1ec18f0e0deb4ff88d7d5b3d3b7b500a80d0c0f33efbd3262f0c8689095022074fd226c0b52e3716ad907d14cba9c79aca482a8f4a51662ca83a5b9db49e15b016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000', @@ -769,10 +775,13 @@ class TestWalletSending(TestCaseForTestnet): wallet2a.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 100000)] + outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) txid = tx.txid() - tx = Transaction(tx.serialize()) # simulates moving partial txn between cosigners + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a0860100000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb1483045022100c254468bbe6b8bd1c8c01b6a223e46cc5c6b56fbba87d59575385ad249133b0e02207139688f8d6ae8076c92a266d98454d25c040d04c8e513a37bf7c32dad3e48210101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertEqual(txid, tx.txid()) self.assertFalse(tx.is_complete()) wallet2b.sign_transaction(tx, password=None) @@ -780,8 +789,8 @@ class TestWalletSending(TestCaseForTestnet): self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet2a.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2a.is_mine(wallet2a.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('0100000000010149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e01000000232200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163cfeffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a0860100000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0400483045022100c254468bbe6b8bd1c8c01b6a223e46cc5c6b56fbba87d59575385ad249133b0e02207139688f8d6ae8076c92a266d98454d25c040d04c8e513a37bf7c32dad3e48210147304402204af5edbab2d674f6a9edef8c97b2f7fdf8ababedc7b287710cc7a64d4699358b022064e2d07f4bb32373be31b2003dc56b7b831a7c01419326efb3011c64b898b3f00147522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae00000000', @@ -825,14 +834,14 @@ class TestWalletSending(TestCaseForTestnet): wallet1a.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # wallet1 -> wallet2 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet2.get_receiving_address(), 1000000)] + outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 1000000)] tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet1a.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet1a.is_mine(wallet1a.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('0100000001a391c8b3d4a551eac85714f3f0a7514381c014ba4688de085b0fcee42dc13711010000009200483045022100fcf03aeb97b66791372c18aa0dd651817cf458d941dd628c966f0305a023360f022016c534530e267b6a52f90e62aa9fb50ace609ffb21e472d3ba7b29db9b30050e014751210245c90e040d4f9d1fc136b3d4d6b7535bbb5df2bd27666c21977042cc1e05b5b02103c9a6bebfce6294488315e58137a279b2efe09f1f528ecf93b40675ded3cf0e5f52aefeffffff0240420f000000000017a9149573eb50f3136dff141ac304190f41c8becc92ce8738b32d000000000017a914b815d1b430ae9b632e3834ed537f7956325ee2a98700000000', @@ -845,14 +854,14 @@ class TestWalletSending(TestCaseForTestnet): wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) # wallet2 -> wallet1 - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, wallet1a.get_receiving_address(), 300000)] + outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 300000)] tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - self.assertEqual(wallet2.txin_type, tx.inputs()[0]['type']) - tx_copy = Transaction(tx.serialize()) + self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet2.is_mine(wallet2.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('010000000001012a4a7e0487c839f211a2f174aa91e94146bdfd408d9271e3d481960b86947e1b00000000171600149fad840ed174584ee054bd26f3e411817338c5edfeffffff02e09304000000000017a914b0b9f31bace76cdfae2c14abc03e223403d7dc4b87d89a0a000000000017a9148ccd0efb2be5b412c4033715f560ed8f446c8ceb87024830450221009c816c3e0c40b37085244f0976f65635b8d711952bad9843c5f51e386fd37cc402202c34a4a7227182742d9f93e9f28c4bd30ded6514550f39614cb5ad00e46690070121038362bbf0b4918b37e9d7c75930ed3a78e3d445724cb5c37ade4a59b6e411fe4e00000000', @@ -871,6 +880,7 @@ class TestWalletSending(TestCaseForTestnet): @needs_test_with_all_ecc_implementations @mock.patch.object(storage.WalletStorage, '_write') def test_rbf(self, mock_write): + self.maxDiff = None for simulate_moving_txs in (False, True): with self.subTest(msg="_bump_fee_p2pkh_when_there_is_a_change_address", simulate_moving_txs=simulate_moving_txs): self._bump_fee_p2pkh_when_there_is_a_change_address(simulate_moving_txs=simulate_moving_txs) @@ -896,20 +906,23 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) tx.set_rbf(True) tx.locktime = 1325501 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007501000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d7200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e57100000000000000000000220203aa6a5d43c6de66d60f50942cf34f20e02c2c6f55349548fbf2cde5dd5d69b9180c8296e571010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -923,17 +936,20 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0) + tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0) tx.locktime = 1325501 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007501000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc392705030000000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400000100fa010000000001011f4db0ecd81f4388db316bc16efb4e9daf874cf4950d54ecb4c0fb372433d68500000000171600143d57fd9e88ef0e70cddb0d8b75ef86698cab0d44fdffffff0280969800000000001976a91472e34cebab371967b038ce41d0e8fa1fb983795e88ac86a0ae020000000017a9149188bc82bdcae077060ebb4f02201b73c806edc887024830450221008e0725d531bd7dee4d8d38a0f921d7b1213e5b16c05312a80464ecc2b649598d0220596d309cf66d5f47cb3df558dbb43c5023a7796a80f5a88b023287e45a4db6b9012102c34d61ceafa8c216f01e05707672354f8119334610f7933a3f80dd7fb6290296bd391400220602a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587a0c8296e5710000000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual('01000000016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc39270503000000006b483045022100a30c21d1ba8cf751b1b78b5a41684cbab6e39687fa188a4295881c7b06f10a6202204ba4f56cbfdeb8ed948d8a18e34112c256c48e921db048f134819b2ca7ed85fd012102a807c07bd7975211078e916bdda061d97e98d59a3631a804aada2f9a3f5b587afdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987a0337200000000001976a914aab9af3fbee0ab4e5c00d53e92f66d4bcb44f1bd88acbd391400', str(tx_copy)) self.assertEqual('40768e1e418f8e851d496448c9627ee29f04c33f67a59ac49d2bbc66288d2077', tx_copy.txid()) @@ -964,7 +980,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertTrue(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual(tx.txid(), tx_copy.txid()) self.assertEqual(tx.wtxid(), tx_copy.wtxid()) @@ -987,20 +1003,23 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1014,17 +1033,20 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 2500000 - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0) + tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0) tx.locktime = 1325500 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede0000000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f9870c4a720000000000160014f0fe5c1867a174a12e70165e728a072619455ed50247304402202a7e412d37f7a54f7ede0f85e58c7f9dc0f7244d222a4f50a90f87b05badeed40220788d4a4a13f660de7d5464dce5e79419361fdd5d1853c7da65469cd32f7981a90121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', str(tx_copy)) self.assertEqual('dad75ab7078b9ce9698a83e7a954c1c38b235d3a4ab79bcb340245e3d9b62b93', tx_copy.txid()) @@ -1044,19 +1066,22 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n', '!')] + outputs = [PartialTxOutput.from_address_and_value('tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n', '!')] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) tx.set_rbf(True) tx.locktime = 1325499 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f882980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1070,16 +1095,19 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, funding_output_value - 5000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=75) + tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=75) tx.locktime = 1325500 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100520200000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff014676980000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual('02000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff014676980000000000160014f0fe5c1867a174a12e70165e728a072619455ed502483045022100ffe74cde6fabd27cecca29e77b863cd901188cb7870e39c237b193e7532d6ef502203efdf069482ce5f89a4a273166ff319f1e122f2e9d56f37696dc15a1b933df420121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', str(tx_copy)) self.assertEqual('0787da6829907ede8a322273d19ba47943ac234ad7fd1cb1821f6a0e78fcc003', tx_copy.txid()) @@ -1098,20 +1126,23 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')] + outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1125,17 +1156,20 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, 0, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0) + tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0) tx.locktime = 1325500 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual('01000000000101c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01267898000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98702473044022069412007c3a6509fdfcfbe90679395c202c973740b0530b8ff366bc86ebff99d02206a02e3c0beb0921fa7d30379db4999d685d4b97239a2b8c7dd839531c72863110121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c5bc391400', str(tx_copy)) self.assertEqual('53824cc67e8fe973b0dfa1b8cc10f4e2441b9b4b2b1eb92576fbba7000c2908a', tx_copy.txid()) @@ -1155,20 +1189,23 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')] + outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100530100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff01f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1190,17 +1227,20 @@ class TestWalletSending(TestCaseForTestnet): self.assertEqual((0, 5_000_000, 0), wallet.get_balance()) # bump tx - tx = wallet.bump_fee(tx=Transaction(tx.serialize()), new_fee_rate=70.0) + tx = wallet.bump_fee(tx=tx_from_any(tx.serialize()), new_fee_rate=70.0) tx.locktime = 1325500 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01009b0100000002c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000feffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987bc3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000001011f404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab90220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f0c3c14aede0000000001000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertFalse(tx.is_complete()) wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual('01000000000102c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000feffffff025c254c0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5f88298000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987024730440220075992f2696076ca14265372c797fa5c6116ef9b8023f36fa7500442fe3e21430220252677cce7b009d8a65681e8f50b78c9a31c6461f67c995b8804041a290893660121028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c502473044022018379b52ea52436eaeef1593e08aba78db1fd624b804ab747722f748203d553702204cbe4c87a010c8b67be9034014b503354e72f9c8205172269c00de20883fac61012102a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469fbc391400', str(tx_copy)) self.assertEqual('056aaf5ec628a492742b083ad7790836e2d12e89061f32d5b517679764fdaff1', tx_copy.txid()) @@ -1221,20 +1261,23 @@ class TestWalletSending(TestCaseForTestnet): wallet.receive_tx_callback(funding_txid1, funding_tx1, TX_HEIGHT_UNCONFIRMED) # create tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)] + outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=5000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100720100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff02a02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f987585d720000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1257,20 +1300,23 @@ class TestWalletSending(TestCaseForTestnet): # create new tx (output should be batched with existing!) # no new input will be needed. just a new output, and change decreased. - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)] + outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=20000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=20000) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100910100000001c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff03a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98720fd4b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede0000000000000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede010000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1285,20 +1331,23 @@ class TestWalletSending(TestCaseForTestnet): # create new tx (output should be batched with existing!) # new input will be needed! - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)] + outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)] coins = wallet.get_spendable_coins(domain=None) - tx = wallet.make_unsigned_transaction(coins, outputs, fixed_fee=100_000) + tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=100_000) tx.set_rbf(True) tx.locktime = 1325499 tx.version = 1 if simulate_moving_txs: - tx = Transaction(str(tx)) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100da0100000002c0ec8b6cdcb6638fa117ead71a8edebc189b30e6e5415bdfb3c8260aa269e6520100000000fdffffff4a5d2593658f7feb9fadcf70dced3bc18db8c90bf77495e608f14dd51c6e6ac30100000000fdffffff04a025260000000000160014268db6c8ba651a25c64f3dd187d0968dbeb0427ba02526000000000017a9145a71fc1a7a98ddd67be935ade1600981c0d066f98760823b0000000000160014f0fe5c1867a174a12e70165e728a072619455ed5808d5b000000000017a914d332f2f63019da6f2d23ee77bbe30eed7739790587bb3914000001011f8096980000000000160014d4ca56fcbad98fb4dcafdc573a75d6a6fffb09b72206028d4c44ca36d2c4bff3813df8d5d3c0278357521ecb892cd694c473c03970e4c50c3c14aede00000000000000000001011f404b4c000000000016001483c3bc7234f17a209cc5dcce14903b54ee4dab90220602a6ff1ffc189b4776b78e20edca969cc45da3e610cc0cc79925604be43fee469f0c3c14aede0000000001000000000000220202105dd9133f33cbd4e50443ef9af428c0be61f097f8942aaa916f50b530125aea0c3c14aede01000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners wallet.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(2, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertTrue(wallet.is_mine(wallet.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1333,7 +1382,7 @@ class TestWalletSending(TestCaseForTestnet): self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual(tx.txid(), tx_copy.txid()) self.assertEqual(tx.wtxid(), tx_copy.wtxid()) @@ -1362,14 +1411,99 @@ class TestWalletSending(TestCaseForTestnet): privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu', ] network = NetworkMock() dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2' - tx = sweep(privkeys, network, config=None, recipient=dest_addr, fee=5000, locktime=1325785, tx_version=1) + tx = sweep(privkeys, network=network, config=None, to_address=dest_addr, fee=5000, locktime=1325785, tx_version=1) - tx_copy = Transaction(tx.serialize()) + tx_copy = tx_from_any(tx.serialize()) self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400', str(tx_copy)) self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.txid()) self.assertEqual('7f827fc5256c274fd1094eb7e020c8ded0baf820356f61aa4f14a9093b0ea0ee', tx_copy.wtxid()) + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_coinjoin_between_two_p2wpkh_electrum_seeds(self, mock_write): + wallet1 = WalletIntegrityHelper.create_standard_wallet( + keystore.from_seed('humor argue expand gain goat shiver remove morning security casual leopard degree', ''), + gap_limit=2, + config=self.config + ) + wallet2 = WalletIntegrityHelper.create_standard_wallet( + keystore.from_seed('couple fade lift useless text thank badge act august roof drastic violin', ''), + gap_limit=2, + config=self.config + ) + + # bootstrap wallet1 + funding_tx = Transaction('0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca010851800') + funding_txid = funding_tx.txid() + self.assertEqual('d8f8186379085cffc9a3fd747e7a7527435db974d1e2941f52f063be8e4fbdd5', funding_txid) + wallet1.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # bootstrap wallet2 + funding_tx = Transaction('02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800') + funding_txid = funding_tx.txid() + self.assertEqual('934f26a72c840293f06c37dc10a358df056dfe245cdf072ae836977c0abc46e5', funding_txid) + wallet2.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # wallet1 creates tx1, with output back to himself + outputs = [PartialTxOutput.from_address_and_value("tb1qhye4wfp26kn0l7ynpn5a4hvt539xc3zf0n76t3", 10_000_000)] + tx1 = wallet1.mktx(outputs=outputs, fee=5000, tx_version=2, rbf=True, sign=False) + tx1.locktime = 1607022 + partial_tx1 = tx1.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a22060205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e04150ca6046cbd00000000000000000022020240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba350ca6046cbd0100000000000000002202024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c80ca6046cbd000000000100000000", + partial_tx1) + tx1.prepare_for_export_for_coinjoin() + partial_tx1 = tx1.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a000000", + partial_tx1) + + # wallet2 creates tx2, with output back to himself + outputs = [PartialTxOutput.from_address_and_value("tb1qufnj5k2rrsnpjq7fg6d2pq3q9um6skdyyehw5m", 10_000_000)] + tx2 = wallet2.mktx(outputs=outputs, fee=5000, tx_version=2, rbf=True, sign=False) + tx2.locktime = 1607023 + partial_tx2 = tx2.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100710200000001e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffff02988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4c8096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c3220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40c467882350000000001000000002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f0c4678823501000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc0c46788235000000000000000000", + partial_tx2) + + # wallet2 gets raw partial tx1, merges it into his own tx2 + tx2.join_with_other_psbt(tx_from_any(partial_tx1)) + partial_tx2 = tx2.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c3220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40c4678823500000000010000000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f0c46788235010000000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc0c46788235000000000000000000", + partial_tx2) + tx2.prepare_for_export_for_coinjoin() + partial_tx2 = tx2.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0000000000", + partial_tx2) + + # wallet2 signs + wallet2.sign_transaction(tx2, password=None) + partial_tx2 = tx2.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c301070001086c02483045022100b87a557003acf04e2b328df6e4fa2bc387ba1a072d9325bd84f162d495720b24022042513f12244318cf94ad7944e8c135c6f47dcfe13ad08a77dca63a76facbe0b8012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f0c46788235010000000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc0c46788235000000000000000000", + partial_tx2) + tx2.prepare_for_export_for_coinjoin() + partial_tx2 = tx2.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100d80200000002e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c301070001086c02483045022100b87a557003acf04e2b328df6e4fa2bc387ba1a072d9325bd84f162d495720b24022042513f12244318cf94ad7944e8c135c6f47dcfe13ad08a77dca63a76facbe0b8012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f40001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0000000000", + partial_tx2) + + # wallet1 gets raw partial tx2, and signs + tx2 = tx_from_any(partial_tx2) + wallet1.sign_transaction(tx2, password=None) + tx = tx_from_any(tx2.serialize_as_bytes().hex()) # simulates moving partial txn between cosigners + + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual("02000000000102e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffffd5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff04988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4cb82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44498096980000000000160014e2672a59431c261903c9469aa082202f37a859a402483045022100b87a557003acf04e2b328df6e4fa2bc387ba1a072d9325bd84f162d495720b24022042513f12244318cf94ad7944e8c135c6f47dcfe13ad08a77dca63a76facbe0b8012102275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f402473044022003010ece3471f7a23f31b2a0fd157f88f7d436c0c73ec408043c7f5dd2b7ccbb02204bd21f5829555c3f94fbd0b5295d1071f739c6b8f2682f8a688e34d0ad26c90101210205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e04156f851800", + str(tx)) + self.assertEqual('4a33546eeaed0e25f9e6a58968be92a804a7e70a5332360dabc79f93cd059752', tx.txid()) + self.assertEqual('868c9f396cc8f998b80e47f199cc85eab971638ecf892b5024cb218bc2025137', tx.wtxid()) + + wallet1.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + wallet2.receive_tx_callback(tx.txid(), tx, TX_HEIGHT_UNCONFIRMED) + + # wallet level checks + self.assertEqual((0, 10995000, 0), wallet1.get_balance()) + self.assertEqual((0, 10495000, 0), wallet2.get_balance()) + class TestWalletOfflineSigning(TestCaseForTestnet): @@ -1398,7 +1532,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1446655 @@ -1407,7 +1541,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007401000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b0200000000fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600000100fd000101000000000101161115f8d8110001aa0883989487f9c7a2faf4451038e4305c7594c5236cbb490100000000fdffffff0338117a0000000000160014c1d7b2ded7017cbde837aab36c1e7b2a3952a57800127a00000000001600143e2ab71fc9738ce16fbe6b3b1c210a68c12db84180969800000000001976a91424b64d981d621c227716b51479faf33019371f4688ac0247304402207a5efc6d970f6a5fdcd1933f68b353b4bf2904743f9f1dc3e9177d8754074baf02202eed707e661493bc450357f12cd7a8b8c610c7cb32ded10516c2933a2ba4346a01210287dce03f594fd889726b13a12970237992a0094a5c9f4eebcca6d50d454b39e9ff121600420604e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f50c343ddcab000000000100000000004202048e2004ca581afcc54a5d9b3b47affdf48b3f89e16d5bd96774fc0f167f2d7873bac6264e3d1f1bb96f64d1530a54e026e0bd7d674151d146fba582e79f4ef5e80c343ddcab010000000000000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1443,7 +1580,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1452,7 +1589,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertFalse(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff010074010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980200000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001976a9149b308d0b3efd4e3469441bc83c3521afde4072b988ac1c391400000100fd4c0d01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400220602ab053d10eda769fab03ab52ee4f1692730288751369643290a8506e31d1e80f00c233d2ae40000000002000000000022020327295144ffff9943356c2d6625f5e2d6411bab77fd56dce571fda6234324e3d90c233d2ae4010000000000000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual(tx.txid(), tx_copy.txid()) @@ -1486,7 +1626,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325341 @@ -1495,7 +1635,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff010072010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980300000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d72000000000017a914191e7373ae7b4829532220e8f281f4581ed52638871d39140000010120809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f870104160014105db4dae7e5b8dd4dda7b7d3b1e588c9bf26f192206030dddd5d3c31738ca2d8b25391f648af6a8b08e6961e8f56d4173d03e9db82d3e0c105d19280000000002000000000001001600144f485261505d5cbd33dce02a723776c99240c28722020211ab9359cc49c95b3b9a87ee95fd4edf0cecce862f9e9f86ff63e10880baaba80c105d1928010000000000000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('3f0d188519237478258ad2bf881643618635d11c2bb95512e830fcf2eda3c522', tx_copy.txid()) @@ -1530,7 +1673,54 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] + tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) + tx.set_rbf(True) + tx.locktime = 1325341 + tx.version = 1 + + self.assertFalse(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual(1, len(tx.inputs())) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners + self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + + self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid()) + self.assertEqual(tx.txid(), tx_copy.txid()) + + # sign tx + tx = wallet_offline.sign_transaction(tx_copy, password=None) + self.assertTrue(tx.is_complete()) + self.assertTrue(tx.is_segwit()) + self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid()) + self.assertEqual('729c2e40a2fccd6b731407c01ed304119c1ac329bdf9baae5b642d916c5f3272', tx.wtxid()) + + @needs_test_with_all_ecc_implementations + @mock.patch.object(storage.WalletStorage, '_write') + def test_offline_signing_beyond_gap_limit(self, mock_write): + wallet_offline = WalletIntegrityHelper.create_standard_wallet( + # bip39: "qwe", der: m/84'/1'/0' + keystore.from_xprv('vprv9K9hbuA23Bidgj1KRSHUZMa59jJLeZBpXPVn4RP7sBLArNhZxJjw4AX7aQmVTErDt4YFC11ptMLjbwxgrsH8GLQ1cx77KggWeVPeDBjr9xM'), + gap_limit=1, # gap limit of offline wallet intentionally set too low + config=self.config + ) + wallet_online = WalletIntegrityHelper.create_standard_wallet( + keystore.from_xpub('vpub5Y941QgusZGvuD5nXTpUvVWohm8q41uftcRNronjRWs9jB2iVr4BbxqbRfAoQjWHgJtDCQEXChgfsPbEuBnidtkFztZSD3zDKTrtwXa2LCa'), + gap_limit=4, + config=self.config + ) + + # bootstrap wallet_online + funding_tx = Transaction('01000000000116e9c9dac2651672316aab3b9553257b6942c5f762c5d795776d9cfa504f183c000000000000fdffffff8085019852fada9da84b58dcf753d292dde314a19f5a5527f6588fa2566142130000000000fdffffffa4154a48db20ce538b28722a89c6b578bd5b5d60d6d7b52323976339e39405230000000000fdffffff0b5ef43f843a96364aebd708e25ea1bdcf2c7df7d0d995560b8b1be5f357b64f0100000000fdffffffd41dfe1199c76fdb3f20e9947ea31136d032d9da48c5e45d85c8f440e2351a510100000000fdffffff5bd015d17e4a1837b01c24ebb4a6b394e3da96a85442bd7dc6abddfbf16f20510000000000fdffffff13a3e7f80b1bd46e38f2abc9e2f335c18a4b0af1778133c7f1c3caae9504345c0200000000fdffffffdf4fc1ab21bca69d18544ddb10a913cd952dbc730ab3d236dd9471445ff405680100000000fdffffffe0424d78a30d5e60ac6b26e2274d7d6e7c6b78fe0b49bdc3ac4dd2147c9535750100000000fdffffff7ab6dd6b3c0d44b0fef0fdc9ab0ad6eee23eef799eee29c005d52bc4461998760000000000fdffffff48a77e5053a21acdf4f235ce00c82c9bc1704700f54d217f6a30704711b9737d0000000000fdffffff86918b39c1d9bb6f34d9b082182f73cedd15504331164dc2b186e95c568ccb870000000000fdffffff15a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff9a2875297f81dfd3b77426d63f621db350c270cc28c634ad86b9969ee33ac6960000000000fdffffffd6eeb1d1833e00967083d1ab86fa5a2e44355bd613d9277135240fe6f60148a20100000000fdffffffd8a6e5a9b68a65ff88220ca33e36faf6f826ae8c5c8a13fe818a5e63828b68a40100000000fdffffff73aab8471f82092e45ed1b1afeffdb49ea1ec74ce4853f971812f6a72a7e85aa0000000000fdffffffacd6459dec7c3c51048eb112630da756f5d4cb4752b8d39aa325407ae0885cba0000000000fdffffff1eddd5e13bef1aba1ff151762b5860837daa9b39db1eae8ea8227c81a5a1c8ba0000000000fdffffff67a096ff7c343d39e96929798097f6d7a61156bbdb905fbe534ba36f273271d40100000000fdffffff109a671eb7daf6dcd07c0ceff99f2de65864ab36d64fb3a890bab951569adeee0100000000fdffffff4f1bdc64da8056d08f79db7f5348d1de55946e57aa7c8279499c703889b6e0fd0200000000fdffffff042f280000000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef80969800000000001976a91422a6daa4a7b695c8a2dd104d47c5dc73d655c96f88ac809698000000000017a914a6885437e0762013facbda93894202a0fe86e35f8702473044022075ef5f04d7a63347064938e15a0c74277a79e5c9d32a26e39e8a517a44d565cc022015246790fb5b29c9bf3eded1b95699b1635bcfc6d521886fddf1135ba1b988ec012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe02473044022061aa9b0d9649ffd7259bc54b35f678565dbbe11507d348dd8885522eaf1fa70c02202cc79de09e8e63e8d57fde6ef66c079ddac4d9828e1936a9db833d4c142615c3012103a8f58fc1f5625f18293403104874f2d38c9279f777e512570e4199c7d292b81b0247304402207744dc1ab0bf77c081b58540c4321d090c0a24a32742a361aa55ad86f0c7c24e02201a9b0dd78b63b495ab5a0b5b161c54cb085d70683c90e188bb4dc2e41e142f6601210361fb354f8259abfcbfbdda36b7cb4c3b05a3ca3d68dd391fd8376e920d93870d0247304402204803e423c321acc6c12cb0ebf196d2906842fdfed6de977cc78277052ee5f15002200634670c1dc25e6b1787a65d3e09c8e6bb0340238d90b9d98887e8fd53944e080121031104c60d027123bf8676bcaefaa66c001a0d3d379dc4a9492a567a9e1004452d02473044022050e4b5348d30011a22b6ae8b43921d29249d88ea71b1fbaa2d9c22dfdef58b7002201c5d5e143aa8835454f61b0742226ebf8cd466bcc2cdcb1f77b92e473d3b13190121030496b9d49aa8efece4f619876c60a77d2c0dc846390ecdc5d9acbfa1bb3128760247304402204d6a9b986e1a0e3473e8aef84b3eb7052442a76dfd7631e35377f141496a55490220131ab342853c01e31f111436f8461e28bc95883b871ca0e01b5f57146e79d7bb012103262ffbc88e25296056a3c65c880e3686297e07f360e6b80f1219d65b0900e84e02483045022100c8ffacf92efa1dddef7e858a241af7a80adcc2489bcc325195970733b1f35fac022076f40c26023a228041a9665c5290b9918d06f03b716e4d8f6d47e79121c7eb37012102d9ba7e02d7cd7dd24302f823b3114c99da21549c663f72440dc87e8ba412120902483045022100b55545d84e43d001bbc10a981f184e7d3b98a7ed6689863716cab053b3655a2f0220537eb76a695fbe86bf020b4b6f7ae93b506d778bbd0885f0a61067616a2c8bce0121034a57f2fa2c32c9246691f6a922fb1ebdf1468792bae7eff253a99fc9f2a5023902483045022100f1d4408463dbfe257f9f778d5e9c8cdb97c8b1d395dbd2e180bc08cad306492c022002a024e19e1a406eaa24467f033659de09ab58822987281e28bb6359288337bd012103e91daa18d924eea62011ce596e15b6d683975cf724ea5bf69a8e2022c26fc12f0247304402204f1e12b923872f396e5e1a3aa94b0b2e86b4ce448f4349a017631db26d7dff8a022069899a05de2ad2bbd8e0202c56ab1025a7db9a4998eea70744e3c367d2a7eb71012103b0eee86792dbef1d4a49bc4ea32d197c8c15d27e6e0c5c33e58e409e26d4a39a0247304402201787dacdb92e0df6ad90226649f0e8321287d0bd8fddc536a297dd19b5fc103e022001fe89300a76e5b46d0e3f7e39e0ee26cc83b71d59a2a5da1dd7b13350cd0c07012103afb1e43d7ec6b7999ef0f1093069e68fe1dfe5d73fc6cfb4f7a5022f7098758c02483045022100acc1212bba0fe4fcc6c3ae5cf8e25f221f140c8444d3c08dfc53a93630ac25da02203f12982847244bd9421ef340293f3a38d2ab5d028af60769e46fcc7d81312e7e012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024830450221009c04934102402949484b21899271c3991c007b783b8efc85a3c3d24641ac7c24022006fb1895ce969d08a2cb29413e1a85427c7e85426f7a185108ca44b5a0328cb301210360248db4c7d7f76fe231998d2967104fee04df8d8da34f10101cc5523e82648c02483045022100b11fe61b393fa5dbe18ab98f65c249345b429b13f69ee2d1b1335725b24a0e73022010960cdc5565cbc81885c8ed95142435d3c202dfa5a3dc5f50f3914c106335ce0121029c878610c34c21381cda12f6f36ab88bf60f5f496c1b82c357b8ac448713e7b50247304402200ca080db069c15bbf98e1d4dff68d0aea51227ff5d17a8cf67ceae464c22bbb0022051e7331c0918cbb71bb2cef29ca62411454508a16180b0fb5df94248890840df0121028f0be0cde43ff047edbda42c91c37152449d69789eb812bb2e148e4f22472c0f0247304402201fefe258938a2c481d5a745ef3aa8d9f8124bbe7f1f8c693e2ddce4ddc9a927c02204049e0060889ede8fda975edf896c03782d71ba53feb51b04f5ae5897d7431dc012103946730b480f52a43218a9edce240e8b234790e21df5e96482703d81c3c19d3f1024730440220126a6a56dbe69af78d156626fc9cf41d6aac0c07b8b5f0f8491f68db5e89cb5002207ee6ed6f2f41da256f3c1e79679a3de6cf34cc08b940b82be14aefe7da031a6b012102801bc7170efb82c490e243204d86970f15966aa3bce6a06bef5c09a83a5bfffe024730440220363204a1586d7f13c148295122cbf9ec7939685e3cadab81d6d9e921436d21b7022044626b8c2bd4aa7c167d74bc4e9eb9d0744e29ce0ad906d78e10d6d854f23d170121037fb9c51716739bb4c146857fab5a783372f72a65987d61f3b58c74360f4328dd0247304402207925a4c2a3a6b76e10558717ee28fcb8c6fde161b9dc6382239af9f372ace99902204a58e31ce0b4a4804a42d2224331289311ded2748062c92c8aca769e81417a4c012102e18a8c235b48e41ef98265a8e07fa005d2602b96d585a61ad67168d74e7391cb02483045022100bbfe060479174a8d846b5a897526003eb2220ba307a5fee6e1e8de3e4e8b38fd02206723857301d447f67ac98a5a5c2b80ef6820e98fae213db1720f93d91161803b01210386728e2ac3ecee15f58d0505ee26f86a68f08c702941ffaf2fb7213e5026aea10247304402203a2613ae68f697eb02b5b7d18e3c4236966dac2b3a760e3021197d76e9ad4239022046f9067d3df650fcabbdfd250308c64f90757dec86f0b08813c979a42d06a6ec012102a1d7ee1cb4dc502f899aaafae0a2eb6cbf80d9a1073ae60ddcaabc3b1d1f15df02483045022100ab1bea2cc5388428fd126c7801550208701e21564bd4bd00cfd4407cfafc1acd0220508ee587f080f3c80a5c0b2175b58edd84b755e659e2135b3152044d75ebc4b501210236dd1b7f27a296447d0eb3750e1bdb2d53af50b31a72a45511dc1ec3fe7a684a19391400') + funding_txid = funding_tx.txid() + self.assertEqual('98574bc5f6e75769eb0c93d41453cc1dfbd15c14e63cc3c42f37cdbd08858762', funding_txid) + wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) + + # create unsigned tx + outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325341 @@ -1539,7 +1729,10 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertTrue(tx.is_segwit()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff010071010000000162878508bdcd372fc4c33ce6145cd1fb1dcc5314d4930ceb6957e7f6c54b57980100000000fdffffff02a0252600000000001600140bf6c540d0218c99511f7c62a49784c88b1870b8585d7200000000001600145543fe1a1364b806b27a5c9dc92ac9bbf0d42aa31d3914000001011f80969800000000001600146c540c1c9f546004539f45318b8d9f4d7b4857ef220603fd88f32a81e812af0187677fc0e7ac9b7fb63ca68c2d98c2afbcf99aa311ac060cdf758ae500000000020000000000220202ac05f54ef082ac98302d57d532e728653565bd55f46fcf03cacbddb168fd6c760cdf758ae5010000000000000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx_copy.txid()) @@ -1567,7 +1760,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1575,9 +1768,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100740100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0100000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d7200000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac1c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual(None, tx_copy.txid()) # not segwit self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx @@ -1602,7 +1799,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1610,9 +1807,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100720100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0200000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d72000000000017a914b808938a8007bc54509cd946944c479c0fa6554f871c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual(None, tx_copy.txid()) # redeem script not available self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx @@ -1637,7 +1838,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1645,9 +1846,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx_copy.txid()) self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx @@ -1676,7 +1881,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1684,9 +1889,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100740100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0100000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d7200000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac1c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual(None, tx_copy.txid()) # not segwit self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx @@ -1715,7 +1924,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1723,9 +1932,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100720100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0200000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d72000000000017a914b808938a8007bc54509cd946944c479c0fa6554f871c391400000100fd200101000000000101197a89cff51096b9dd4214cdee0eb90cb27a25477e739521d728a679724042730100000000fdffffff048096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a80969800000000001976a91405a20074ef7eb42c7c6fcd4f499faa699742783288ac809698000000000017a914b808938a8007bc54509cd946944c479c0fa6554f87131b2c0400000000160014a04dfdb9a9aeac3b3fada6f43c2a66886186e2440247304402204f5dbb9dda65eab26179f1ca7c37c8baf028153815085dd1bbb2b826296e3b870220379fcd825742d6e2bdff772f347b629047824f289a5499a501033f6c3495594901210363c9c98740fe0455c646215cea9b13807b758791c8af7b74e62968bef57ff8ae1e391400000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual(None, tx_copy.txid()) # redeem script not available self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx @@ -1754,7 +1967,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, 'tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325340 @@ -1762,9 +1975,13 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff0100710100000001626bbbb7a4ad82dbf7f6bd64ac3f40d0e2695b606d7953f2802b9ea426ea080a0000000000fdffffff02a025260000000000160014e5bddbfee3883729b48fe3385216e64e6035f6eb585d720000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a1c3914000001011f8096980000000000160014dab37af8fefbbb31887a0a5f9b2698f4a7b45f6a000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual('f8039bd85279f2b5698f15d47f2e338d067d09af391bd8a19467aa94d03f280c', tx_copy.txid()) self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx @@ -1806,7 +2023,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325503 @@ -1814,20 +2031,27 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual(None, tx_copy.txid()) # not segwit self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx - first tx = wallet_offline1.sign_transaction(tx_copy, password=None) self.assertFalse(tx.is_complete()) - tx = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f28483045022100cfe41e783629a2ad0b1f17cd2dbd69db05763fa7a22691131fa321ba3140d7cb02203fbda2ccc6212315464cd814d4e909b4f80a2361e3af0f9deda06478f91a0f3901010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners # sign tx - second tx = wallet_offline2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) - tx = Transaction(tx.serialize()) + tx = tx_from_any(tx.serialize()) self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fdfe0000483045022100cfe41e783629a2ad0b1f17cd2dbd69db05763fa7a22691131fa321ba3140d7cb02203fbda2ccc6212315464cd814d4e909b4f80a2361e3af0f9deda06478f91a0f3901483045022100b84fd63e957f2409558f63962fc91ba58334efde8b88ff53ca71da3d0fe7219702206001c6caeb30e18a7525fc72de0003e12646bf815b12fb132c1aadd6ffa1989c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400', str(tx)) @@ -1866,7 +2090,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325504 @@ -1874,21 +2098,31 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007301000000013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d35540100000000fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987c0391400000100f70100000000010118d494d28e5c3bf61566ca0313e22c3b561b888a317d689cc8b47b947adebd440000000017160014aec84704ea8508ddb94a3c6e53f0992d33a2a529fdffffff020f0925000000000017a91409f7aae0265787a02de22839d41e9c927768230287809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf989870247304402206b906369f4075ebcfc149f7429dcfc34e11e1b7bbfc85d1185d5e9c324be0d3702203ce7fc12fd3131920fbcbb733250f05dbf7d03e18a4656232ee69d5c54dd46bd0121028a4b697a37f3f57f6e53f90db077fa9696095b277454fda839c211d640d48649c0391400000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual(None, tx_copy.txid()) # redeem script not available self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx - first tx = wallet_offline1.sign_transaction(tx_copy, password=None) self.assertFalse(tx.is_complete()) self.assertEqual('6a58a51591142429203b62b6ddf6b799a6926882efac229998c51bee6c3573eb', tx.txid()) - tx = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + # note re PSBT: online wallet had put a NON-WITNESS UTXO for input0, as they did not know if it was segwit. + # offline wallet now replaced this with a WITNESS-UTXO. + # this switch is needed to interop with bitcoin core... https://github.com/bitcoin/bitcoin/blob/fba574c908bb61eff1a0e83c935f3526ba9035f2/src/psbt.cpp#L163 + self.assertEqual("70736274ff01007301000000013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d35540100000000fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987c039140000010120809698000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987220202d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c4730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0101042200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704010547522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52ae220602975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777460c17cea9140000000001000000220602d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c0cd1dbcc210000000001000000000001002200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704010147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52ae220202975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777460c17cea9140000000001000000220202d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c0cd1dbcc21000000000100000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners # sign tx - second tx = wallet_offline2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) - tx = Transaction(tx.serialize()) + tx = tx_from_any(tx.serialize()) self.assertEqual('010000000001013eee274625ae78394847614a8bf513558bb6bd514dfd16855cb856e1e96d355401000000232200206ee8d4bb1277b7dbe1d4e49b880993aa993f417a9101cb23865c7c7258732704fdffffff02a02526000000000017a914a4189ef02c95cfe36f8e880c6cb54dff0837b22687585d72000000000017a91400698bd11c38f887f17c99846d9be96321fbf98987040047304402205a9dd9eb5676196893fb08f60079a2e9f567ee39614075d8c5d9fab0f11cbbc7022039640855188ebb7bccd9e3f00b397a888766d42d00d006f1ca7457c15449285f014730440220234f6648c5741eb195f0f4cd645298a10ce02f6ef557d05df93331e21c4f58cb022058ce2af0de1c238c4a8dd3b3c7a9a0da6e381ddad7593cddfc0480f9fe5baadf0147522102975c00f6af579f9a1d283f1e5a43032deadbab2308aef30fb307c0cfe54777462102d3f47041b424a84898e315cc8ef58190f6aec79c178c12de0790890ba7166e9c52aec0391400', str(tx)) @@ -1928,7 +2162,7 @@ class TestWalletOfflineSigning(TestCaseForTestnet): wallet_online.receive_tx_callback(funding_txid, funding_tx, TX_HEIGHT_UNCONFIRMED) # create unsigned tx - outputs = [TxOutput(bitcoin.TYPE_ADDRESS, '2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] + outputs = [PartialTxOutput.from_address_and_value('2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)] tx = wallet_online.mktx(outputs=outputs, password=None, fee=5000) tx.set_rbf(True) tx.locktime = 1325505 @@ -1936,21 +2170,28 @@ class TestWalletOfflineSigning(TestCaseForTestnet): self.assertFalse(tx.is_complete()) self.assertEqual(1, len(tx.inputs())) - tx_copy = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007e0100000001a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63cc13914000001012b80969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c000000", + partial_tx) + tx_copy = tx_from_any(partial_tx) # simulates moving partial txn between cosigners self.assertTrue(wallet_online.is_mine(wallet_online.get_txin_address(tx_copy.inputs()[0]))) + self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx_copy.txid()) self.assertEqual(tx.txid(), tx_copy.txid()) # sign tx - first tx = wallet_offline1.sign_transaction(tx_copy, password=None) self.assertFalse(tx.is_complete()) self.assertEqual('32e946761b4e718c1fa8d044db9e72d5831f6395eb284faf2fb5c4af0743e501', tx.txid()) - tx = Transaction(tx.serialize()) + partial_tx = tx.serialize_as_bytes().hex() + self.assertEqual("70736274ff01007e0100000001a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63cc13914000001012b80969800000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa4730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa0cb5c37672000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e0c043b02bf0000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9410c3b1da3ef0000000000000000000001016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa0cb5c37672000000000000000022020273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e0c043b02bf0000000000000000220202aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9410c3b1da3ef000000000000000000", + partial_tx) + tx = tx_from_any(partial_tx) # simulates moving partial txn between cosigners # sign tx - second tx = wallet_offline2.sign_transaction(tx, password=None) self.assertTrue(tx.is_complete()) - tx = Transaction(tx.serialize()) + tx = tx_from_any(tx.serialize()) self.assertEqual('01000000000101a36fa6d72cb8aadf795097ff18609e278db156ce14f39ddd27023d08b97a3a640000000000fdffffff02a02526000000000017a91447ee5a659f6ffb53f7e3afc1681b6415f3c00fa187585d7200000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c04004730440220629d89626585f563202e6b38ceddc26ccd00737e0b7ee4239b9266ef9174ea2f02200b74828399a2e35ed46c9b484af4817438d5fea890606ebb201b821944db1fdc0147304402205d1a59c84c419992069e9764a7992abca6a812cc5dfd4f0d6515d4283e660ce802202597a38899f31545aaf305629bd488f36bf54e4a05fe983932cafbb3906efb8f016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153aec1391400', str(tx)) diff --git a/electrum/transaction.py b/electrum/transaction.py @@ -30,19 +30,23 @@ import struct import traceback import sys +import io +import base64 from typing import (Sequence, Union, NamedTuple, Tuple, Optional, Iterable, Callable, List, Dict, Set, TYPE_CHECKING) from collections import defaultdict +from enum import IntEnum +import itertools -from . import ecc, bitcoin, constants, segwit_addr -from .util import profiler, to_bytes, bh2u, bfh -from .bitcoin import (TYPE_ADDRESS, TYPE_PUBKEY, TYPE_SCRIPT, hash_160, +from . import ecc, bitcoin, constants, segwit_addr, bip32 +from .bip32 import BIP32Node +from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str +from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, - hash_encode, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, - push_script, int_to_hex, push_script, b58_address_to_hash160, + var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, + int_to_hex, push_script, b58_address_to_hash160, opcodes, add_number_to_script, base_decode, is_segwit_script_type) from .crypto import sha256d -from .keystore import xpubkey_to_address, xpubkey_to_pubkey from .logging import get_logger if TYPE_CHECKING: @@ -50,10 +54,7 @@ if TYPE_CHECKING: _logger = get_logger(__name__) - - -NO_SIGNATURE = 'ff' -PARTIAL_TXN_HEADER_MAGIC = b'EPTF\xff' +DEBUG_PSBT_PARSING = False class SerializationError(Exception): @@ -64,7 +65,15 @@ class UnknownTxinType(Exception): pass -class NotRecognizedRedeemScript(Exception): +class BadHeaderMagic(SerializationError): + pass + + +class UnexpectedEndOfStream(SerializationError): + pass + + +class PSBTInputConsistencyFailure(SerializationError): pass @@ -72,23 +81,83 @@ class MalformedBitcoinScript(Exception): pass -class TxOutput(NamedTuple): - type: int - address: str - value: Union[int, str] # str when the output is set to max: '!' +class MissingTxInputAmount(Exception): + pass + + +SIGHASH_ALL = 1 + + +class TxOutput: + scriptpubkey: bytes + value: Union[int, str] + + def __init__(self, *, scriptpubkey: bytes, value: Union[int, str]): + self.scriptpubkey = scriptpubkey + self.value = value # str when the output is set to max: '!' # in satoshis + @classmethod + def from_address_and_value(cls, address: str, value: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']: + return cls(scriptpubkey=bfh(bitcoin.address_to_script(address)), + value=value) + + def serialize_to_network(self) -> bytes: + buf = int.to_bytes(self.value, 8, byteorder="little", signed=False) + script = self.scriptpubkey + buf += bfh(var_int(len(script.hex()) // 2)) + buf += script + return buf + + @classmethod + def from_network_bytes(cls, raw: bytes) -> 'TxOutput': + vds = BCDataStream() + vds.write(raw) + txout = parse_output(vds) + if vds.can_read_more(): + raise SerializationError('extra junk at the end of TxOutput bytes') + return txout + + def to_legacy_tuple(self) -> Tuple[int, str, Union[int, str]]: + if self.address: + return TYPE_ADDRESS, self.address, self.value + return TYPE_SCRIPT, self.scriptpubkey.hex(), self.value + + @classmethod + def from_legacy_tuple(cls, _type: int, addr: str, val: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']: + if _type == TYPE_ADDRESS: + return cls.from_address_and_value(addr, val) + if _type == TYPE_SCRIPT: + return cls(scriptpubkey=bfh(addr), value=val) + raise Exception(f"unexptected legacy address type: {_type}") + + @property + def address(self) -> Optional[str]: + return get_address_from_output_script(self.scriptpubkey) # TODO cache this? + + def get_ui_address_str(self) -> str: + addr = self.address + if addr is not None: + return addr + return f"SCRIPT {self.scriptpubkey.hex()}" -class TxOutputForUI(NamedTuple): - address: str - value: int + def __repr__(self): + return f"<TxOutput script={self.scriptpubkey.hex()} address={self.address} value={self.value}>" + def __eq__(self, other): + if not isinstance(other, TxOutput): + return False + return self.scriptpubkey == other.scriptpubkey and self.value == other.value + + def __ne__(self, other): + return not (self == other) -class TxOutputHwInfo(NamedTuple): - address_index: Tuple - sorted_xpubs: Sequence[str] - num_sig: Optional[int] - script_type: str - is_change: bool # whether the wallet considers the output to be change + def to_json(self): + d = { + 'scriptpubkey': self.scriptpubkey.hex(), + 'address': self.address, + 'value_sats': self.value, + } + return d class BIP143SharedTxDigestFields(NamedTuple): @@ -97,11 +166,67 @@ class BIP143SharedTxDigestFields(NamedTuple): hashOutputs: str +class TxOutpoint(NamedTuple): + txid: bytes # endianness same as hex string displayed; reverse of tx serialization order + out_idx: int + + @classmethod + def from_str(cls, s: str) -> 'TxOutpoint': + hash_str, idx_str = s.split(':') + return TxOutpoint(txid=bfh(hash_str), + out_idx=int(idx_str)) + + def to_str(self) -> str: + return f"{self.txid.hex()}:{self.out_idx}" + + def serialize_to_network(self) -> bytes: + return self.txid[::-1] + bfh(int_to_hex(self.out_idx, 4)) + + def is_coinbase(self) -> bool: + return self.txid == bytes(32) + + +class TxInput: + prevout: TxOutpoint + script_sig: Optional[bytes] + nsequence: int + witness: Optional[bytes] + + def __init__(self, *, + prevout: TxOutpoint, + script_sig: bytes = None, + nsequence: int = 0xffffffff - 1, + witness: bytes = None): + self.prevout = prevout + self.script_sig = script_sig + self.nsequence = nsequence + self.witness = witness + + def is_coinbase(self) -> bool: + return self.prevout.is_coinbase() + + def value_sats(self) -> Optional[int]: + return None + + def to_json(self): + d = { + 'prevout_hash': self.prevout.txid.hex(), + 'prevout_n': self.prevout.out_idx, + 'coinbase': self.is_coinbase(), + 'nsequence': self.nsequence, + } + if self.script_sig is not None: + d['scriptSig'] = self.script_sig.hex() + if self.witness is not None: + d['witness'] = self.witness.hex() + return d + + class BCDataStream(object): """Workalike python implementation of Bitcoin's CDataStream class.""" def __init__(self): - self.input = None + self.input = None # type: Optional[bytearray] self.read_cursor = 0 def clear(self): @@ -135,11 +260,11 @@ class BCDataStream(object): self.write_compact_size(len(string)) self.write(string) - def read_bytes(self, length): + def read_bytes(self, length) -> bytes: try: - result = self.input[self.read_cursor:self.read_cursor+length] + result = self.input[self.read_cursor:self.read_cursor+length] # type: bytearray self.read_cursor += length - return result + return bytes(result) except IndexError: raise SerializationError("attempt to read past end of buffer") from None @@ -192,6 +317,8 @@ class BCDataStream(object): elif size < 2**64: self.write(b'\xff') self._write_num('<Q', size) + else: + raise Exception(f"size {size} too large for compact_size") def _read_num(self, format): try: @@ -270,195 +397,44 @@ def match_decoded(decoded, to_match): return True -def parse_sig(x_sig): - return [None if x == NO_SIGNATURE else x for x in x_sig] - -def safe_parse_pubkey(x): - try: - return xpubkey_to_pubkey(x) - except: - return x - -def parse_scriptSig(d, _bytes): - try: - decoded = [ x for x in script_GetOp(_bytes) ] - except Exception as e: - # coinbase transactions raise an exception - _logger.info(f"parse_scriptSig: cannot find address in input script (coinbase?) {bh2u(_bytes)}") - return - - match = [OPPushDataGeneric] - if match_decoded(decoded, match): - item = decoded[0][1] - if item[0] == 0: - # segwit embedded into p2sh - # witness version 0 - d['address'] = bitcoin.hash160_to_p2sh(hash_160(item)) - if len(item) == 22: - d['type'] = 'p2wpkh-p2sh' - elif len(item) == 34: - d['type'] = 'p2wsh-p2sh' - else: - _logger.info(f"unrecognized txin type {bh2u(item)}") - elif opcodes.OP_1 <= item[0] <= opcodes.OP_16: - # segwit embedded into p2sh - # witness version 1-16 - pass - else: - # assert item[0] == 0x30 - # pay-to-pubkey - d['type'] = 'p2pk' - d['address'] = "(pubkey)" - d['signatures'] = [bh2u(item)] - d['num_sig'] = 1 - d['x_pubkeys'] = ["(pubkey)"] - d['pubkeys'] = ["(pubkey)"] - return - - # p2pkh TxIn transactions push a signature - # (71-73 bytes) and then their public key - # (33 or 65 bytes) onto the stack: - match = [OPPushDataGeneric, OPPushDataGeneric] - if match_decoded(decoded, match): - sig = bh2u(decoded[0][1]) - x_pubkey = bh2u(decoded[1][1]) - try: - signatures = parse_sig([sig]) - pubkey, address = xpubkey_to_address(x_pubkey) - except: - _logger.info(f"parse_scriptSig: cannot find address in input script (p2pkh?) {bh2u(_bytes)}") - return - d['type'] = 'p2pkh' - d['signatures'] = signatures - d['x_pubkeys'] = [x_pubkey] - d['num_sig'] = 1 - d['pubkeys'] = [pubkey] - d['address'] = address - return - - # p2sh transaction, m of n - match = [opcodes.OP_0] + [OPPushDataGeneric] * (len(decoded) - 1) - if match_decoded(decoded, match): - x_sig = [bh2u(x[1]) for x in decoded[1:-1]] - redeem_script_unsanitized = decoded[-1][1] # for partial multisig txn, this has x_pubkeys - try: - m, n, x_pubkeys, pubkeys, redeem_script = parse_redeemScript_multisig(redeem_script_unsanitized) - except NotRecognizedRedeemScript: - _logger.info(f"parse_scriptSig: cannot find address in input script (p2sh?) {bh2u(_bytes)}") - - # we could still guess: - # d['address'] = hash160_to_p2sh(hash_160(decoded[-1][1])) - return - # write result in d - d['type'] = 'p2sh' - d['num_sig'] = m - d['signatures'] = parse_sig(x_sig) - d['x_pubkeys'] = x_pubkeys - d['pubkeys'] = pubkeys - d['redeem_script'] = redeem_script - d['address'] = hash160_to_p2sh(hash_160(bfh(redeem_script))) - return - - # custom partial format for imported addresses - match = [opcodes.OP_INVALIDOPCODE, opcodes.OP_0, OPPushDataGeneric] - if match_decoded(decoded, match): - x_pubkey = bh2u(decoded[2][1]) - pubkey, address = xpubkey_to_address(x_pubkey) - d['type'] = 'address' - d['address'] = address - d['num_sig'] = 1 - d['x_pubkeys'] = [x_pubkey] - d['pubkeys'] = None # get_sorted_pubkeys will populate this - d['signatures'] = [None] - return - - _logger.info(f"parse_scriptSig: cannot find address in input script (unknown) {bh2u(_bytes)}") - - -def parse_redeemScript_multisig(redeem_script: bytes): - try: - dec2 = [ x for x in script_GetOp(redeem_script) ] - except MalformedBitcoinScript: - raise NotRecognizedRedeemScript() - try: - m = dec2[0][0] - opcodes.OP_1 + 1 - n = dec2[-2][0] - opcodes.OP_1 + 1 - except IndexError: - raise NotRecognizedRedeemScript() - op_m = opcodes.OP_1 + m - 1 - op_n = opcodes.OP_1 + n - 1 - match_multisig = [op_m] + [OPPushDataGeneric] * n + [op_n, opcodes.OP_CHECKMULTISIG] - if not match_decoded(dec2, match_multisig): - raise NotRecognizedRedeemScript() - x_pubkeys = [bh2u(x[1]) for x in dec2[1:-2]] - pubkeys = [safe_parse_pubkey(x) for x in x_pubkeys] - redeem_script2 = bfh(multisig_script(x_pubkeys, m)) - if redeem_script2 != redeem_script: - raise NotRecognizedRedeemScript() - redeem_script_sanitized = multisig_script(pubkeys, m) - return m, n, x_pubkeys, pubkeys, redeem_script_sanitized - - -def get_address_from_output_script(_bytes: bytes, *, net=None) -> Tuple[int, str]: +def get_address_from_output_script(_bytes: bytes, *, net=None) -> Optional[str]: try: decoded = [x for x in script_GetOp(_bytes)] except MalformedBitcoinScript: decoded = None - # p2pk - match = [OPPushDataPubkey, opcodes.OP_CHECKSIG] - if match_decoded(decoded, match) and ecc.ECPubkey.is_pubkey_bytes(decoded[0][1]): - return TYPE_PUBKEY, bh2u(decoded[0][1]) - # p2pkh match = [opcodes.OP_DUP, opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG] if match_decoded(decoded, match): - return TYPE_ADDRESS, hash160_to_p2pkh(decoded[2][1], net=net) + return hash160_to_p2pkh(decoded[2][1], net=net) # p2sh match = [opcodes.OP_HASH160, OPPushDataGeneric(lambda x: x == 20), opcodes.OP_EQUAL] if match_decoded(decoded, match): - return TYPE_ADDRESS, hash160_to_p2sh(decoded[1][1], net=net) + return hash160_to_p2sh(decoded[1][1], net=net) # segwit address (version 0) match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))] if match_decoded(decoded, match): - return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=0, net=net) + return hash_to_segwit_addr(decoded[1][1], witver=0, net=net) # segwit address (version 1-16) future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1)) for witver, opcode in enumerate(future_witness_versions, start=1): match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)] if match_decoded(decoded, match): - return TYPE_ADDRESS, hash_to_segwit_addr(decoded[1][1], witver=witver, net=net) + return hash_to_segwit_addr(decoded[1][1], witver=witver, net=net) - return TYPE_SCRIPT, bh2u(_bytes) + return None -def parse_input(vds, full_parse: bool): - d = {} - prevout_hash = hash_encode(vds.read_bytes(32)) +def parse_input(vds) -> TxInput: + prevout_hash = vds.read_bytes(32)[::-1] prevout_n = vds.read_uint32() - scriptSig = vds.read_bytes(vds.read_compact_size()) - sequence = vds.read_uint32() - d['prevout_hash'] = prevout_hash - d['prevout_n'] = prevout_n - d['scriptSig'] = bh2u(scriptSig) - d['sequence'] = sequence - d['type'] = 'unknown' if prevout_hash != '00'*32 else 'coinbase' - d['address'] = None - d['num_sig'] = 0 - if not full_parse: - return d - d['x_pubkeys'] = [] - d['pubkeys'] = [] - d['signatures'] = {} - if d['type'] != 'coinbase' and scriptSig: - try: - parse_scriptSig(d, scriptSig) - except BaseException: - _logger.exception(f'failed to parse scriptSig {bh2u(scriptSig)}') - return d + prevout = TxOutpoint(txid=prevout_hash, out_idx=prevout_n) + script_sig = vds.read_bytes(vds.read_compact_size()) + nsequence = vds.read_uint32() + return TxInput(prevout=prevout, script_sig=script_sig, nsequence=nsequence) def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: @@ -467,114 +443,26 @@ def construct_witness(items: Sequence[Union[str, int, bytes]]) -> str: for item in items: if type(item) is int: item = bitcoin.script_num_to_hex(item) - elif type(item) is bytes: + elif isinstance(item, (bytes, bytearray)): item = bh2u(item) witness += bitcoin.witness_push(item) return witness -def parse_witness(vds, txin, full_parse: bool): +def parse_witness(vds: BCDataStream, txin: TxInput) -> None: n = vds.read_compact_size() - if n == 0: - txin['witness'] = '00' - return - if n == 0xffffffff: - txin['value'] = vds.read_uint64() - txin['witness_version'] = vds.read_uint16() - n = vds.read_compact_size() - # now 'n' is the number of items in the witness - w = list(bh2u(vds.read_bytes(vds.read_compact_size())) for i in range(n)) - txin['witness'] = construct_witness(w) - if not full_parse: - return + witness_elements = list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) + txin.witness = bfh(construct_witness(witness_elements)) - try: - if txin.get('witness_version', 0) != 0: - raise UnknownTxinType() - if txin['type'] == 'coinbase': - pass - elif txin['type'] == 'address': - pass - elif txin['type'] == 'p2wsh-p2sh' or n > 2: - witness_script_unsanitized = w[-1] # for partial multisig txn, this has x_pubkeys - try: - m, n, x_pubkeys, pubkeys, witness_script = parse_redeemScript_multisig(bfh(witness_script_unsanitized)) - except NotRecognizedRedeemScript: - raise UnknownTxinType() - txin['signatures'] = parse_sig(w[1:-1]) - txin['num_sig'] = m - txin['x_pubkeys'] = x_pubkeys - txin['pubkeys'] = pubkeys - txin['witness_script'] = witness_script - if not txin.get('scriptSig'): # native segwit script - txin['type'] = 'p2wsh' - txin['address'] = bitcoin.script_to_p2wsh(witness_script) - elif txin['type'] == 'p2wpkh-p2sh' or n == 2: - txin['num_sig'] = 1 - txin['x_pubkeys'] = [w[1]] - txin['pubkeys'] = [safe_parse_pubkey(w[1])] - txin['signatures'] = parse_sig([w[0]]) - if not txin.get('scriptSig'): # native segwit script - txin['type'] = 'p2wpkh' - txin['address'] = bitcoin.public_key_to_p2wpkh(bfh(txin['pubkeys'][0])) - else: - raise UnknownTxinType() - except UnknownTxinType: - txin['type'] = 'unknown' - except BaseException: - txin['type'] = 'unknown' - _logger.exception(f"failed to parse witness {txin.get('witness')}") - - -def parse_output(vds, i): - d = {} - d['value'] = vds.read_int64() - if d['value'] > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: + +def parse_output(vds: BCDataStream) -> TxOutput: + value = vds.read_int64() + if value > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN: raise SerializationError('invalid output amount (too large)') - if d['value'] < 0: + if value < 0: raise SerializationError('invalid output amount (negative)') - scriptPubKey = vds.read_bytes(vds.read_compact_size()) - d['type'], d['address'] = get_address_from_output_script(scriptPubKey) - d['scriptPubKey'] = bh2u(scriptPubKey) - d['prevout_n'] = i - return d - - -def deserialize(raw: str, force_full_parse=False) -> dict: - raw_bytes = bfh(raw) - d = {} - if raw_bytes[:5] == PARTIAL_TXN_HEADER_MAGIC: - d['partial'] = is_partial = True - partial_format_version = raw_bytes[5] - if partial_format_version != 0: - raise SerializationError('unknown tx partial serialization format version: {}' - .format(partial_format_version)) - raw_bytes = raw_bytes[6:] - else: - d['partial'] = is_partial = False - full_parse = force_full_parse or is_partial - vds = BCDataStream() - vds.write(raw_bytes) - d['version'] = vds.read_int32() - n_vin = vds.read_compact_size() - is_segwit = (n_vin == 0) - if is_segwit: - marker = vds.read_bytes(1) - if marker != b'\x01': - raise ValueError('invalid txn marker byte: {}'.format(marker)) - n_vin = vds.read_compact_size() - d['segwit_ser'] = is_segwit - d['inputs'] = [parse_input(vds, full_parse=full_parse) for i in range(n_vin)] - n_vout = vds.read_compact_size() - d['outputs'] = [parse_output(vds, i) for i in range(n_vout)] - if is_segwit: - for i in range(n_vin): - txin = d['inputs'][i] - parse_witness(vds, txin, full_parse=full_parse) - d['lockTime'] = vds.read_uint32() - if vds.can_read_more(): - raise SerializationError('extra junk at the end') - return d + scriptpubkey = vds.read_bytes(vds.read_compact_size()) + return TxOutput(value=value, scriptpubkey=scriptpubkey) # pay & redeem scripts @@ -591,247 +479,139 @@ def multisig_script(public_keys: Sequence[str], m: int) -> str: class Transaction: + _cached_network_ser: Optional[str] def __str__(self): - if self.raw is None: - self.raw = self.serialize() - return self.raw + return self.serialize() def __init__(self, raw): if raw is None: - self.raw = None + self._cached_network_ser = None elif isinstance(raw, str): - self.raw = raw.strip() if raw else None - elif isinstance(raw, dict): - self.raw = raw['hex'] + self._cached_network_ser = raw.strip() if raw else None + assert is_hex_str(self._cached_network_ser) + elif isinstance(raw, (bytes, bytearray)): + self._cached_network_ser = bh2u(raw) else: - raise Exception("cannot initialize transaction", raw) - self._inputs = None + raise Exception(f"cannot initialize transaction from {raw}") + self._inputs = None # type: List[TxInput] self._outputs = None # type: List[TxOutput] self.locktime = 0 self.version = 2 - # by default we assume this is a partial txn; - # this value will get properly set when deserializing - self.is_partial_originally = True - self._segwit_ser = None # None means "don't know" - self.output_info = None # type: Optional[Dict[str, TxOutputHwInfo]] - - def update(self, raw): - self.raw = raw - self._inputs = None - self.deserialize() - def inputs(self): + self._cached_txid = None # type: Optional[str] + + def to_json(self) -> dict: + d = { + 'version': self.version, + 'locktime': self.locktime, + 'inputs': [txin.to_json() for txin in self.inputs()], + 'outputs': [txout.to_json() for txout in self.outputs()], + } + return d + + def inputs(self) -> Sequence[TxInput]: if self._inputs is None: self.deserialize() - return self._inputs or [] + return self._inputs - def outputs(self) -> List[TxOutput]: + def outputs(self) -> Sequence[TxOutput]: if self._outputs is None: self.deserialize() - return self._outputs or [] - - @classmethod - def get_sorted_pubkeys(self, txin): - # sort pubkeys and x_pubkeys, using the order of pubkeys - if txin['type'] == 'coinbase': - return [], [] - x_pubkeys = txin['x_pubkeys'] - pubkeys = txin.get('pubkeys') - if pubkeys is None: - pubkeys = [xpubkey_to_pubkey(x) for x in x_pubkeys] - pubkeys, x_pubkeys = zip(*sorted(zip(pubkeys, x_pubkeys))) - txin['pubkeys'] = pubkeys = list(pubkeys) - txin['x_pubkeys'] = x_pubkeys = list(x_pubkeys) - return pubkeys, x_pubkeys - - def update_signatures(self, signatures: Sequence[str]): - """Add new signatures to a transaction - - `signatures` is expected to be a list of sigs with signatures[i] - intended for self._inputs[i]. - This is used by the Trezor, KeepKey an Safe-T plugins. - """ - if self.is_complete(): - return - if len(self.inputs()) != len(signatures): - raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures))) - for i, txin in enumerate(self.inputs()): - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - sig = signatures[i] - if sig in txin.get('signatures'): - continue - pre_hash = sha256d(bfh(self.serialize_preimage(i))) - sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2])) - for recid in range(4): - try: - public_key = ecc.ECPubkey.from_sig_string(sig_string, recid, pre_hash) - except ecc.InvalidECPointException: - # the point might not be on the curve for some recid values - continue - pubkey_hex = public_key.get_public_key_hex(compressed=True) - if pubkey_hex in pubkeys: - try: - public_key.verify_message_hash(sig_string, pre_hash) - except Exception: - _logger.exception('') - continue - j = pubkeys.index(pubkey_hex) - _logger.info(f"adding sig {i} {j} {pubkey_hex} {sig}") - self.add_signature_to_txin(i, j, sig) - break - # redo raw - self.raw = self.serialize() - - def add_signature_to_txin(self, i, signingPos, sig): - txin = self._inputs[i] - txin['signatures'][signingPos] = sig - txin['scriptSig'] = None # force re-serialization - txin['witness'] = None # force re-serialization - self.raw = None - - def add_inputs_info(self, wallet: 'Abstract_Wallet') -> None: - if self.is_complete(): - return - for txin in self.inputs(): - wallet.add_input_info(txin) + return self._outputs - def remove_signatures(self): - for txin in self.inputs(): - txin['signatures'] = [None] * len(txin['signatures']) - txin['scriptSig'] = None - txin['witness'] = None - assert not self.is_complete() - self.raw = None - - def deserialize(self, force_full_parse=False): - if self.raw is None: + def deserialize(self) -> None: + if self._cached_network_ser is None: return - #self.raw = self.serialize() if self._inputs is not None: return - d = deserialize(self.raw, force_full_parse) - self._inputs = d['inputs'] - self._outputs = [TxOutput(x['type'], x['address'], x['value']) for x in d['outputs']] - self.locktime = d['lockTime'] - self.version = d['version'] - self.is_partial_originally = d['partial'] - self._segwit_ser = d['segwit_ser'] - return d - - @classmethod - def from_io(klass, inputs, outputs, *, locktime=0, version=None): - self = klass(None) - self._inputs = inputs - self._outputs = outputs - self.locktime = locktime - if version is not None: - self.version = version - self.BIP69_sort() - return self - - @classmethod - def pay_script(self, output_type, addr: str) -> str: - """Returns scriptPubKey in hex form.""" - if output_type == TYPE_SCRIPT: - return addr - elif output_type == TYPE_ADDRESS: - return bitcoin.address_to_script(addr) - elif output_type == TYPE_PUBKEY: - return bitcoin.public_key_to_p2pk_script(addr) - else: - raise TypeError('Unknown output type') - - @classmethod - def estimate_pubkey_size_from_x_pubkey(cls, x_pubkey): - try: - if x_pubkey[0:2] in ['02', '03']: # compressed pubkey - return 0x21 - elif x_pubkey[0:2] == '04': # uncompressed pubkey - return 0x41 - elif x_pubkey[0:2] == 'ff': # bip32 extended pubkey - return 0x21 - elif x_pubkey[0:2] == 'fe': # old electrum extended pubkey - return 0x41 - except Exception as e: - pass - return 0x21 # just guess it is compressed - @classmethod - def estimate_pubkey_size_for_txin(cls, txin): - pubkeys = txin.get('pubkeys', []) - x_pubkeys = txin.get('x_pubkeys', []) - if pubkeys and len(pubkeys) > 0: - return cls.estimate_pubkey_size_from_x_pubkey(pubkeys[0]) - elif x_pubkeys and len(x_pubkeys) > 0: - return cls.estimate_pubkey_size_from_x_pubkey(x_pubkeys[0]) - else: - return 0x21 # just guess it is compressed + raw_bytes = bfh(self._cached_network_ser) + vds = BCDataStream() + vds.write(raw_bytes) + self.version = vds.read_int32() + n_vin = vds.read_compact_size() + is_segwit = (n_vin == 0) + if is_segwit: + marker = vds.read_bytes(1) + if marker != b'\x01': + raise ValueError('invalid txn marker byte: {}'.format(marker)) + n_vin = vds.read_compact_size() + self._inputs = [parse_input(vds) for i in range(n_vin)] + n_vout = vds.read_compact_size() + self._outputs = [parse_output(vds) for i in range(n_vout)] + if is_segwit: + for txin in self._inputs: + parse_witness(vds, txin) + self.locktime = vds.read_uint32() + if vds.can_read_more(): + raise SerializationError('extra junk at the end') @classmethod - def get_siglist(self, txin, estimate_size=False): - # if we have enough signatures, we use the actual pubkeys - # otherwise, use extended pubkeys (with bip32 derivation) - if txin['type'] == 'coinbase': + def get_siglist(self, txin: 'PartialTxInput', *, estimate_size=False): + if txin.prevout.is_coinbase(): return [], [] - num_sig = txin.get('num_sig', 1) + if estimate_size: - pubkey_size = self.estimate_pubkey_size_for_txin(txin) - pk_list = ["00" * pubkey_size] * len(txin.get('x_pubkeys', [None])) + try: + pubkey_size = len(txin.pubkeys[0]) + except IndexError: + pubkey_size = 33 # guess it is compressed + num_pubkeys = max(1, len(txin.pubkeys)) + pk_list = ["00" * pubkey_size] * num_pubkeys # we assume that signature will be 0x48 bytes long + num_sig = max(1, txin.num_sig) sig_list = [ "00" * 0x48 ] * num_sig else: - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - x_signatures = txin['signatures'] - signatures = list(filter(None, x_signatures)) - is_complete = len(signatures) == num_sig - if is_complete: - pk_list = pubkeys - sig_list = signatures - else: - pk_list = x_pubkeys - sig_list = [sig if sig else NO_SIGNATURE for sig in x_signatures] + pk_list = [pubkey.hex() for pubkey in txin.pubkeys] + sig_list = [txin.part_sigs.get(pubkey, b'').hex() for pubkey in txin.pubkeys] + if txin.is_complete(): + sig_list = [sig for sig in sig_list if sig] return pk_list, sig_list @classmethod - def serialize_witness(self, txin, estimate_size=False): - _type = txin['type'] - if not self.is_segwit_input(txin) and not txin['type'] == 'address': + def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: + if txin.witness is not None: + return txin.witness.hex() + if txin.prevout.is_coinbase(): + return '' + assert isinstance(txin, PartialTxInput) + + _type = txin.script_type + if not cls.is_segwit_input(txin): return '00' - if _type == 'coinbase': - return txin['witness'] - - witness = txin.get('witness', None) - if witness is None or estimate_size: - if _type == 'address' and estimate_size: - _type = self.guess_txintype_from_address(txin['address']) - pubkeys, sig_list = self.get_siglist(txin, estimate_size) - if _type in ['p2wpkh', 'p2wpkh-p2sh']: - witness = construct_witness([sig_list[0], pubkeys[0]]) - elif _type in ['p2wsh', 'p2wsh-p2sh']: - witness_script = multisig_script(pubkeys, txin['num_sig']) - witness = construct_witness([0] + sig_list + [witness_script]) - else: - witness = txin.get('witness', '00') - if self.is_txin_complete(txin) or estimate_size: - partial_format_witness_prefix = '' - else: - input_value = int_to_hex(txin['value'], 8) - witness_version = int_to_hex(txin.get('witness_version', 0), 2) - partial_format_witness_prefix = var_int(0xffffffff) + input_value + witness_version - return partial_format_witness_prefix + witness + if _type in ('address', 'unknown') and estimate_size: + _type = cls.guess_txintype_from_address(txin.address) + pubkeys, sig_list = cls.get_siglist(txin, estimate_size=estimate_size) + if _type in ['p2wpkh', 'p2wpkh-p2sh']: + return construct_witness([sig_list[0], pubkeys[0]]) + elif _type in ['p2wsh', 'p2wsh-p2sh']: + witness_script = multisig_script(pubkeys, txin.num_sig) + return construct_witness([0] + sig_list + [witness_script]) + elif _type in ['p2pk', 'p2pkh', 'p2sh']: + return '00' + raise UnknownTxinType(f'cannot construct witness for txin_type: {_type}') @classmethod - def is_segwit_input(cls, txin, guess_for_address=False): - _type = txin['type'] + def is_segwit_input(cls, txin: 'TxInput', *, guess_for_address=False) -> bool: + if txin.witness not in (b'\x00', b'', None): + return True + if not isinstance(txin, PartialTxInput): + return False + if txin.is_native_segwit() or txin.is_p2sh_segwit(): + return True + if txin.is_native_segwit() is False and txin.is_p2sh_segwit() is False: + return False + if txin.witness_script: + return True + _type = txin.script_type if _type == 'address' and guess_for_address: - _type = cls.guess_txintype_from_address(txin['address']) - has_nonzero_witness = txin.get('witness', '00') not in ('00', None) - return is_segwit_script_type(_type) or has_nonzero_witness + _type = cls.guess_txintype_from_address(txin.address) + return is_segwit_script_type(_type) @classmethod - def guess_txintype_from_address(cls, addr): + def guess_txintype_from_address(cls, addr: Optional[str]) -> str: # It's not possible to tell the script type in general # just from an address. # - "1" addresses are of course p2pkh @@ -841,6 +621,8 @@ class Transaction: # If we don't know the script, we _guess_ it is pubkeyhash. # As this method is used e.g. for tx size estimation, # the estimation will not be precise. + if addr is None: + return 'p2wpkh' witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr) if witprog is not None: return 'p2wpkh' @@ -849,236 +631,169 @@ class Transaction: return 'p2pkh' elif addrtype == constants.net.ADDRTYPE_P2SH: return 'p2wpkh-p2sh' + raise Exception(f'unrecognized address: {repr(addr)}') @classmethod - def input_script(self, txin, estimate_size=False): - _type = txin['type'] - if _type == 'coinbase': - return txin['scriptSig'] - - # If there is already a saved scriptSig, just return that. - # This allows manual creation of txins of any custom type. - # However, if the txin is not complete, we might have some garbage - # saved from our partial txn ser format, so we re-serialize then. - script_sig = txin.get('scriptSig', None) - if script_sig is not None and self.is_txin_complete(txin): - return script_sig - - pubkeys, sig_list = self.get_siglist(txin, estimate_size) + def input_script(self, txin: TxInput, *, estimate_size=False) -> str: + if txin.script_sig is not None: + return txin.script_sig.hex() + if txin.prevout.is_coinbase(): + return '' + assert isinstance(txin, PartialTxInput) + + if txin.is_p2sh_segwit() and txin.redeem_script: + return push_script(txin.redeem_script.hex()) + if txin.is_native_segwit(): + return '' + + _type = txin.script_type + pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) script = ''.join(push_script(x) for x in sig_list) - if _type == 'address' and estimate_size: - _type = self.guess_txintype_from_address(txin['address']) + if _type in ('address', 'unknown') and estimate_size: + _type = self.guess_txintype_from_address(txin.address) if _type == 'p2pk': - pass + return script elif _type == 'p2sh': # put op_0 before script script = '00' + script - redeem_script = multisig_script(pubkeys, txin['num_sig']) + redeem_script = multisig_script(pubkeys, txin.num_sig) script += push_script(redeem_script) + return script elif _type == 'p2pkh': script += push_script(pubkeys[0]) + return script elif _type in ['p2wpkh', 'p2wsh']: return '' elif _type == 'p2wpkh-p2sh': - pubkey = safe_parse_pubkey(pubkeys[0]) - scriptSig = bitcoin.p2wpkh_nested_script(pubkey) - return push_script(scriptSig) + redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0]) + return push_script(redeem_script) elif _type == 'p2wsh-p2sh': if estimate_size: witness_script = '' else: witness_script = self.get_preimage_script(txin) - scriptSig = bitcoin.p2wsh_nested_script(witness_script) - return push_script(scriptSig) - elif _type == 'address': - return bytes([opcodes.OP_INVALIDOPCODE, opcodes.OP_0]).hex() + push_script(pubkeys[0]) - elif _type == 'unknown': - return txin['scriptSig'] - return script + redeem_script = bitcoin.p2wsh_nested_script(witness_script) + return push_script(redeem_script) + raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}') @classmethod - def is_txin_complete(cls, txin): - if txin['type'] == 'coinbase': - return True - num_sig = txin.get('num_sig', 1) - if num_sig == 0: - return True - x_signatures = txin['signatures'] - signatures = list(filter(None, x_signatures)) - return len(signatures) == num_sig - - @classmethod - def get_preimage_script(self, txin): - preimage_script = txin.get('preimage_script', None) - if preimage_script is not None: - return preimage_script - - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - if txin['type'] in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: - return multisig_script(pubkeys, txin['num_sig']) - elif txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: + def get_preimage_script(cls, txin: 'PartialTxInput') -> str: + if txin.witness_script: + opcodes_in_witness_script = [x[0] for x in script_GetOp(txin.witness_script)] + if opcodes.OP_CODESEPARATOR in opcodes_in_witness_script: + raise Exception('OP_CODESEPARATOR black magic is not supported') + return txin.witness_script.hex() + + pubkeys = [pk.hex() for pk in txin.pubkeys] + if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: + return multisig_script(pubkeys, txin.num_sig) + elif txin.script_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: pubkey = pubkeys[0] pkh = bh2u(hash_160(bfh(pubkey))) return bitcoin.pubkeyhash_to_p2pkh_script(pkh) - elif txin['type'] == 'p2pk': + elif txin.script_type == 'p2pk': pubkey = pubkeys[0] return bitcoin.public_key_to_p2pk_script(pubkey) else: - raise TypeError('Unknown txin type', txin['type']) - - @classmethod - def serialize_outpoint(self, txin): - return bh2u(bfh(txin['prevout_hash'])[::-1]) + int_to_hex(txin['prevout_n'], 4) + raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}') @classmethod - def get_outpoint_from_txin(cls, txin): - if txin['type'] == 'coinbase': - return None - prevout_hash = txin['prevout_hash'] - prevout_n = txin['prevout_n'] - return prevout_hash + ':%d' % prevout_n - - def prevout(self, index): - return self.get_outpoint_from_txin(self.inputs()[index]) - - @classmethod - def serialize_input(self, txin, script): + def serialize_input(self, txin: TxInput, script: str) -> str: # Prev hash and index - s = self.serialize_outpoint(txin) + s = txin.prevout.serialize_to_network().hex() # Script length, script, sequence s += var_int(len(script)//2) s += script - s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) - return s - - def set_rbf(self, rbf): - nSequence = 0xffffffff - (2 if rbf else 1) - for txin in self.inputs(): - txin['sequence'] = nSequence - - def BIP69_sort(self, inputs=True, outputs=True): - # NOTE: other parts of the code rely on these sorts being *stable* sorts - if inputs: - self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) - if outputs: - self._outputs.sort(key = lambda o: (o.value, self.pay_script(o.type, o.address))) - - @classmethod - def serialize_output(cls, output: TxOutput) -> str: - s = int_to_hex(output.value, 8) - script = cls.pay_script(output.type, output.address) - s += var_int(len(script)//2) - s += script + s += int_to_hex(txin.nsequence, 4) return s def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: inputs = self.inputs() outputs = self.outputs() - hashPrevouts = bh2u(sha256d(bfh(''.join(self.serialize_outpoint(txin) for txin in inputs)))) - hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) for txin in inputs)))) - hashOutputs = bh2u(sha256d(bfh(''.join(self.serialize_output(o) for o in outputs)))) + hashPrevouts = bh2u(sha256d(b''.join(txin.prevout.serialize_to_network() for txin in inputs))) + hashSequence = bh2u(sha256d(bfh(''.join(int_to_hex(txin.nsequence, 4) for txin in inputs)))) + hashOutputs = bh2u(sha256d(bfh(''.join(o.serialize_to_network().hex() for o in outputs)))) return BIP143SharedTxDigestFields(hashPrevouts=hashPrevouts, hashSequence=hashSequence, hashOutputs=hashOutputs) - def serialize_preimage(self, txin_index: int, *, - bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: - nVersion = int_to_hex(self.version, 4) - nHashType = int_to_hex(1, 4) # SIGHASH_ALL - nLocktime = int_to_hex(self.locktime, 4) - inputs = self.inputs() - outputs = self.outputs() - txin = inputs[txin_index] - if self.is_segwit_input(txin): - if bip143_shared_txdigest_fields is None: - bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() - hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts - hashSequence = bip143_shared_txdigest_fields.hashSequence - hashOutputs = bip143_shared_txdigest_fields.hashOutputs - outpoint = self.serialize_outpoint(txin) - preimage_script = self.get_preimage_script(txin) - scriptCode = var_int(len(preimage_script) // 2) + preimage_script - amount = int_to_hex(txin['value'], 8) - nSequence = int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) - preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType - else: - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.get_preimage_script(txin) if txin_index==k else '') - for k, txin in enumerate(inputs)) - txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) - preimage = nVersion + txins + txouts + nLocktime + nHashType - return preimage + def is_segwit(self, *, guess_for_address=False): + return any(self.is_segwit_input(txin, guess_for_address=guess_for_address) + for txin in self.inputs()) - def is_segwit(self, guess_for_address=False): - if not self.is_partial_originally: - return self._segwit_ser - return any(self.is_segwit_input(x, guess_for_address=guess_for_address) for x in self.inputs()) + def invalidate_ser_cache(self): + self._cached_network_ser = None + self._cached_txid = None - def serialize(self, estimate_size=False, witness=True): - network_ser = self.serialize_to_network(estimate_size, witness) - if estimate_size: - return network_ser - if self.is_partial_originally and not self.is_complete(): - partial_format_version = '00' - return bh2u(PARTIAL_TXN_HEADER_MAGIC) + partial_format_version + network_ser - else: - return network_ser + def serialize(self) -> str: + if not self._cached_network_ser: + self._cached_network_ser = self.serialize_to_network(estimate_size=False, include_sigs=True) + return self._cached_network_ser + + def serialize_as_bytes(self) -> bytes: + return bfh(self.serialize()) - def serialize_to_network(self, estimate_size=False, witness=True): + def serialize_to_network(self, *, estimate_size=False, include_sigs=True, force_legacy=False) -> str: + """Serialize the transaction as used on the Bitcoin network, into hex. + `include_sigs` signals whether to include scriptSigs and witnesses. + `force_legacy` signals to use the pre-segwit format + note: (not include_sigs) implies force_legacy + """ self.deserialize() nVersion = int_to_hex(self.version, 4) nLocktime = int_to_hex(self.locktime, 4) inputs = self.inputs() outputs = self.outputs() - txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, self.input_script(txin, estimate_size)) for txin in inputs) - txouts = var_int(len(outputs)) + ''.join(self.serialize_output(o) for o in outputs) + + def create_script_sig(txin: TxInput) -> str: + if include_sigs: + return self.input_script(txin, estimate_size=estimate_size) + return '' + txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, create_script_sig(txin)) + for txin in inputs) + txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs) + use_segwit_ser_for_estimate_size = estimate_size and self.is_segwit(guess_for_address=True) - use_segwit_ser_for_actual_use = not estimate_size and \ - (self.is_segwit() or any(txin['type'] == 'address' for txin in inputs)) + use_segwit_ser_for_actual_use = not estimate_size and self.is_segwit() use_segwit_ser = use_segwit_ser_for_estimate_size or use_segwit_ser_for_actual_use - if witness and use_segwit_ser: + if include_sigs and not force_legacy and use_segwit_ser: marker = '00' flag = '01' - witness = ''.join(self.serialize_witness(x, estimate_size) for x in inputs) + witness = ''.join(self.serialize_witness(x, estimate_size=estimate_size) for x in inputs) return nVersion + marker + flag + txins + txouts + witness + nLocktime else: return nVersion + txins + txouts + nLocktime - def txid(self): - self.deserialize() - all_segwit = all(self.is_segwit_input(x) for x in self.inputs()) - if not all_segwit and not self.is_complete(): - return None - ser = self.serialize_to_network(witness=False) - return bh2u(sha256d(bfh(ser))[::-1]) - - def wtxid(self): + def txid(self) -> Optional[str]: + if self._cached_txid is None: + self.deserialize() + all_segwit = all(self.is_segwit_input(x) for x in self.inputs()) + if not all_segwit and not self.is_complete(): + return None + try: + ser = self.serialize_to_network(force_legacy=True) + except UnknownTxinType: + # we might not know how to construct scriptSig for some scripts + return None + self._cached_txid = bh2u(sha256d(bfh(ser))[::-1]) + return self._cached_txid + + def wtxid(self) -> Optional[str]: self.deserialize() if not self.is_complete(): return None - ser = self.serialize_to_network(witness=True) + try: + ser = self.serialize_to_network() + except UnknownTxinType: + # we might not know how to construct scriptSig/witness for some scripts + return None return bh2u(sha256d(bfh(ser))[::-1]) - def add_inputs(self, inputs): - self._inputs.extend(inputs) - self.raw = None - self.BIP69_sort(outputs=False) - - def add_outputs(self, outputs): - self._outputs.extend(outputs) - self.raw = None - self.BIP69_sort(inputs=False) - - def input_value(self) -> int: - return sum(x['value'] for x in self.inputs()) - - def output_value(self) -> int: - return sum(o.value for o in self.outputs()) - - def get_fee(self) -> int: - return self.input_value() - self.output_value() + def add_info_from_wallet(self, wallet: 'Abstract_Wallet') -> None: + return # no-op def is_final(self): - return not any([x.get('sequence', 0xffffffff - 1) < 0xffffffff - 1 for x in self.inputs()]) + return not any([txin.nsequence < 0xffffffff - 1 for txin in self.inputs()]) def estimated_size(self): """Return an estimated virtual tx size in vbytes. @@ -1093,11 +808,11 @@ class Transaction: @classmethod def estimated_input_weight(cls, txin, is_segwit_tx): '''Return an estimate of serialized input weight in weight units.''' - script = cls.input_script(txin, True) + script = cls.input_script(txin, estimate_size=True) input_size = len(cls.serialize_input(txin, script)) // 2 if cls.is_segwit_input(txin, guess_for_address=True): - witness_size = len(cls.serialize_witness(txin, True)) // 2 + witness_size = len(cls.serialize_witness(txin, estimate_size=True)) // 2 else: witness_size = 1 if is_segwit_tx else 0 @@ -1116,7 +831,10 @@ class Transaction: def estimated_total_size(self): """Return an estimated total transaction size in bytes.""" - return len(self.serialize(True)) // 2 if not self.is_complete() or self.raw is None else len(self.raw) // 2 # ASCII hex string + if not self.is_complete() or self._cached_network_ser is None: + return len(self.serialize_to_network(estimate_size=True)) // 2 + else: + return len(self._cached_network_ser) // 2 # ASCII hex string def estimated_witness_size(self): """Return an estimate of witness size in bytes.""" @@ -1124,7 +842,7 @@ class Transaction: if not self.is_segwit(guess_for_address=estimate): return 0 inputs = self.inputs() - witness = ''.join(self.serialize_witness(x, estimate) for x in inputs) + witness = ''.join(self.serialize_witness(x, estimate_size=estimate) for x in inputs) witness_size = len(witness) // 2 + 2 # include marker and flag return witness_size @@ -1138,66 +856,8 @@ class Transaction: base_tx_size = self.estimated_base_size() return 3 * base_tx_size + total_tx_size - def signature_count(self): - r = 0 - s = 0 - for txin in self.inputs(): - if txin['type'] == 'coinbase': - continue - signatures = list(filter(None, txin.get('signatures',[]))) - s += len(signatures) - r += txin.get('num_sig',-1) - return s, r - - def is_complete(self): - s, r = self.signature_count() - return r == s - - def sign(self, keypairs) -> None: - # keypairs: (x_)pubkey -> secret_bytes - bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() - for i, txin in enumerate(self.inputs()): - pubkeys, x_pubkeys = self.get_sorted_pubkeys(txin) - for j, (pubkey, x_pubkey) in enumerate(zip(pubkeys, x_pubkeys)): - if self.is_txin_complete(txin): - break - if pubkey in keypairs: - _pubkey = pubkey - elif x_pubkey in keypairs: - _pubkey = x_pubkey - else: - continue - _logger.info(f"adding signature for {_pubkey}") - sec, compressed = keypairs.get(_pubkey) - sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) - self.add_signature_to_txin(i, j, sig) - - _logger.debug(f"is_complete {self.is_complete()}") - self.raw = self.serialize() - - def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields=None) -> str: - pre_hash = sha256d(bfh(self.serialize_preimage(txin_index, - bip143_shared_txdigest_fields=bip143_shared_txdigest_fields))) - privkey = ecc.ECPrivkey(privkey_bytes) - sig = privkey.sign_transaction(pre_hash) - sig = bh2u(sig) + '01' - return sig - - def get_outputs_for_UI(self) -> Sequence[TxOutputForUI]: - outputs = [] - for o in self.outputs(): - if o.type == TYPE_ADDRESS: - addr = o.address - elif o.type == TYPE_PUBKEY: - addr = 'PUBKEY ' + o.address - else: - addr = 'SCRIPT ' + o.address - outputs.append(TxOutputForUI(addr, o.value)) # consider using yield - return outputs - - def has_address(self, addr: str) -> bool: - return (addr in (o.address for o in self.outputs())) \ - or (addr in (txin.get("address") for txin in self.inputs())) + def is_complete(self) -> bool: + return True def get_output_idxs_from_scriptpubkey(self, script: str) -> Set[int]: """Returns the set indices of outputs with given script.""" @@ -1208,7 +868,7 @@ class Transaction: if not hasattr(self, '_script_to_output_idx'): d = defaultdict(set) for output_idx, o in enumerate(self.outputs()): - o_script = self.pay_script(o.type, o.address) + o_script = o.scriptpubkey.hex() assert isinstance(o_script, str) d[o_script].add(output_idx) self._script_to_output_idx = d @@ -1218,27 +878,10 @@ class Transaction: script = bitcoin.address_to_script(addr) return self.get_output_idxs_from_scriptpubkey(script) - def as_dict(self): - if self.raw is None: - self.raw = self.serialize() - self.deserialize() - out = { - 'hex': self.raw, - 'complete': self.is_complete(), - 'final': self.is_final(), - } - return out - - @classmethod - def from_dict(cls, d): - tx = cls(d['hex']) - tx.deserialize(True) - return tx - -def tx_from_str(txt: str) -> str: - """Sanitizes tx-describing input (json or raw hex or base43) into - raw hex transaction.""" +def convert_tx_str_to_hex(txt: str) -> str: + """Sanitizes tx-describing input (hex/base43/base64) into + raw tx hex string.""" assert isinstance(txt, str), f"txt must be str, not {type(txt)}" txt = txt.strip() if not txt: @@ -1254,8 +897,997 @@ def tx_from_str(txt: str) -> str: return base_decode(txt, length=None, base=43).hex() except: pass - # try json - import json - tx_dict = json.loads(str(txt)) - assert "hex" in tx_dict.keys() - return tx_dict["hex"] + # try base64 + if txt[0:6] == 'cHNidP': # base64 psbt + try: + return base64.b64decode(txt).hex() + except: + pass + raise ValueError(f"failed to recognize transaction encoding for txt: {txt[:30]}...") + + +def tx_from_any(raw: Union[str, bytes]) -> Union['PartialTransaction', 'Transaction']: + if isinstance(raw, (bytes, bytearray)): + raw = raw.hex() + raw = convert_tx_str_to_hex(raw) + try: + return PartialTransaction.from_raw_psbt(raw) + except BadHeaderMagic: + if raw[:10] == b'EPTF\xff'.hex(): + raise Exception("Partial transactions generated with old Electrum versions " + "(< 4.0) are no longer supported. Please upgrade Electrum on " + "the other machine where this transaction was created.") + tx = Transaction(raw) + tx.deserialize() + return tx + + +class PSBTGlobalType(IntEnum): + UNSIGNED_TX = 0 + XPUB = 1 + VERSION = 0xFB + + +class PSBTInputType(IntEnum): + NON_WITNESS_UTXO = 0 + WITNESS_UTXO = 1 + PARTIAL_SIG = 2 + SIGHASH_TYPE = 3 + REDEEM_SCRIPT = 4 + WITNESS_SCRIPT = 5 + BIP32_DERIVATION = 6 + FINAL_SCRIPTSIG = 7 + FINAL_SCRIPTWITNESS = 8 + + +class PSBTOutputType(IntEnum): + REDEEM_SCRIPT = 0 + WITNESS_SCRIPT = 1 + BIP32_DERIVATION = 2 + + +# Serialization/deserialization tools +def deser_compact_size(f) -> Optional[int]: + 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 + + +class PSBTSection: + + def _populate_psbt_fields_from_fd(self, fd=None): + if not fd: return + + while True: + try: + key_type, key, val = self.get_next_kv_from_fd(fd) + except StopIteration: + break + self.parse_psbt_section_kv(key_type, key, val) + + @classmethod + def get_next_kv_from_fd(cls, fd) -> Tuple[int, bytes, bytes]: + key_size = deser_compact_size(fd) + if key_size == 0: + raise StopIteration() + if key_size is None: + raise UnexpectedEndOfStream() + + full_key = fd.read(key_size) + key_type, key = cls.get_keytype_and_key_from_fullkey(full_key) + + val_size = deser_compact_size(fd) + if val_size is None: raise UnexpectedEndOfStream() + val = fd.read(val_size) + + return key_type, key, val + + @classmethod + def create_psbt_writer(cls, fd): + def wr(key_type: int, val: bytes, key: bytes = b''): + full_key = cls.get_fullkey_from_keytype_and_key(key_type, key) + fd.write(bytes.fromhex(var_int(len(full_key)))) # key_size + fd.write(full_key) # key + fd.write(bytes.fromhex(var_int(len(val)))) # val_size + fd.write(val) # val + return wr + + @classmethod + def get_keytype_and_key_from_fullkey(cls, full_key: bytes) -> Tuple[int, bytes]: + with io.BytesIO(full_key) as key_stream: + key_type = deser_compact_size(key_stream) + if key_type is None: raise UnexpectedEndOfStream() + key = key_stream.read() + return key_type, key + + @classmethod + def get_fullkey_from_keytype_and_key(cls, key_type: int, key: bytes) -> bytes: + key_type_bytes = bytes.fromhex(var_int(key_type)) + return key_type_bytes + key + + def _serialize_psbt_section(self, fd): + wr = self.create_psbt_writer(fd) + self.serialize_psbt_section_kvs(wr) + fd.write(b'\x00') # section-separator + + def parse_psbt_section_kv(self, kt: int, key: bytes, val: bytes) -> None: + raise NotImplementedError() # implemented by subclasses + + def serialize_psbt_section_kvs(self, wr) -> None: + raise NotImplementedError() # implemented by subclasses + + +class PartialTxInput(TxInput, PSBTSection): + def __init__(self, *args, **kwargs): + TxInput.__init__(self, *args, **kwargs) + self.utxo = None # type: Optional[Transaction] + self.witness_utxo = None # type: Optional[TxOutput] + self.part_sigs = {} # type: Dict[bytes, bytes] # pubkey -> sig + self.sighash = None # type: Optional[int] + self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) + self.redeem_script = None # type: Optional[bytes] + self.witness_script = None # type: Optional[bytes] + self._unknown = {} # type: Dict[bytes, bytes] + + self.script_type = 'unknown' + self.num_sig = 0 # type: int # num req sigs for multisig + self.pubkeys = [] # type: List[bytes] # note: order matters + self._trusted_value_sats = None # type: Optional[int] + self._trusted_address = None # type: Optional[str] + self.block_height = None # type: Optional[int] # height at which the TXO is mined; None means unknown + self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown + self._is_native_segwit = None # type: Optional[bool] # None means unknown + + def to_json(self): + d = super().to_json() + d.update({ + 'height': self.block_height, + 'value_sats': self.value_sats(), + 'address': self.address, + 'utxo': str(self.utxo) if self.utxo else None, + 'witness_utxo': self.witness_utxo.serialize_to_network().hex() if self.witness_utxo else None, + 'sighash': self.sighash, + 'redeem_script': self.redeem_script.hex() if self.redeem_script else None, + 'witness_script': self.witness_script.hex() if self.witness_script else None, + 'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()}, + 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) + for pubkey, (xfp, path) in self.bip32_paths.items()}, + 'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()}, + }) + return d + + @classmethod + def from_txin(cls, txin: TxInput, *, strip_witness: bool = True) -> 'PartialTxInput': + res = PartialTxInput(prevout=txin.prevout, + script_sig=None if strip_witness else txin.script_sig, + nsequence=txin.nsequence, + witness=None if strip_witness else txin.witness) + return res + + def validate_data(self, *, for_signing=False) -> None: + if self.utxo: + if self.prevout.txid.hex() != self.utxo.txid(): + raise PSBTInputConsistencyFailure(f"PSBT input validation: " + f"If a non-witness UTXO is provided, its hash must match the hash specified in the prevout") + # The following test is disabled, so we are willing to sign non-segwit inputs + # without verifying the input amount. This means, given a maliciously modified PSBT, + # for non-segwit inputs, we might end up burning coins as miner fees. + if for_signing and False: + if not Transaction.is_segwit_input(self) and self.witness_utxo: + raise PSBTInputConsistencyFailure(f"PSBT input validation: " + f"If a witness UTXO is provided, no non-witness signature may be created") + if self.redeem_script and self.address: + addr = hash160_to_p2sh(hash_160(self.redeem_script)) + if self.address != addr: + raise PSBTInputConsistencyFailure(f"PSBT input validation: " + f"If a redeemScript is provided, the scriptPubKey must be for that redeemScript") + if self.witness_script: + if self.redeem_script: + if self.redeem_script != bfh(bitcoin.p2wsh_nested_script(self.witness_script.hex())): + raise PSBTInputConsistencyFailure(f"PSBT input validation: " + f"If a witnessScript is provided, the redeemScript must be for that witnessScript") + elif self.address: + if self.address != bitcoin.script_to_p2wsh(self.witness_script.hex()): + raise PSBTInputConsistencyFailure(f"PSBT input validation: " + f"If a witnessScript is provided, the scriptPubKey must be for that witnessScript") + + def parse_psbt_section_kv(self, kt, key, val): + try: + kt = PSBTInputType(kt) + except ValueError: + pass # unknown type + if DEBUG_PSBT_PARSING: print(f"{repr(kt)} {key.hex()} {val.hex()}") + if kt == PSBTInputType.NON_WITNESS_UTXO: + if self.utxo is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + if self.witness_utxo is not None: + raise SerializationError(f"PSBT input cannot have both PSBT_IN_NON_WITNESS_UTXO and PSBT_IN_WITNESS_UTXO") + self.utxo = Transaction(val) + self.utxo.deserialize() + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.WITNESS_UTXO: + if self.witness_utxo is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + if self.utxo is not None: + raise SerializationError(f"PSBT input cannot have both PSBT_IN_NON_WITNESS_UTXO and PSBT_IN_WITNESS_UTXO") + self.witness_utxo = TxOutput.from_network_bytes(val) + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.PARTIAL_SIG: + if key in self.part_sigs: + raise SerializationError(f"duplicate key: {repr(kt)}") + if len(key) not in (33, 65): # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32... + raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}") + self.part_sigs[key] = val + elif kt == PSBTInputType.SIGHASH_TYPE: + if self.sighash is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + if len(val) != 4: + raise SerializationError(f"value for {repr(kt)} has unexpected length: {len(val)}") + self.sighash = struct.unpack("<I", val)[0] + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.BIP32_DERIVATION: + if key in self.bip32_paths: + raise SerializationError(f"duplicate key: {repr(kt)}") + if len(key) not in (33, 65): # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32... + raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}") + self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val) + elif kt == PSBTInputType.REDEEM_SCRIPT: + if self.redeem_script is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.redeem_script = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.WITNESS_SCRIPT: + if self.witness_script is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.witness_script = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.FINAL_SCRIPTSIG: + if self.script_sig is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.script_sig = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.FINAL_SCRIPTWITNESS: + if self.witness is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.witness = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + else: + full_key = self.get_fullkey_from_keytype_and_key(kt, key) + if full_key in self._unknown: + raise SerializationError(f'duplicate key. PSBT input key for unknown type: {full_key}') + self._unknown[full_key] = val + + def serialize_psbt_section_kvs(self, wr): + if self.witness_utxo: + wr(PSBTInputType.WITNESS_UTXO, self.witness_utxo.serialize_to_network()) + elif self.utxo: + wr(PSBTInputType.NON_WITNESS_UTXO, bfh(self.utxo.serialize_to_network(include_sigs=True))) + for pk, val in sorted(self.part_sigs.items()): + wr(PSBTInputType.PARTIAL_SIG, val, pk) + if self.sighash is not None: + wr(PSBTInputType.SIGHASH_TYPE, struct.pack('<I', self.sighash)) + if self.redeem_script is not None: + wr(PSBTInputType.REDEEM_SCRIPT, self.redeem_script) + if self.witness_script is not None: + wr(PSBTInputType.WITNESS_SCRIPT, self.witness_script) + for k in sorted(self.bip32_paths): + packed_path = pack_bip32_root_fingerprint_and_int_path(*self.bip32_paths[k]) + wr(PSBTInputType.BIP32_DERIVATION, packed_path, k) + if self.script_sig is not None: + wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig) + if self.witness is not None: + wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness) + for full_key, val in sorted(self._unknown.items()): + key_type, key = self.get_keytype_and_key_from_fullkey(full_key) + wr(key_type, val, key=key) + + def value_sats(self) -> Optional[int]: + if self._trusted_value_sats is not None: + return self._trusted_value_sats + if self.utxo: + out_idx = self.prevout.out_idx + return self.utxo.outputs()[out_idx].value + if self.witness_utxo: + return self.witness_utxo.value + return None + + @property + def address(self) -> Optional[str]: + if self._trusted_address is not None: + return self._trusted_address + scriptpubkey = self.scriptpubkey + if scriptpubkey: + return get_address_from_output_script(scriptpubkey) + return None + + @property + def scriptpubkey(self) -> Optional[bytes]: + if self._trusted_address is not None: + return bfh(bitcoin.address_to_script(self._trusted_address)) + if self.utxo: + out_idx = self.prevout.out_idx + return self.utxo.outputs()[out_idx].scriptpubkey + if self.witness_utxo: + return self.witness_utxo.scriptpubkey + return None + + def is_complete(self) -> bool: + if self.script_sig is not None and self.witness is not None: + return True + if self.prevout.is_coinbase(): + return True + if self.script_sig is not None and not Transaction.is_segwit_input(self): + return True + signatures = list(self.part_sigs.values()) + s = len(signatures) + # note: The 'script_type' field is currently only set by the wallet, + # for its own addresses. This means we can only finalize inputs + # that are related to the wallet. + # The 'fix' would be adding extra logic that matches on templates, + # and figures out the script_type from available fields. + if self.script_type in ('p2pk', 'p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): + return s >= 1 + if self.script_type in ('p2sh', 'p2wsh', 'p2wsh-p2sh'): + return s >= self.num_sig + return False + + def finalize(self) -> None: + def clear_fields_when_finalized(): + # BIP-174: "All other data except the UTXO and unknown fields in the + # input key-value map should be cleared from the PSBT" + self.part_sigs = {} + self.sighash = None + self.bip32_paths = {} + self.redeem_script = None + self.witness_script = None + + if self.script_sig is not None and self.witness is not None: + clear_fields_when_finalized() + return # already finalized + if self.is_complete(): + self.script_sig = bfh(Transaction.input_script(self)) + self.witness = bfh(Transaction.serialize_witness(self)) + clear_fields_when_finalized() + + def combine_with_other_txin(self, other_txin: 'TxInput') -> None: + assert self.prevout == other_txin.prevout + if other_txin.script_sig is not None: + self.script_sig = other_txin.script_sig + if other_txin.witness is not None: + self.witness = other_txin.witness + if isinstance(other_txin, PartialTxInput): + if other_txin.witness_utxo: + self.witness_utxo = other_txin.witness_utxo + if other_txin.utxo: + self.utxo = other_txin.utxo + self.part_sigs.update(other_txin.part_sigs) + if other_txin.sighash is not None: + self.sighash = other_txin.sighash + self.bip32_paths.update(other_txin.bip32_paths) + if other_txin.redeem_script is not None: + self.redeem_script = other_txin.redeem_script + if other_txin.witness_script is not None: + self.witness_script = other_txin.witness_script + self._unknown.update(other_txin._unknown) + self.ensure_there_is_only_one_utxo() + # try to finalize now + self.finalize() + + def ensure_there_is_only_one_utxo(self): + if self.utxo is not None and self.witness_utxo is not None: + if Transaction.is_segwit_input(self): + self.utxo = None + else: + self.witness_utxo = None + + def convert_utxo_to_witness_utxo(self) -> None: + if self.utxo: + self.witness_utxo = self.utxo.outputs()[self.prevout.out_idx] + self.utxo = None # type: Optional[Transaction] + + def is_native_segwit(self) -> Optional[bool]: + """Whether this input is native segwit. None means inconclusive.""" + if self._is_native_segwit is None: + if self.address: + self._is_native_segwit = bitcoin.is_segwit_address(self.address) + return self._is_native_segwit + + def is_p2sh_segwit(self) -> Optional[bool]: + """Whether this input is p2sh-embedded-segwit. None means inconclusive.""" + if self._is_p2sh_segwit is None: + def calc_if_p2sh_segwit_now(): + if not (self.address and self.redeem_script): + return None + if self.address != bitcoin.hash160_to_p2sh(hash_160(self.redeem_script)): + # not p2sh address + return False + try: + decoded = [x for x in script_GetOp(self.redeem_script)] + except MalformedBitcoinScript: + decoded = None + # witness version 0 + match = [opcodes.OP_0, OPPushDataGeneric(lambda x: x in (20, 32))] + if match_decoded(decoded, match): + return True + # witness version 1-16 + future_witness_versions = list(range(opcodes.OP_1, opcodes.OP_16 + 1)) + for witver, opcode in enumerate(future_witness_versions, start=1): + match = [opcode, OPPushDataGeneric(lambda x: 2 <= x <= 40)] + if match_decoded(decoded, match): + return True + return False + + self._is_p2sh_segwit = calc_if_p2sh_segwit_now() + return self._is_p2sh_segwit + + +class PartialTxOutput(TxOutput, PSBTSection): + def __init__(self, *args, **kwargs): + TxOutput.__init__(self, *args, **kwargs) + self.redeem_script = None # type: Optional[bytes] + self.witness_script = None # type: Optional[bytes] + self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) + self._unknown = {} # type: Dict[bytes, bytes] + + self.script_type = 'unknown' + self.num_sig = 0 # num req sigs for multisig + self.pubkeys = [] # type: List[bytes] # note: order matters + self.is_mine = False # type: bool # whether the wallet considers the output to be ismine + self.is_change = False # type: bool # whether the wallet considers the output to be change + + def to_json(self): + d = super().to_json() + d.update({ + 'redeem_script': self.redeem_script.hex() if self.redeem_script else None, + 'witness_script': self.witness_script.hex() if self.witness_script else None, + 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) + for pubkey, (xfp, path) in self.bip32_paths.items()}, + 'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()}, + }) + return d + + @classmethod + def from_txout(cls, txout: TxOutput) -> 'PartialTxOutput': + res = PartialTxOutput(scriptpubkey=txout.scriptpubkey, + value=txout.value) + return res + + def parse_psbt_section_kv(self, kt, key, val): + try: + kt = PSBTOutputType(kt) + except ValueError: + pass # unknown type + if DEBUG_PSBT_PARSING: print(f"{repr(kt)} {key.hex()} {val.hex()}") + if kt == PSBTOutputType.REDEEM_SCRIPT: + if self.redeem_script is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.redeem_script = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTOutputType.WITNESS_SCRIPT: + if self.witness_script is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.witness_script = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTOutputType.BIP32_DERIVATION: + if key in self.bip32_paths: + raise SerializationError(f"duplicate key: {repr(kt)}") + if len(key) not in (33, 65): # TODO also allow 32? one of the tests in the BIP is "supposed to" fail with len==32... + raise SerializationError(f"key for {repr(kt)} has unexpected length: {len(key)}") + self.bip32_paths[key] = unpack_bip32_root_fingerprint_and_int_path(val) + else: + full_key = self.get_fullkey_from_keytype_and_key(kt, key) + if full_key in self._unknown: + raise SerializationError(f'duplicate key. PSBT output key for unknown type: {full_key}') + self._unknown[full_key] = val + + def serialize_psbt_section_kvs(self, wr): + if self.redeem_script is not None: + wr(PSBTOutputType.REDEEM_SCRIPT, self.redeem_script) + if self.witness_script is not None: + wr(PSBTOutputType.WITNESS_SCRIPT, self.witness_script) + for k in sorted(self.bip32_paths): + packed_path = pack_bip32_root_fingerprint_and_int_path(*self.bip32_paths[k]) + wr(PSBTOutputType.BIP32_DERIVATION, packed_path, k) + for full_key, val in sorted(self._unknown.items()): + key_type, key = self.get_keytype_and_key_from_fullkey(full_key) + wr(key_type, val, key=key) + + def combine_with_other_txout(self, other_txout: 'TxOutput') -> None: + assert self.scriptpubkey == other_txout.scriptpubkey + if not isinstance(other_txout, PartialTxOutput): + return + if other_txout.redeem_script is not None: + self.redeem_script = other_txout.redeem_script + if other_txout.witness_script is not None: + self.witness_script = other_txout.witness_script + self.bip32_paths.update(other_txout.bip32_paths) + self._unknown.update(other_txout._unknown) + + +class PartialTransaction(Transaction): + + def __init__(self, raw_unsigned_tx): + Transaction.__init__(self, raw_unsigned_tx) + self.xpubs = {} # type: Dict[BIP32Node, Tuple[bytes, Sequence[int]]] # intermediate bip32node -> (xfp, der_prefix) + self._inputs = [] # type: List[PartialTxInput] + self._outputs = [] # type: List[PartialTxOutput] + self._unknown = {} # type: Dict[bytes, bytes] + + def to_json(self) -> dict: + d = super().to_json() + d.update({ + 'xpubs': {bip32node.to_xpub(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) + for bip32node, (xfp, path) in self.xpubs.items()}, + 'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()}, + }) + return d + + @classmethod + def from_tx(cls, tx: Transaction) -> 'PartialTransaction': + res = cls(None) + res._inputs = [PartialTxInput.from_txin(txin) for txin in tx.inputs()] + res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()] + res.version = tx.version + res.locktime = tx.locktime + return res + + @classmethod + def from_raw_psbt(cls, raw) -> 'PartialTransaction': + # auto-detect and decode Base64 and Hex. + if raw[0:10].lower() in (b'70736274ff', '70736274ff'): # hex + raw = bytes.fromhex(raw.strip()) + elif raw[0:6] in (b'cHNidP', 'cHNidP'): # base64 + raw = base64.b64decode(raw) + if not isinstance(raw, (bytes, bytearray)) or raw[0:5] != b'psbt\xff': + raise BadHeaderMagic("bad magic") + + tx = None # type: Optional[PartialTransaction] + + # We parse the raw stream twice. The first pass is used to find the + # PSBT_GLOBAL_UNSIGNED_TX key in the global section and set 'tx'. + # The second pass does everything else. + with io.BytesIO(raw[5:]) as fd: # parsing "first pass" + while True: + try: + kt, key, val = PSBTSection.get_next_kv_from_fd(fd) + except StopIteration: + break + try: + kt = PSBTGlobalType(kt) + except ValueError: + pass # unknown type + if kt == PSBTGlobalType.UNSIGNED_TX: + if tx is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + unsigned_tx = Transaction(val.hex()) + for txin in unsigned_tx.inputs(): + if txin.script_sig or txin.witness: + raise SerializationError(f"PSBT {repr(kt)} must have empty scriptSigs and witnesses") + tx = PartialTransaction.from_tx(unsigned_tx) + + if tx is None: + raise SerializationError(f"PSBT missing required global section PSBT_GLOBAL_UNSIGNED_TX") + + with io.BytesIO(raw[5:]) as fd: # parsing "second pass" + # global section + while True: + try: + kt, key, val = PSBTSection.get_next_kv_from_fd(fd) + except StopIteration: + break + try: + kt = PSBTGlobalType(kt) + except ValueError: + pass # unknown type + if DEBUG_PSBT_PARSING: print(f"{repr(kt)} {key.hex()} {val.hex()}") + if kt == PSBTGlobalType.UNSIGNED_TX: + pass # already handled during "first" parsing pass + elif kt == PSBTGlobalType.XPUB: + bip32node = BIP32Node.from_bytes(key) + if bip32node in tx.xpubs: + raise SerializationError(f"duplicate key: {repr(kt)}") + xfp, path = unpack_bip32_root_fingerprint_and_int_path(val) + if bip32node.depth != len(path): + raise SerializationError(f"PSBT global xpub has mismatching depth ({bip32node.depth}) " + f"and derivation prefix len ({len(path)})") + child_number_of_xpub = int.from_bytes(bip32node.child_number, 'big') + if not ((bip32node.depth == 0 and child_number_of_xpub == 0) + or (bip32node.depth != 0 and child_number_of_xpub == path[-1])): + raise SerializationError(f"PSBT global xpub has inconsistent child_number and derivation prefix") + tx.xpubs[bip32node] = xfp, path + elif kt == PSBTGlobalType.VERSION: + if len(val) > 4: + raise SerializationError(f"value for {repr(kt)} has unexpected length: {len(val)} > 4") + psbt_version = int.from_bytes(val, byteorder='little', signed=False) + if psbt_version > 0: + raise SerializationError(f"Only PSBTs with version 0 are supported. Found version: {psbt_version}") + if key: raise SerializationError(f"key for {repr(kt)} must be empty") + else: + full_key = PSBTSection.get_fullkey_from_keytype_and_key(kt, key) + if full_key in tx._unknown: + raise SerializationError(f'duplicate key. PSBT global key for unknown type: {full_key}') + tx._unknown[full_key] = val + try: + # inputs sections + for txin in tx.inputs(): + if DEBUG_PSBT_PARSING: print("-> new input starts") + txin._populate_psbt_fields_from_fd(fd) + # outputs sections + for txout in tx.outputs(): + if DEBUG_PSBT_PARSING: print("-> new output starts") + txout._populate_psbt_fields_from_fd(fd) + except UnexpectedEndOfStream: + raise UnexpectedEndOfStream('Unexpected end of stream. Num input and output maps provided does not match unsigned tx.') from None + + if fd.read(1) != b'': + raise SerializationError("extra junk at the end of PSBT") + + for txin in tx.inputs(): + txin.validate_data() + + return tx + + @classmethod + def from_io(cls, inputs: Sequence[PartialTxInput], outputs: Sequence[PartialTxOutput], *, + locktime: int = None, version: int = None): + self = cls(None) + self._inputs = list(inputs) + self._outputs = list(outputs) + if locktime is not None: + self.locktime = locktime + if version is not None: + self.version = version + self.BIP69_sort() + return self + + def _serialize_psbt(self, fd) -> None: + wr = PSBTSection.create_psbt_writer(fd) + fd.write(b'psbt\xff') + # global section + wr(PSBTGlobalType.UNSIGNED_TX, bfh(self.serialize_to_network(include_sigs=False))) + for bip32node, (xfp, path) in sorted(self.xpubs.items()): + val = pack_bip32_root_fingerprint_and_int_path(xfp, path) + wr(PSBTGlobalType.XPUB, val, key=bip32node.to_bytes()) + for full_key, val in sorted(self._unknown.items()): + key_type, key = PSBTSection.get_keytype_and_key_from_fullkey(full_key) + wr(key_type, val, key=key) + fd.write(b'\x00') # section-separator + # input sections + for inp in self._inputs: + inp._serialize_psbt_section(fd) + # output sections + for outp in self._outputs: + outp._serialize_psbt_section(fd) + + def finalize_psbt(self) -> None: + for txin in self.inputs(): + txin.finalize() + + def combine_with_other_psbt(self, other_tx: 'Transaction') -> None: + """Pulls in all data from other_tx we don't yet have (e.g. signatures). + other_tx must be concerning the same unsigned tx. + """ + if self.serialize_to_network(include_sigs=False) != other_tx.serialize_to_network(include_sigs=False): + raise Exception('A Combiner must not combine two different PSBTs.') + # BIP-174: "The resulting PSBT must contain all of the key-value pairs from each of the PSBTs. + # The Combiner must remove any duplicate key-value pairs, in accordance with the specification." + # global section + if isinstance(other_tx, PartialTransaction): + self.xpubs.update(other_tx.xpubs) + self._unknown.update(other_tx._unknown) + # input sections + for txin, other_txin in zip(self.inputs(), other_tx.inputs()): + txin.combine_with_other_txin(other_txin) + # output sections + for txout, other_txout in zip(self.outputs(), other_tx.outputs()): + txout.combine_with_other_txout(other_txout) + self.invalidate_ser_cache() + + def join_with_other_psbt(self, other_tx: 'PartialTransaction') -> None: + """Adds inputs and outputs from other_tx into this one.""" + if not isinstance(other_tx, PartialTransaction): + raise Exception('Can only join partial transactions.') + # make sure there are no duplicate prevouts + prevouts = set() + for txin in itertools.chain(self.inputs(), other_tx.inputs()): + prevout_str = txin.prevout.to_str() + if prevout_str in prevouts: + raise Exception(f"Duplicate inputs! " + f"Transactions that spend the same prevout cannot be joined.") + prevouts.add(prevout_str) + # copy global PSBT section + self.xpubs.update(other_tx.xpubs) + self._unknown.update(other_tx._unknown) + # copy and add inputs and outputs + self.add_inputs(list(other_tx.inputs())) + self.add_outputs(list(other_tx.outputs())) + self.remove_signatures() + self.invalidate_ser_cache() + + def inputs(self) -> Sequence[PartialTxInput]: + return self._inputs + + def outputs(self) -> Sequence[PartialTxOutput]: + return self._outputs + + def add_inputs(self, inputs: List[PartialTxInput]) -> None: + self._inputs.extend(inputs) + self.BIP69_sort(outputs=False) + self.invalidate_ser_cache() + + def add_outputs(self, outputs: List[PartialTxOutput]) -> None: + self._outputs.extend(outputs) + self.BIP69_sort(inputs=False) + self.invalidate_ser_cache() + + def set_rbf(self, rbf: bool) -> None: + nSequence = 0xffffffff - (2 if rbf else 1) + for txin in self.inputs(): + txin.nsequence = nSequence + self.invalidate_ser_cache() + + def BIP69_sort(self, inputs=True, outputs=True): + # NOTE: other parts of the code rely on these sorts being *stable* sorts + if inputs: + self._inputs.sort(key = lambda i: (i.prevout.txid, i.prevout.out_idx)) + if outputs: + self._outputs.sort(key = lambda o: (o.value, o.scriptpubkey)) + self.invalidate_ser_cache() + + def input_value(self) -> int: + input_values = [txin.value_sats() for txin in self.inputs()] + if any([val is None for val in input_values]): + raise MissingTxInputAmount() + return sum(input_values) + + def output_value(self) -> int: + return sum(o.value for o in self.outputs()) + + def get_fee(self) -> Optional[int]: + try: + return self.input_value() - self.output_value() + except MissingTxInputAmount: + return None + + def serialize_preimage(self, txin_index: int, *, + bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: + nVersion = int_to_hex(self.version, 4) + nLocktime = int_to_hex(self.locktime, 4) + inputs = self.inputs() + outputs = self.outputs() + txin = inputs[txin_index] + sighash = txin.sighash if txin.sighash is not None else SIGHASH_ALL + if sighash != SIGHASH_ALL: + raise Exception("only SIGHASH_ALL signing is supported!") + nHashType = int_to_hex(sighash, 4) + preimage_script = self.get_preimage_script(txin) + if self.is_segwit_input(txin): + if bip143_shared_txdigest_fields is None: + bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() + hashPrevouts = bip143_shared_txdigest_fields.hashPrevouts + hashSequence = bip143_shared_txdigest_fields.hashSequence + hashOutputs = bip143_shared_txdigest_fields.hashOutputs + outpoint = txin.prevout.serialize_to_network().hex() + scriptCode = var_int(len(preimage_script) // 2) + preimage_script + amount = int_to_hex(txin.value_sats(), 8) + nSequence = int_to_hex(txin.nsequence, 4) + preimage = nVersion + hashPrevouts + hashSequence + outpoint + scriptCode + amount + nSequence + hashOutputs + nLocktime + nHashType + else: + txins = var_int(len(inputs)) + ''.join(self.serialize_input(txin, preimage_script if txin_index==k else '') + for k, txin in enumerate(inputs)) + txouts = var_int(len(outputs)) + ''.join(o.serialize_to_network().hex() for o in outputs) + preimage = nVersion + txins + txouts + nLocktime + nHashType + return preimage + + def sign(self, keypairs) -> None: + # keypairs: pubkey_hex -> (secret_bytes, is_compressed) + bip143_shared_txdigest_fields = self._calc_bip143_shared_txdigest_fields() + for i, txin in enumerate(self.inputs()): + pubkeys = [pk.hex() for pk in txin.pubkeys] + for pubkey in pubkeys: + if txin.is_complete(): + break + if pubkey not in keypairs: + continue + _logger.info(f"adding signature for {pubkey}") + sec, compressed = keypairs[pubkey] + sig = self.sign_txin(i, sec, bip143_shared_txdigest_fields=bip143_shared_txdigest_fields) + self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey, sig=sig) + + _logger.debug(f"is_complete {self.is_complete()}") + self.invalidate_ser_cache() + + def sign_txin(self, txin_index, privkey_bytes, *, bip143_shared_txdigest_fields=None) -> str: + txin = self.inputs()[txin_index] + txin.validate_data(for_signing=True) + pre_hash = sha256d(bfh(self.serialize_preimage(txin_index, + bip143_shared_txdigest_fields=bip143_shared_txdigest_fields))) + privkey = ecc.ECPrivkey(privkey_bytes) + sig = privkey.sign_transaction(pre_hash) + sig = bh2u(sig) + '01' # SIGHASH_ALL + return sig + + def is_complete(self) -> bool: + return all([txin.is_complete() for txin in self.inputs()]) + + def signature_count(self) -> Tuple[int, int]: + s = 0 # "num Sigs we have" + r = 0 # "Required" + for txin in self.inputs(): + if txin.prevout.is_coinbase(): + continue + signatures = list(txin.part_sigs.values()) + s += len(signatures) + r += txin.num_sig + return s, r + + def serialize(self) -> str: + """Returns PSBT as base64 text, or raw hex of network tx (if complete).""" + self.finalize_psbt() + if self.is_complete(): + return Transaction.serialize(self) + return self._serialize_as_base64() + + def serialize_as_bytes(self, *, force_psbt: bool = False) -> bytes: + """Returns PSBT as raw bytes, or raw bytes of network tx (if complete).""" + self.finalize_psbt() + if force_psbt or not self.is_complete(): + with io.BytesIO() as fd: + self._serialize_psbt(fd) + return fd.getvalue() + else: + return Transaction.serialize_as_bytes(self) + + def _serialize_as_base64(self) -> str: + raw_bytes = self.serialize_as_bytes() + return base64.b64encode(raw_bytes).decode('ascii') + + def update_signatures(self, signatures: Sequence[str]): + """Add new signatures to a transaction + + `signatures` is expected to be a list of sigs with signatures[i] + intended for self._inputs[i]. + This is used by the Trezor, KeepKey and Safe-T plugins. + """ + if self.is_complete(): + return + if len(self.inputs()) != len(signatures): + raise Exception('expected {} signatures; got {}'.format(len(self.inputs()), len(signatures))) + for i, txin in enumerate(self.inputs()): + pubkeys = [pk.hex() for pk in txin.pubkeys] + sig = signatures[i] + if bfh(sig) in list(txin.part_sigs.values()): + continue + pre_hash = sha256d(bfh(self.serialize_preimage(i))) + sig_string = ecc.sig_string_from_der_sig(bfh(sig[:-2])) + for recid in range(4): + try: + public_key = ecc.ECPubkey.from_sig_string(sig_string, recid, pre_hash) + except ecc.InvalidECPointException: + # the point might not be on the curve for some recid values + continue + pubkey_hex = public_key.get_public_key_hex(compressed=True) + if pubkey_hex in pubkeys: + try: + public_key.verify_message_hash(sig_string, pre_hash) + except Exception: + _logger.exception('') + continue + _logger.info(f"adding sig: txin_idx={i}, signing_pubkey={pubkey_hex}, sig={sig}") + self.add_signature_to_txin(txin_idx=i, signing_pubkey=pubkey_hex, sig=sig) + break + # redo raw + self.invalidate_ser_cache() + + def add_signature_to_txin(self, *, txin_idx: int, signing_pubkey: str, sig: str): + txin = self._inputs[txin_idx] + txin.part_sigs[bfh(signing_pubkey)] = bfh(sig) + # force re-serialization + txin.script_sig = None + txin.witness = None + self.invalidate_ser_cache() + + def add_info_from_wallet(self, wallet: 'Abstract_Wallet', *, + include_xpubs_and_full_paths: bool = False) -> None: + if self.is_complete(): + return + only_der_suffix = not include_xpubs_and_full_paths + # only include xpubs for multisig wallets; currently only they need it in practice + # note: coldcard fw have a limitation that if they are included then all + # inputs are assumed to be multisig... https://github.com/spesmilo/electrum/pull/5440#issuecomment-549504761 + # note: trezor plugin needs xpubs included, if there are multisig inputs/change_outputs + from .wallet import Multisig_Wallet + if include_xpubs_and_full_paths and isinstance(wallet, Multisig_Wallet): + from .keystore import Xpub + for ks in wallet.get_keystores(): + if isinstance(ks, Xpub): + fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[], + only_der_suffix=only_der_suffix) + xpub = ks.get_xpub_to_be_used_in_partial_tx(only_der_suffix=only_der_suffix) + bip32node = BIP32Node.from_xkey(xpub) + self.xpubs[bip32node] = (fp_bytes, der_full) + for txin in self.inputs(): + wallet.add_input_info(txin, only_der_suffix=only_der_suffix) + for txout in self.outputs(): + wallet.add_output_info(txout, only_der_suffix=only_der_suffix) + + def remove_xpubs_and_bip32_paths(self) -> None: + self.xpubs.clear() + for txin in self.inputs(): + txin.bip32_paths.clear() + for txout in self.outputs(): + txout.bip32_paths.clear() + + def prepare_for_export_for_coinjoin(self) -> None: + """Removes all sensitive details.""" + # globals + self.xpubs.clear() + self._unknown.clear() + # inputs + for txin in self.inputs(): + txin.bip32_paths.clear() + # outputs + for txout in self.outputs(): + txout.redeem_script = None + txout.witness_script = None + txout.bip32_paths.clear() + txout._unknown.clear() + + def convert_all_utxos_to_witness_utxos(self) -> None: + """Replaces all NON-WITNESS-UTXOs with WITNESS-UTXOs. + This will likely make an exported PSBT invalid spec-wise, + but it makes e.g. QR codes significantly smaller. + """ + for txin in self.inputs(): + txin.convert_utxo_to_witness_utxo() + + def is_there_risk_of_burning_coins_as_fees(self) -> bool: + """Returns whether there is risk of burning coins as fees if we sign. + + Note: + - legacy sighash does not commit to any input amounts + - BIP-0143 sighash only commits to the *corresponding* input amount + - BIP-taproot sighash commits to *all* input amounts + """ + for txin in self.inputs(): + # if we have full previous tx, we *know* the input amount + if txin.utxo: + continue + # if we have just the previous output, we only have guarantees if + # the sighash commits to this data + if txin.witness_utxo and Transaction.is_segwit_input(txin): + continue + return True + return False + + def remove_signatures(self): + for txin in self.inputs(): + txin.part_sigs = {} + txin.script_sig = None + txin.witness = None + assert not self.is_complete() + self.invalidate_ser_cache() + + +def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: + if len(xfp) != 4: + raise Exception(f'unexpected xfp length. xfp={xfp}') + return xfp + b''.join(i.to_bytes(4, byteorder='little', signed=False) for i in path) + + +def unpack_bip32_root_fingerprint_and_int_path(path: bytes) -> Tuple[bytes, Sequence[int]]: + if len(path) % 4 != 0: + raise Exception(f'unexpected packed path length. path={path.hex()}') + xfp = path[0:4] + int_path = [int.from_bytes(b, byteorder='little', signed=False) for b in chunks(path[4:], 4)] + return xfp, int_path diff --git a/electrum/util.py b/electrum/util.py @@ -256,9 +256,11 @@ class Fiat(object): class MyEncoder(json.JSONEncoder): def default(self, obj): # note: this does not get called for namedtuples :( https://bugs.python.org/issue30343 - from .transaction import Transaction + from .transaction import Transaction, TxOutput if isinstance(obj, Transaction): - return obj.as_dict() + return obj.serialize() + if isinstance(obj, TxOutput): + return obj.to_legacy_tuple() if isinstance(obj, Satoshis): return str(obj) if isinstance(obj, Fiat): diff --git a/electrum/wallet.py b/electrum/wallet.py @@ -38,7 +38,7 @@ import operator from functools import partial from numbers import Number from decimal import Decimal -from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence, Dict, Any from .i18n import _ from .bip32 import BIP32Node @@ -50,7 +50,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN from .simple_config import SimpleConfig -from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, +from .bitcoin import (COIN, is_address, address_to_script, is_minikey, relayfee, dust_threshold) from .crypto import sha256d from . import keystore @@ -58,7 +58,8 @@ from .keystore import load_keystore, Hardware_KeyStore, KeyStore from .util import multisig_type from .storage import StorageEncryptionVersion, WalletStorage from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 -from .transaction import Transaction, TxOutput, TxOutputHwInfo +from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput, + PartialTransaction, PartialTxInput, PartialTxOutput, TxOutpoint) from .plugin import run_hook from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) @@ -85,36 +86,41 @@ TX_STATUS = [ ] -def append_utxos_to_inputs(inputs, network: 'Network', pubkey, txin_type, imax): - if txin_type != 'p2pk': +def _append_utxos_to_inputs(inputs: List[PartialTxInput], network: 'Network', pubkey, txin_type, imax): + if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): address = bitcoin.pubkey_to_address(txin_type, pubkey) scripthash = bitcoin.address_to_scripthash(address) - else: + elif txin_type == 'p2pk': script = bitcoin.public_key_to_p2pk_script(pubkey) scripthash = bitcoin.script_to_scripthash(script) - address = '(pubkey)' + address = None + else: + raise Exception(f'unexpected txin_type to sweep: {txin_type}') u = network.run_from_another_thread(network.listunspent_for_scripthash(scripthash)) for item in u: if len(inputs) >= imax: break - item['address'] = address - item['type'] = txin_type - item['prevout_hash'] = item['tx_hash'] - item['prevout_n'] = int(item['tx_pos']) - item['pubkeys'] = [pubkey] - item['x_pubkeys'] = [pubkey] - item['signatures'] = [None] - item['num_sig'] = 1 - inputs.append(item) + prevout_str = item['tx_hash'] + ':%d' % item['tx_pos'] + prevout = TxOutpoint.from_str(prevout_str) + utxo = PartialTxInput(prevout=prevout) + utxo._trusted_value_sats = int(item['value']) + utxo._trusted_address = address + utxo.block_height = int(item['height']) + utxo.script_type = txin_type + utxo.pubkeys = [bfh(pubkey)] + utxo.num_sig = 1 + if txin_type == 'p2wpkh-p2sh': + utxo.redeem_script = bfh(bitcoin.p2wpkh_nested_script(pubkey)) + inputs.append(utxo) def sweep_preparations(privkeys, network: 'Network', imax=100): def find_utxos_for_privkey(txin_type, privkey, compressed): pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) - append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax) + _append_utxos_to_inputs(inputs, network, pubkey, txin_type, imax) keypairs[pubkey] = privkey, compressed - inputs = [] + inputs = [] # type: List[PartialTxInput] keypairs = {} for sec in privkeys: txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) @@ -134,24 +140,27 @@ def sweep_preparations(privkeys, network: 'Network', imax=100): return inputs, keypairs -def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100, - *, locktime=None, tx_version=None): +def sweep(privkeys, *, network: 'Network', config: 'SimpleConfig', + to_address: str, fee: int = None, imax=100, + locktime=None, tx_version=None) -> PartialTransaction: inputs, keypairs = sweep_preparations(privkeys, network, imax) - total = sum(i.get('value') for i in inputs) + total = sum(txin.value_sats() for txin in inputs) if fee is None: - outputs = [TxOutput(TYPE_ADDRESS, recipient, total)] - tx = Transaction.from_io(inputs, outputs) + outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)), + value=total)] + tx = PartialTransaction.from_io(inputs, outputs) fee = config.estimate_fee(tx.estimated_size()) if total - fee < 0: raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee)) if total - fee < dust_threshold(network): raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network))) - outputs = [TxOutput(TYPE_ADDRESS, recipient, total - fee)] + outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)), + value=total - fee)] if locktime is None: locktime = get_locktime_for_new_transaction(network) - tx = Transaction.from_io(inputs, outputs, locktime=locktime, version=tx_version) + tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version) tx.set_rbf(True) tx.sign(keypairs) return tx @@ -231,9 +240,13 @@ class Abstract_Wallet(AddressSynchronizer): self.receive_requests = storage.get('payment_requests', {}) self.invoices = storage.get('invoices', {}) # convert invoices + # TODO invoices being these contextual dicts even internally, + # where certain keys are only present depending on values of other keys... + # it's horrible. we need to change this, at least for the internal representation, + # to something that can be typed. for invoice_key, invoice in self.invoices.items(): if invoice.get('type') == PR_TYPE_ONCHAIN: - outputs = [TxOutput(*output) for output in invoice.get('outputs')] + outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')] invoice['outputs'] = outputs self.calc_unused_change_addresses() # save wallet type the first time @@ -272,6 +285,8 @@ class Abstract_Wallet(AddressSynchronizer): def stop_threads(self): super().stop_threads() + if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]): + self.save_keystore() self.storage.write() def set_up_to_date(self, b): @@ -305,7 +320,10 @@ class Abstract_Wallet(AddressSynchronizer): def get_master_public_key(self): return None - def basename(self): + def get_master_public_keys(self): + return [] + + def basename(self) -> str: return os.path.basename(self.storage.path) def test_addresses_sanity(self): @@ -392,15 +410,28 @@ class Abstract_Wallet(AddressSynchronizer): def is_change(self, address) -> bool: if not self.is_mine(address): return False - return self.get_address_index(address)[0] + return self.get_address_index(address)[0] == 1 def get_address_index(self, address): raise NotImplementedError() - def get_redeem_script(self, address): + def get_redeem_script(self, address: str) -> Optional[str]: + txin_type = self.get_txin_type(address) + if txin_type in ('p2pkh', 'p2wpkh', 'p2pk'): + return None + if txin_type == 'p2wpkh-p2sh': + pubkey = self.get_public_key(address) + return bitcoin.p2wpkh_nested_script(pubkey) + raise UnknownTxinType(f'unexpected txin_type {txin_type}') + + def get_witness_script(self, address: str) -> Optional[str]: return None - def export_private_key(self, address, password): + def get_txin_type(self, address: str) -> str: + """Return script type of wallet address.""" + raise NotImplementedError() + + def export_private_key(self, address, password) -> str: if self.is_watching_only(): raise Exception(_("This is a watching-only wallet")) if not is_address(address): @@ -410,19 +441,24 @@ class Abstract_Wallet(AddressSynchronizer): index = self.get_address_index(address) pk, compressed = self.keystore.get_private_key(index, password) txin_type = self.get_txin_type(address) - redeem_script = self.get_redeem_script(address) serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type) - return serialized_privkey, redeem_script + return serialized_privkey def get_public_keys(self, address): return [self.get_public_key(address)] + def get_public_keys_with_deriv_info(self, address: str) -> Dict[str, Tuple[KeyStore, Sequence[int]]]: + """Returns a map: pubkey_hex -> (keystore, derivation_suffix)""" + return {} + def is_found(self): return True #return self.history.values() != [[]] * len(self.history) def get_tx_info(self, tx) -> TxWalletDetails: is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) + if fee is None and isinstance(tx, PartialTransaction): + fee = tx.get_fee() exp_n = None can_broadcast = False can_bump = False @@ -480,7 +516,7 @@ class Abstract_Wallet(AddressSynchronizer): mempool_depth_bytes=exp_n, ) - def get_spendable_coins(self, domain, *, nonlocal_only=False): + def get_spendable_coins(self, domain, *, nonlocal_only=False) -> Sequence[PartialTxInput]: confirmed_only = self.config.get('confirmed_only', False) utxos = self.get_utxos(domain, excluded_addresses=self.frozen_addresses, @@ -490,10 +526,10 @@ class Abstract_Wallet(AddressSynchronizer): utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)] return utxos - def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence: + def get_receiving_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]: raise NotImplementedError() # implemented by subclasses - def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence: + def get_change_addresses(self, *, slice_start=None, slice_stop=None) -> Sequence[str]: raise NotImplementedError() # implemented by subclasses def dummy_address(self): @@ -536,7 +572,7 @@ class Abstract_Wallet(AddressSynchronizer): 'txpos_in_block': hist_item.tx_mined_status.txpos, } - def create_invoice(self, outputs: List[TxOutput], message, pr, URI): + def create_invoice(self, outputs: List[PartialTxOutput], message, pr, URI): if '!' in (x.value for x in outputs): amount = '!' else: @@ -676,9 +712,9 @@ class Abstract_Wallet(AddressSynchronizer): tx_fee = item['fee_sat'] item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None if show_addresses: - item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) - item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)}, - tx.get_outputs_for_UI())) + item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs())) + item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)}, + tx.outputs())) # fixme: use in and out values value = item['bc_value'].value if value < 0: @@ -756,10 +792,10 @@ class Abstract_Wallet(AddressSynchronizer): item['capital_gain'] = Fiat(cg, fx.ccy) return item - def get_label(self, tx_hash): + def get_label(self, tx_hash: str) -> str: return self.labels.get(tx_hash, '') or self.get_default_label(tx_hash) - def get_default_label(self, tx_hash): + def get_default_label(self, tx_hash) -> str: if not self.db.get_txi_addresses(tx_hash): labels = [] for addr in self.db.get_txo_addresses(tx_hash): @@ -876,34 +912,32 @@ class Abstract_Wallet(AddressSynchronizer): max_change = self.max_change_outputs if self.multiple_change else 1 return change_addrs[:max_change] - def make_unsigned_transaction(self, coins, outputs, fixed_fee=None, - change_addr=None, is_sweep=False): + def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput], + outputs: List[PartialTxOutput], fee=None, + change_addr: str = None, is_sweep=False) -> PartialTransaction: # check outputs i_max = None for i, o in enumerate(outputs): - if o.type == TYPE_ADDRESS: - if not is_address(o.address): - raise Exception("Invalid bitcoin address: {}".format(o.address)) if o.value == '!': if i_max is not None: raise Exception("More than one output set to spend max") i_max = i - if fixed_fee is None and self.config.fee_per_kb() is None: + if fee is None and self.config.fee_per_kb() is None: raise NoDynamicFeeEstimates() for item in coins: self.add_input_info(item) # Fee estimator - if fixed_fee is None: + if fee is None: fee_estimator = self.config.estimate_fee - elif isinstance(fixed_fee, Number): - fee_estimator = lambda size: fixed_fee - elif callable(fixed_fee): - fee_estimator = fixed_fee + elif isinstance(fee, Number): + fee_estimator = lambda size: fee + elif callable(fee): + fee_estimator = fee else: - raise Exception('Invalid argument fixed_fee: %s' % fixed_fee) + raise Exception(f'Invalid argument fee: {fee}') if i_max is None: # Let the coin chooser select the coins to spend @@ -912,12 +946,10 @@ class Abstract_Wallet(AddressSynchronizer): base_tx = self.get_unconfirmed_base_tx_for_batching() if self.config.get('batch_rbf', False) and base_tx: # make sure we don't try to spend change from the tx-to-be-replaced: - coins = [c for c in coins if c['prevout_hash'] != base_tx.txid()] + coins = [c for c in coins if c.prevout.txid.hex() != base_tx.txid()] is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL - base_tx = Transaction(base_tx.serialize()) - base_tx.deserialize(force_full_parse=True) - base_tx.remove_signatures() - base_tx.add_inputs_info(self) + base_tx = PartialTransaction.from_tx(base_tx) + base_tx.add_info_from_wallet(self) base_tx_fee = base_tx.get_fee() relayfeerate = Decimal(self.relayfee()) / 1000 original_fee_estimator = fee_estimator @@ -935,8 +967,12 @@ class Abstract_Wallet(AddressSynchronizer): old_change_addrs = [] # change address. if empty, coin_chooser will set it change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs) - tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs, - fee_estimator, self.dust_threshold()) + tx = coin_chooser.make_tx(coins=coins, + inputs=txi, + outputs=list(outputs) + txo, + change_addrs=change_addrs, + fee_estimator_vb=fee_estimator, + dust_threshold=self.dust_threshold()) else: # "spend max" branch # note: This *will* spend inputs with negative effective value (if there are any). @@ -945,38 +981,43 @@ class Abstract_Wallet(AddressSynchronizer): # forever. see #5433 # note: Actually it might be the case that not all UTXOs from the wallet are # being spent if the user manually selected UTXOs. - sendable = sum(map(lambda x:x['value'], coins)) - outputs[i_max] = outputs[i_max]._replace(value=0) - tx = Transaction.from_io(coins, outputs[:]) + sendable = sum(map(lambda c: c.value_sats(), coins)) + outputs[i_max].value = 0 + tx = PartialTransaction.from_io(list(coins), list(outputs)) fee = fee_estimator(tx.estimated_size()) amount = sendable - tx.output_value() - fee if amount < 0: raise NotEnoughFunds() - outputs[i_max] = outputs[i_max]._replace(value=amount) - tx = Transaction.from_io(coins, outputs[:]) + outputs[i_max].value = amount + tx = PartialTransaction.from_io(list(coins), list(outputs)) # Timelock tx to current height. tx.locktime = get_locktime_for_new_transaction(self.network) + + tx.add_info_from_wallet(self) run_hook('make_unsigned_transaction', self, tx) return tx - def mktx(self, outputs, password, fee=None, change_addr=None, - domain=None, rbf=False, nonlocal_only=False, *, tx_version=None): + def mktx(self, *, outputs: List[PartialTxOutput], password=None, fee=None, change_addr=None, + domain=None, rbf=False, nonlocal_only=False, tx_version=None, sign=True) -> PartialTransaction: coins = self.get_spendable_coins(domain, nonlocal_only=nonlocal_only) - tx = self.make_unsigned_transaction(coins, outputs, fee, change_addr) + tx = self.make_unsigned_transaction(coins=coins, + outputs=outputs, + fee=fee, + change_addr=change_addr) tx.set_rbf(rbf) if tx_version is not None: tx.version = tx_version - self.sign_transaction(tx, password) + if sign: + self.sign_transaction(tx, password) return tx def is_frozen_address(self, addr: str) -> bool: return addr in self.frozen_addresses - def is_frozen_coin(self, utxo) -> bool: - # utxo is either a txid:vout str, or a dict - utxo = self._utxo_str_from_utxo(utxo) - return utxo in self.frozen_coins + def is_frozen_coin(self, utxo: PartialTxInput) -> bool: + prevout_str = utxo.prevout.to_str() + return prevout_str in self.frozen_coins def set_frozen_state_of_addresses(self, addrs, freeze: bool): """Set frozen state of the addresses to FREEZE, True or False""" @@ -990,9 +1031,9 @@ class Abstract_Wallet(AddressSynchronizer): return True return False - def set_frozen_state_of_coins(self, utxos, freeze: bool): + def set_frozen_state_of_coins(self, utxos: Sequence[PartialTxInput], freeze: bool): """Set frozen state of the utxos to FREEZE, True or False""" - utxos = {self._utxo_str_from_utxo(utxo) for utxo in utxos} + utxos = {utxo.prevout.to_str() for utxo in utxos} # FIXME take lock? if freeze: self.frozen_coins |= set(utxos) @@ -1000,15 +1041,6 @@ class Abstract_Wallet(AddressSynchronizer): self.frozen_coins -= set(utxos) self.storage.put('frozen_coins', list(self.frozen_coins)) - @staticmethod - def _utxo_str_from_utxo(utxo: Union[dict, str]) -> str: - """Return a txid:vout str""" - if isinstance(utxo, dict): - return "{}:{}".format(utxo['prevout_hash'], utxo['prevout_n']) - assert isinstance(utxo, str), f"utxo should be a str, not {type(utxo)}" - # just assume it is already of the correct format - return utxo - def wait_until_synchronized(self, callback=None): def wait_for_wallet(): self.set_up_to_date(False) @@ -1055,7 +1087,7 @@ class Abstract_Wallet(AddressSynchronizer): max_conf = max(max_conf, tx_age) return max_conf >= req_conf - def bump_fee(self, *, tx: Transaction, new_fee_rate) -> Transaction: + def bump_fee(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction: """Increase the miner fee of 'tx'. 'new_fee_rate' is the target min rate in sat/vbyte """ @@ -1097,13 +1129,11 @@ class Abstract_Wallet(AddressSynchronizer): tx_new.locktime = get_locktime_for_new_transaction(self.network) return tx_new - def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> Transaction: - tx = Transaction(tx.serialize()) - tx.deserialize(force_full_parse=True) # need to parse inputs - tx.remove_signatures() - tx.add_inputs_info(self) - old_inputs = tx.inputs()[:] - old_outputs = tx.outputs()[:] + def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction: + tx = PartialTransaction.from_tx(tx) + tx.add_info_from_wallet(self) + old_inputs = list(tx.inputs()) + old_outputs = list(tx.outputs()) # change address old_change_addrs = [o.address for o in old_outputs if self.is_change(o.address)] change_addrs = self.get_change_addresses_for_new_transaction(old_change_addrs) @@ -1131,18 +1161,20 @@ class Abstract_Wallet(AddressSynchronizer): return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size) coin_chooser = coinchooser.get_coin_chooser(self.config) try: - return coin_chooser.make_tx(coins, old_inputs, fixed_outputs, change_addrs, - fee_estimator, self.dust_threshold()) + return coin_chooser.make_tx(coins=coins, + inputs=old_inputs, + outputs=fixed_outputs, + change_addrs=change_addrs, + fee_estimator_vb=fee_estimator, + dust_threshold=self.dust_threshold()) except NotEnoughFunds as e: raise CannotBumpFee(e) - def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> Transaction: - tx = Transaction(tx.serialize()) - tx.deserialize(force_full_parse=True) # need to parse inputs - tx.remove_signatures() - tx.add_inputs_info(self) + def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction: + tx = PartialTransaction.from_tx(tx) + tx.add_info_from_wallet(self) inputs = tx.inputs() - outputs = tx.outputs() + outputs = list(tx.outputs()) # use own outputs s = list(filter(lambda o: self.is_mine(o.address), outputs)) @@ -1165,7 +1197,7 @@ class Abstract_Wallet(AddressSynchronizer): if o.value - delta >= self.dust_threshold(): new_output_value = o.value - delta assert isinstance(new_output_value, int) - outputs[i] = o._replace(value=new_output_value) + outputs[i].value = new_output_value delta = 0 break else: @@ -1176,48 +1208,92 @@ class Abstract_Wallet(AddressSynchronizer): if delta > 0: raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) - return Transaction.from_io(inputs, outputs) + return PartialTransaction.from_io(inputs, outputs) - def cpfp(self, tx: Transaction, fee: int) -> Optional[Transaction]: + def cpfp(self, tx: Transaction, fee: int) -> Optional[PartialTransaction]: txid = tx.txid() for i, o in enumerate(tx.outputs()): address, value = o.address, o.value - if o.type == TYPE_ADDRESS and self.is_mine(address): + if self.is_mine(address): break else: return coins = self.get_addr_utxo(address) - item = coins.get(txid+':%d'%i) + item = coins.get(TxOutpoint.from_str(txid+':%d'%i)) if not item: return self.add_input_info(item) inputs = [item] out_address = self.get_unused_address() or address - outputs = [TxOutput(TYPE_ADDRESS, out_address, value - fee)] + outputs = [PartialTxOutput.from_address_and_value(out_address, value - fee)] locktime = get_locktime_for_new_transaction(self.network) - return Transaction.from_io(inputs, outputs, locktime=locktime) + return PartialTransaction.from_io(inputs, outputs, locktime=locktime) - def add_input_sig_info(self, txin, address): + def _add_input_sig_info(self, txin: PartialTxInput, address: str, *, only_der_suffix: bool = True) -> None: raise NotImplementedError() # implemented by subclasses - def add_input_info(self, txin): - address = self.get_txin_address(txin) - if self.is_mine(address): - txin['address'] = address - txin['type'] = self.get_txin_type(address) - # segwit needs value to sign - if txin.get('value') is None: + def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput], + address: str, *, only_der_suffix: bool = True) -> None: + pass # implemented by subclasses + + def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None: + if Transaction.is_segwit_input(txin): + if txin.witness_utxo is None: received, spent = self.get_addr_io(address) - item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n']) + item = received.get(txin.prevout.to_str()) if item: - txin['value'] = item[1] - self.add_input_sig_info(txin, address) + txin_value = item[1] + txin.witness_utxo = TxOutput.from_address_and_value(address, txin_value) + else: # legacy input + if txin.utxo is None: + # note: for hw wallets, for legacy inputs, ignore_network_issues used to be False + txin.utxo = self.get_input_tx(txin.prevout.txid.hex(), ignore_network_issues=True) + # If there is a NON-WITNESS UTXO, but we know input is segwit, add a WITNESS UTXO, based on it. + # This could have happened if previously another wallet had put a NON-WITNESS UTXO for txin, + # as they did not know if it was segwit. This switch is needed to interop with bitcoin core. + if txin.utxo and Transaction.is_segwit_input(txin): + txin.convert_utxo_to_witness_utxo() + txin.ensure_there_is_only_one_utxo() + + def _learn_derivation_path_for_address_from_txinout(self, txinout: Union[PartialTxInput, PartialTxOutput], + address: str) -> bool: + """Tries to learn the derivation path for an address (potentially beyond gap limit) + using data available in given txin/txout. + Returns whether the address was found to be is_mine. + """ + return False # implemented by subclasses + + def add_input_info(self, txin: PartialTxInput, *, only_der_suffix: bool = True) -> None: + address = self.get_txin_address(txin) + if not self.is_mine(address): + is_mine = self._learn_derivation_path_for_address_from_txinout(txin, address) + if not is_mine: + return + # set script_type first, as later checks might rely on it: + txin.script_type = self.get_txin_type(address) + self._add_input_utxo_info(txin, address) + txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1 + if txin.redeem_script is None: + try: + redeem_script_hex = self.get_redeem_script(address) + txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None + except UnknownTxinType: + pass + if txin.witness_script is None: + try: + witness_script_hex = self.get_witness_script(address) + txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None + except UnknownTxinType: + pass + self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix) def can_sign(self, tx: Transaction) -> bool: + if not isinstance(tx, PartialTransaction): + return False if tx.is_complete(): return False # add info to inputs if we can; otherwise we might return a false negative: - tx.add_inputs_info(self) + tx.add_info_from_wallet(self) for k in self.get_keystores(): if k.can_sign(tx): return True @@ -1241,45 +1317,51 @@ class Abstract_Wallet(AddressSynchronizer): tx = Transaction(raw_tx) return tx - def add_hw_info(self, tx: Transaction) -> None: - # add previous tx for hw wallets - for txin in tx.inputs(): - tx_hash = txin['prevout_hash'] - # segwit inputs might not be needed for some hw wallets - ignore_network_issues = Transaction.is_segwit_input(txin) - txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_network_issues=ignore_network_issues) - # add output info for hw wallets - info = {} - xpubs = self.get_master_public_keys() - for o in tx.outputs(): - if self.is_mine(o.address): - index = self.get_address_index(o.address) - pubkeys = self.get_public_keys(o.address) - # sort xpubs using the order of pubkeys - sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) - num_sig = self.m if isinstance(self, Multisig_Wallet) else None - is_change = self.is_change(o.address) - info[o.address] = TxOutputHwInfo(address_index=index, - sorted_xpubs=sorted_xpubs, - num_sig=num_sig, - script_type=self.txin_type, - is_change=is_change) - tx.output_info = info - - def sign_transaction(self, tx, password): + def add_output_info(self, txout: PartialTxOutput, *, only_der_suffix: bool = True) -> None: + address = txout.address + if not self.is_mine(address): + is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address) + if not is_mine: + return + txout.script_type = self.get_txin_type(address) + txout.is_mine = True + txout.is_change = self.is_change(address) + if isinstance(self, Multisig_Wallet): + txout.num_sig = self.m + self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) + if txout.redeem_script is None: + try: + redeem_script_hex = self.get_redeem_script(address) + txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None + except UnknownTxinType: + pass + if txout.witness_script is None: + try: + witness_script_hex = self.get_witness_script(address) + txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None + except UnknownTxinType: + pass + + def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: if self.is_watching_only(): return - tx.add_inputs_info(self) - # hardware wallets require extra info - if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]): - self.add_hw_info(tx) + if not isinstance(tx, PartialTransaction): + return + # add info to a temporary tx copy; including xpubs + # and full derivation paths as hw keystores might want them + tmp_tx = copy.deepcopy(tx) + tmp_tx.add_info_from_wallet(self, include_xpubs_and_full_paths=True) # sign. start with ready keystores. for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): try: - if k.can_sign(tx): - k.sign_transaction(tx, password) + if k.can_sign(tmp_tx): + k.sign_transaction(tmp_tx, password) except UserCancelled: continue + # remove sensitive info; then copy back details from temporary tx + tmp_tx.remove_xpubs_and_bip32_paths() + tx.combine_with_other_psbt(tmp_tx) + tx.add_info_from_wallet(self, include_xpubs_and_full_paths=False) return tx def try_detecting_internal_addresses_corruption(self): @@ -1423,7 +1505,6 @@ class Abstract_Wallet(AddressSynchronizer): self.network.trigger_callback('payment_received', self, addr, status) def make_payment_request(self, addr, amount, message, expiration): - from .bitcoin import TYPE_ADDRESS timestamp = int(time.time()) _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10] return { @@ -1434,12 +1515,12 @@ class Abstract_Wallet(AddressSynchronizer): 'address':addr, 'memo':message, 'id':_id, - 'outputs': [(TYPE_ADDRESS, addr, amount)] + 'outputs': [PartialTxOutput.from_address_and_value(addr, amount)], } def sign_payment_request(self, key, alias, alias_addr, password): req = self.receive_requests.get(key) - alias_privkey = self.export_private_key(alias_addr, password)[0] + alias_privkey = self.export_private_key(alias_addr, password) pr = paymentrequest.make_unsigned_request(req) paymentrequest.sign_request_with_alias(pr, alias, alias_privkey) req['name'] = pr.pki_data @@ -1577,9 +1658,12 @@ class Abstract_Wallet(AddressSynchronizer): index = self.get_address_index(addr) return self.keystore.decrypt_message(index, message, password) - def txin_value(self, txin): - txid = txin['prevout_hash'] - prev_n = txin['prevout_n'] + def txin_value(self, txin: TxInput) -> Optional[int]: + if isinstance(txin, PartialTxInput): + v = txin.value_sats() + if v: return v + txid = txin.prevout.txid.hex() + prev_n = txin.prevout.out_idx for addr in self.db.get_txo_addresses(txid): d = self.db.get_txo_addr(txid, addr) for n, v, cb in d: @@ -1597,8 +1681,8 @@ class Abstract_Wallet(AddressSynchronizer): coins = self.get_utxos(domain) now = time.time() p = price_func(now) - ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins) - lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN) + ap = sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.txin_value(coin)) for coin in coins) + lp = sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN) return lp - ap def average_price(self, txid, price_func, ccy): @@ -1651,6 +1735,9 @@ class Abstract_Wallet(AddressSynchronizer): def get_keystores(self) -> Sequence[KeyStore]: return [self.keystore] if self.keystore else [] + def save_keystore(self): + raise NotImplementedError() + class Simple_Wallet(Abstract_Wallet): # wallet with a single keystore @@ -1684,9 +1771,6 @@ class Imported_Wallet(Simple_Wallet): def load_keystore(self): self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None - # fixme: a reference to addresses is needed - if self.keystore: - self.keystore.addresses = self.db.imported_addresses def save_keystore(self): self.storage.put('keystore', self.keystore.dump()) @@ -1706,9 +1790,6 @@ class Imported_Wallet(Simple_Wallet): def is_change(self, address): return False - def get_master_public_keys(self): - return [] - def is_beyond_limit(self, address): return False @@ -1795,11 +1876,11 @@ class Imported_Wallet(Simple_Wallet): def is_mine(self, address) -> bool: return self.db.has_imported_address(address) - def get_address_index(self, address): + def get_address_index(self, address) -> Optional[str]: # returns None if address is not mine return self.get_public_key(address) - def get_public_key(self, address): + def get_public_key(self, address) -> Optional[str]: x = self.db.get_imported_address(address) return x.get('pubkey') if x else None @@ -1818,7 +1899,7 @@ class Imported_Wallet(Simple_Wallet): continue addr = bitcoin.pubkey_to_address(txin_type, pubkey) good_addr.append(addr) - self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey, 'redeem_script':None}) + self.db.add_imported_address(addr, {'type':txin_type, 'pubkey':pubkey}) self.add_address(addr) self.save_keystore() if write_to_disk: @@ -1832,27 +1913,22 @@ class Imported_Wallet(Simple_Wallet): else: raise BitcoinException(str(bad_keys[0][1])) - def get_redeem_script(self, address): - d = self.db.get_imported_address(address) - redeem_script = d['redeem_script'] - return redeem_script - def get_txin_type(self, address): return self.db.get_imported_address(address).get('type', 'address') - def add_input_sig_info(self, txin, address): - if self.is_watching_only(): - x_pubkey = 'fd' + address_to_script(address) - txin['x_pubkeys'] = [x_pubkey] - txin['signatures'] = [None] + def _add_input_sig_info(self, txin, address, *, only_der_suffix=True): + if not self.is_mine(address): return - if txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: - pubkey = self.db.get_imported_address(address)['pubkey'] - txin['num_sig'] = 1 - txin['x_pubkeys'] = [pubkey] - txin['signatures'] = [None] + if txin.script_type in ('unknown', 'address'): + return + elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): + pubkey = self.get_public_key(address) + if not pubkey: + return + txin.pubkeys = [bfh(pubkey)] else: - raise NotImplementedError('imported wallets for p2sh are not implemented') + raise Exception(f'Unexpected script type: {txin.script_type}. ' + f'Imported wallets are not implemented to handle this.') def pubkeys_to_address(self, pubkey): for addr in self.db.get_imported_addresses(): @@ -1862,6 +1938,7 @@ class Imported_Wallet(Simple_Wallet): class Deterministic_Wallet(Abstract_Wallet): def __init__(self, storage, *, config): + self._ephemeral_addr_to_addr_index = {} # type: Dict[str, Sequence[int]] Abstract_Wallet.__init__(self, storage, config=config) self.gap_limit = storage.get('gap_limit', 20) # generate addresses now. note that without libsecp this might block @@ -1945,6 +2022,26 @@ class Deterministic_Wallet(Abstract_Wallet): x = self.derive_pubkeys(for_change, n) return self.pubkeys_to_address(x) + def get_public_keys_with_deriv_info(self, address: str): + der_suffix = self.get_address_index(address) + der_suffix = [int(x) for x in der_suffix] + return {k.derive_pubkey(*der_suffix): (k, der_suffix) + for k in self.get_keystores()} + + def _add_input_sig_info(self, txin, address, *, only_der_suffix=True): + self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) + + def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix=True): + if not self.is_mine(address): + return + pubkey_deriv_info = self.get_public_keys_with_deriv_info(address) + txinout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)]) + for pubkey_hex in pubkey_deriv_info: + ks, der_suffix = pubkey_deriv_info[pubkey_hex] + fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, + only_der_suffix=only_der_suffix) + txinout.bip32_paths[bfh(pubkey_hex)] = (fp_bytes, der_full) + def create_new_address(self, for_change=False): assert type(for_change) is bool with self.lock: @@ -1995,8 +2092,16 @@ class Deterministic_Wallet(Abstract_Wallet): return False return True - def get_address_index(self, address): - return self.db.get_address_index(address) + def get_address_index(self, address) -> Optional[Sequence[int]]: + return self.db.get_address_index(address) or self._ephemeral_addr_to_addr_index.get(address) + + def _learn_derivation_path_for_address_from_txinout(self, txinout, address): + for ks in self.get_keystores(): + pubkey, der_suffix = ks.find_my_pubkey_in_txinout(txinout, only_der_suffix=True) + if der_suffix is not None: + self._ephemeral_addr_to_addr_index[address] = list(der_suffix) + return True + return False def get_master_public_keys(self): return [self.get_master_public_key()] @@ -2017,7 +2122,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): def get_public_key(self, address): sequence = self.get_address_index(address) - pubkey = self.get_pubkey(*sequence) + pubkey = self.derive_pubkeys(*sequence) return pubkey def load_keystore(self): @@ -2028,16 +2133,6 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet): xtype = 'standard' self.txin_type = 'p2pkh' if xtype == 'standard' else xtype - def get_pubkey(self, c, i): - return self.derive_pubkeys(c, i) - - def add_input_sig_info(self, txin, address): - derivation = self.get_address_index(address) - x_pubkey = self.keystore.get_xpubkey(*derivation) - txin['x_pubkeys'] = [x_pubkey] - txin['signatures'] = [None] - txin['num_sig'] = 1 - def get_master_public_key(self): return self.keystore.get_master_public_key() @@ -2065,24 +2160,37 @@ class Multisig_Wallet(Deterministic_Wallet): self.m, self.n = multisig_type(self.wallet_type) Deterministic_Wallet.__init__(self, storage, config=config) - def get_pubkeys(self, c, i): - return self.derive_pubkeys(c, i) - def get_public_keys(self, address): - sequence = self.get_address_index(address) - return self.get_pubkeys(*sequence) + return list(self.get_public_keys_with_deriv_info(address)) def pubkeys_to_address(self, pubkeys): - redeem_script = self.pubkeys_to_redeem_script(pubkeys) + redeem_script = self.pubkeys_to_scriptcode(pubkeys) return bitcoin.redeem_script_to_address(self.txin_type, redeem_script) - def pubkeys_to_redeem_script(self, pubkeys): + def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: return transaction.multisig_script(sorted(pubkeys), self.m) def get_redeem_script(self, address): + txin_type = self.get_txin_type(address) pubkeys = self.get_public_keys(address) - redeem_script = self.pubkeys_to_redeem_script(pubkeys) - return redeem_script + scriptcode = self.pubkeys_to_scriptcode(pubkeys) + if txin_type == 'p2sh': + return scriptcode + elif txin_type == 'p2wsh-p2sh': + return bitcoin.p2wsh_nested_script(scriptcode) + elif txin_type == 'p2wsh': + return None + raise UnknownTxinType(f'unexpected txin_type {txin_type}') + + def get_witness_script(self, address): + txin_type = self.get_txin_type(address) + pubkeys = self.get_public_keys(address) + scriptcode = self.pubkeys_to_scriptcode(pubkeys) + if txin_type == 'p2sh': + return None + elif txin_type in ('p2wsh-p2sh', 'p2wsh'): + return scriptcode + raise UnknownTxinType(f'unexpected txin_type {txin_type}') def derive_pubkeys(self, c, i): return [k.derive_pubkey(c, i) for k in self.get_keystores()] @@ -2140,23 +2248,6 @@ class Multisig_Wallet(Deterministic_Wallet): def get_fingerprint(self): return ''.join(sorted(self.get_master_public_keys())) - def add_input_sig_info(self, txin, address): - # x_pubkeys are not sorted here because it would be too slow - # they are sorted in transaction.get_sorted_pubkeys - # pubkeys is set to None to signal that x_pubkeys are unsorted - derivation = self.get_address_index(address) - x_pubkeys_expected = [k.get_xpubkey(*derivation) for k in self.get_keystores()] - x_pubkeys_actual = txin.get('x_pubkeys') - # if 'x_pubkeys' is already set correctly (ignoring order, as above), leave it. - # otherwise we might delete signatures - if x_pubkeys_actual and set(x_pubkeys_actual) == set(x_pubkeys_expected): - return - txin['x_pubkeys'] = x_pubkeys_expected - txin['pubkeys'] = None - # we need n place holders - txin['signatures'] = [None] * self.n - txin['num_sig'] = self.m - wallet_types = ['standard', 'multisig', 'imported']