Browse Source

integrate PSBT support natively. WIP

patch-1
SomberNight 5 years ago
parent
commit
bafe8a2fff
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 92
      electrum/address_synchronizer.py
  2. 7
      electrum/base_wizard.py
  3. 33
      electrum/bip32.py
  4. 23
      electrum/bitcoin.py
  5. 81
      electrum/coinchooser.py
  6. 97
      electrum/commands.py
  7. 10
      electrum/ecc.py
  8. 9
      electrum/gui/kivy/main_window.py
  9. 11
      electrum/gui/kivy/uix/dialogs/__init__.py
  10. 14
      electrum/gui/kivy/uix/dialogs/tx_dialog.py
  11. 12
      electrum/gui/kivy/uix/screens.py
  12. 12
      electrum/gui/qt/address_dialog.py
  13. 99
      electrum/gui/qt/main_window.py
  14. 40
      electrum/gui/qt/paytoedit.py
  15. 84
      electrum/gui/qt/transaction_dialog.py
  16. 9
      electrum/gui/qt/util.py
  17. 48
      electrum/gui/qt/utxo_list.py
  18. 9
      electrum/gui/stdio.py
  19. 9
      electrum/gui/text.py
  20. 61
      electrum/json_db.py
  21. 280
      electrum/keystore.py
  22. 48
      electrum/lnchannel.py
  23. 28
      electrum/lnpeer.py
  24. 113
      electrum/lnsweep.py
  25. 105
      electrum/lnutil.py
  26. 6
      electrum/lnwatcher.py
  27. 2
      electrum/lnworker.py
  28. 5
      electrum/network.py
  29. 23
      electrum/paymentrequest.py
  30. 2
      electrum/plugin.py
  31. 8
      electrum/plugins/audio_modem/qt.py
  32. 313
      electrum/plugins/coldcard/basic_psbt.py
  33. 397
      electrum/plugins/coldcard/build_psbt.py
  34. 166
      electrum/plugins/coldcard/coldcard.py
  35. 131
      electrum/plugins/coldcard/qt.py
  36. 48
      electrum/plugins/cosigner_pool/qt.py
  37. 101
      electrum/plugins/digitalbitbox/digitalbitbox.py
  38. 12
      electrum/plugins/digitalbitbox/qt.py
  39. 9
      electrum/plugins/greenaddress_instant/qt.py
  40. 40
      electrum/plugins/hw_wallet/plugin.py
  41. 218
      electrum/plugins/keepkey/keepkey.py
  42. 106
      electrum/plugins/ledger/ledger.py
  43. 218
      electrum/plugins/safe_t/safe_t.py
  44. 149
      electrum/plugins/trezor/trezor.py
  45. 2
      electrum/plugins/trustedcoin/cmdline.py
  46. 106
      electrum/plugins/trustedcoin/legacy_tx_format.py
  47. 37
      electrum/plugins/trustedcoin/trustedcoin.py
  48. 3
      electrum/scripts/bip70.py
  49. 2
      electrum/segwit_addr.py
  50. 6
      electrum/synchronizer.py
  51. 6
      electrum/tests/regtest/regtest.sh
  52. 21
      electrum/tests/test_commands.py
  53. 2
      electrum/tests/test_lnchannel.py
  54. 11
      electrum/tests/test_lnutil.py
  55. 267
      electrum/tests/test_psbt.py
  56. 253
      electrum/tests/test_transaction.py
  57. 2
      electrum/tests/test_wallet.py
  58. 275
      electrum/tests/test_wallet_vertical.py
  59. 1959
      electrum/transaction.py
  60. 6
      electrum/util.py
  61. 507
      electrum/wallet.py

92
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
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,9 +643,9 @@ 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:
@ -736,23 +730,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 +789,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 +800,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

7
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()
@ -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,
}

33
electrum/bip32.py

@ -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

23
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,8 @@ 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"
if i<0xfd:
return int_to_hex(i)
elif i<=0xffff:
@ -372,24 +375,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

81
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)

97
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,

10
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)

9
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
@ -855,7 +854,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 +865,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 +1198,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")

11
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

14
electrum/gui/kivy/uix/dialogs/tx_dialog.py

@ -1,5 +1,5 @@
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
@ -17,6 +17,10 @@ from electrum.util import InvalidPassword
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.wallet import CannotBumpFee
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
from electrum.transaction import Transaction
Builder.load_string('''
@ -121,9 +125,9 @@ 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
def on_open(self):
@ -166,7 +170,7 @@ class TxDialog(Factory.Popup):
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,7 +256,7 @@ class TxDialog(Factory.Popup):
def show_qr(self):
from electrum.bitcoin import base_encode, bfh
raw_tx = str(self.tx)
raw_tx = self.tx.serialize()
text = bfh(raw_tx)
text = base_encode(text, base=43)
self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx)

12
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

12
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)

99
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, txt: 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(txt)
except BaseException as e:
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
return
@ -2752,14 +2756,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
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 +2836,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 +3135,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 +3262,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):

40
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[:]

84
electrum/gui/qt/transaction_dialog.py

@ -26,8 +26,8 @@
import sys
import copy
import datetime
import json
import traceback
import time
from typing import TYPE_CHECKING
from PyQt5.QtCore import QSize, Qt
@ -42,11 +42,11 @@ 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)
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog)
if TYPE_CHECKING:
from .main_window import ElectrumWindow
@ -60,9 +60,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 +73,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.
'''
@ -97,7 +97,7 @@ 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)
tx.add_info_from_wallet(self.wallet)
self.setMinimumWidth(950)
self.setWindowTitle(_("Transaction"))
@ -123,6 +123,9 @@ class TxDialog(QDialog, MessageBoxMixin):
self.broadcast_button = b = QPushButton(_("Broadcast"))
b.clicked.connect(self.do_broadcast)
self.merge_sigs_button = b = QPushButton(_("Merge sigs from"))
b.clicked.connect(self.merge_sigs)
self.save_button = b = QPushButton(_("Save"))
save_button_disabled = not tx.is_complete()
b.setDisabled(save_button_disabled)
@ -152,7 +155,10 @@ class TxDialog(QDialog, MessageBoxMixin):
self.export_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.merge_sigs_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]
@ -190,7 +196,7 @@ class TxDialog(QDialog, MessageBoxMixin):
self.close()
def show_qr(self):
text = bfh(str(self.tx))
text = self.tx.serialize_as_bytes()
text = base_encode(text, base=43)
try:
self.main_window.show_qrcode(text, 'Transaction', parent=self)
@ -222,15 +228,43 @@ 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:
if isinstance(self.tx, PartialTransaction):
self.tx.finalize_psbt()
if self.tx.is_complete():
name = 'signed_%s.txn' % (self.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 self.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 = self.tx.serialize_to_network()
f.write(network_tx_hex + '\n')
else: # if partial: PSBT bytes
assert isinstance(self.tx, PartialTransaction)
with open(fileName, "wb+") as f:
f.write(self.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:"), _("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 update(self):
desc = self.desc
@ -319,19 +353,19 @@ class TxDialog(QDialog, MessageBoxMixin):
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)
@ -340,8 +374,8 @@ class TxDialog(QDialog, MessageBoxMixin):
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)

9
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

48
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):

9
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

9
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

61
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,34 @@ 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
if not self._is_upgrade_method_needed(19, 19):
return
from .bip32 import BIP32Node
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
# derivation prefix
derivation_prefix = ks.get('derivation', 'm')
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 we don't have prior data, we set it to the fp of the xpub
# EVEN IF there was already a derivation prefix saved different than 'm'
node = BIP32Node.from_xkey(xpub)
root_fingerprint = node.calc_fingerprint_of_this_node().hex().lower()
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 +779,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 +822,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):

280
electrum/keystore.py

@ -26,16 +26,15 @@
from unicodedata import normalize
import hashlib
from typing import Tuple, TYPE_CHECKING, Union, Sequence
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List
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)
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,7 +42,7 @@ 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):
@ -67,25 +66,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 +101,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 +258,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 +274,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 +320,54 @@ 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
# if these are None now, then it is the responsibility of the caller to
# also call self.add_derivation_prefix_and_root_fingerprint:
self._derivation_prefix = derivation_prefix # note: subclass should persist this
self._root_fingerprint = root_fingerprint # note: subclass should persist this
def get_master_public_key(self):
return self.xpub
def derive_pubkey(self, for_change, n):
def get_derivation_prefix(self) -> str:
"""Returns to bip32 path from some root node to self.xpub"""
assert self._derivation_prefix is not None, 'derivation_prefix should have been set already'
return self._derivation_prefix
def get_root_fingerprint(self) -> 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
"""
assert self._root_fingerprint is not None, 'root_fingerprint should have been set already'
return self._root_fingerprint
def add_derivation_prefix_and_root_fingerprint(self, *, derivation_prefix: str, root_node: BIP32Node):
assert self.xpub
derivation_prefix = normalize_bip32_derivation(derivation_prefix)
# 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")
# store
self._root_fingerprint = root_node.calc_fingerprint_of_this_node().hex().lower()
self._derivation_prefix = derivation_prefix
def reset_derivation_prefix(self):
assert self.xpub
self._derivation_prefix = 'm'
self._root_fingerprint = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node().hex().lower()
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 +383,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 +401,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 +431,20 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
def is_watching_only(self):
return self.xprv is None
def add_xprv(self, xprv):
def add_xpub(self, xpub, *, default_der_prefix=True):
self.xpub = xpub
if default_der_prefix:
self.reset_derivation_prefix()
def add_xprv(self, xprv, *, default_der_prefix=True):
self.xprv = xprv
self.xpub = bip32.xpub_from_xprv(xprv)
self.add_xpub(bip32.xpub_from_xprv(xprv), default_der_prefix=default_der_prefix)
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_xprv(node.to_xprv(), default_der_prefix=False)
self.add_derivation_prefix_and_root_fingerprint(derivation_prefix=derivation, root_node=rootnode)
def get_private_key(self, sequence, password):
xprv = self.get_master_private_key(password)
@ -415,6 +464,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 +527,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 +558,15 @@ 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
def get_derivation_prefix(self) -> str:
return 'm'
@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_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
def update_password(self, old_password, new_password):
self.check_password(old_password)
@ -554,14 +588,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 +615,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,
}
@ -704,40 +738,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):
@ -861,14 +861,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):

48
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

28
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()

113
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

105
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

6
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

2
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)

5
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

23
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:

2
electrum/plugin.py

@ -449,7 +449,7 @@ class DeviceMgr(ThreadJob):
handler.update_status(False)
devices = self.scan_devices()
xpub = keystore.xpub
derivation = keystore.get_derivation()
derivation = keystore.get_derivation_prefix()
client = self.client_by_xpub(plugin, xpub, handler, devices)
if client is None and force_pair:
info = self.select_device(plugin, handler, keystore, devices)

8
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)

313
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

397
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

166
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,47 +254,26 @@ 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_client(self):
# called when user tries to do something like view address, sign somthing.
# - not called during probing/setup
# - 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 = xfp_int_for_keystore(self)
rv.verify_connection(xfp_int, self.ckcc_xpub)
return rv
@ -332,7 +315,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 +355,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 == xfp_int_for_keystore(self)
# 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 +398,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 +418,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 +544,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 +557,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 +569,10 @@ 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()):
der_prefix = ks.get_derivation_prefix()
xpubs.append( (ks.get_root_fingerprint(), xpub, der_prefix) )
derivs.add(der_prefix)
# 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 +583,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 +603,38 @@ 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]
xfp_int = xfp_int_for_keystore(ks)
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
der_full = der_prefix + list(der_suffix)
xfp_paths.append([xfp_int] + 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_for_keystore(keystore: Xpub) -> int:
xfp = keystore.get_root_fingerprint()
return int.from_bytes(bfh(xfp), 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

131
electrum/plugins/coldcard/qt.py

@ -15,11 +15,6 @@ 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 +68,11 @@ 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
# 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)
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
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 +178,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('')

48
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)

101
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
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):
@ -788,11 +769,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',

12
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)

9
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

40
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
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
if TYPE_CHECKING:
from electrum.bip32 import BIP32Node
from electrum.wallet import Abstract_Wallet
from electrum.keystore import Hardware_KeyStore
class HW_PluginBase(BasePlugin):
@ -65,7 +72,7 @@ class HW_PluginBase(BasePlugin):
"""
raise NotImplementedError()
def show_address(self, wallet, address, keystore=None):
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 +139,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 +153,21 @@ 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]
xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps]
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

218
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,112 @@ 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'])
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']
assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput)
assert keystore
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
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 = 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' = None):
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,
)
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)
script_type = self.get_keepkey_output_script_type(txout.script_type)
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
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 +436,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)

106
electrum/plugins/ledger/ledger.py

@ -5,10 +5,10 @@ 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.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
@ -217,6 +217,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 +233,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 +259,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 +269,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 +310,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 +325,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)
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 +384,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)
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 +448,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 +472,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 +495,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'

218
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,114 @@ 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'])
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']
assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput)
assert keystore
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
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.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' = None):
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,
)
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)
script_type = self.get_safet_output_script_type(txout.script_type)
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
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 +434,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)

149
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,101 @@ 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
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
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' = None):
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)
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
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 +452,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

2
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():

106
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

37
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):

3
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)

2
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)

6
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)

6
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"

21
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,)))

2
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

11
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',

267
electrum/tests/test_psbt.py

@ -0,0 +1,267 @@
from pprint import pprint
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
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())

253
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
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,72 +117,49 @@ 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'))
#####
@ -811,45 +749,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+Ph7BLoE1bNPAQQ1h88BggeJdoAAAAFvoSi9wKWkb8evGv0gXbYgcZHUxAUbbtvQZBPNwLGi3wNqTky+e8Rm3WU3hMhrN3Sb7D6CgCnOo0wVaQlW/WFXbwQqQERnAAEA3wIAAAABnCh8O3p5TIeiImyJBHikJS+aVEdsCr+hgtTU3eimPIIBAAAAakcwRAIgTR7BvR+c9dHhx7CDnuJBXb52St/BOycfgpq7teEnUP4CIFp4DWI/xfKhwIZHZVPgYGOZLQC9jHiFKKiCSl7nXBUTASEDVPOeil/J5isfPp2yEcI6UQL8jFq6CRs/hegA8M2L/xj9////ApydBwAAAAAAGXapFLEFQG/gWw3IvkTHg2ulgS2/Z0zoiKwgoQcAAAAAABepFCwWF9JPk25A0UsN8pTst7uq11M3hxIrGAAiAgP+Qtq1hxjqBBP3yN5pPN7uIs4Zsdw0wLvdekgkVGXFokgwRQIhANA8NcspOwHae+DXcc7a+oke1dcKZ3z8zWlnE1b2vr7cAiALKldsTNHGQkgHVs4f1mCsrwulJhk6MJnh+BzdAa0gkwEBBGlSIQIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YiEDXy+CY7s2CNbMTuA71MuNZcTXCvcQSfBfv+5JeIMqH9IhA/5C2rWHGOoEE/fI3mk83u4izhmx3DTAu916SCRUZcWiU64iBgP+Qtq1hxjqBBP3yN5pPN7uIs4Zsdw0wLvdekgkVGXFogy6BNWzAAAAAAAAAAAiBgIJHwtNirMAFqXRwIgkngKIP62BYPBvpTWIrYWYZQo+YgwqQERnAAAAAAAAAAAiBgNfL4JjuzYI1sxO4DvUy41lxNcK9xBJ8F+/7kl4gyof0gxwffHEAAAAAAAAAAAAAAEAaVIhAiq2ef2i4zfECrvAggPT1x0qlv2784yQj3uS5TfBxO//IQNM0UcWdnXGYoZFDMVpQBP0N4OTY115ZKuKVzzD0EqNTiEDWOhaggFFh96oM+tf+5PjmFaQ7kumHvMtWyitWqFhc39TriICA0zRRxZ2dcZihkUMxWlAE/Q3g5NjXXlkq4pXPMPQSo1ODLoE1bMBAAAAAAAAACICAiq2ef2i4zfECrvAggPT1x0qlv2784yQj3uS5TfBxO//DCpARGcBAAAAAAAAACICA1joWoIBRYfeqDPrX/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('cHNidP8BAJ8CAAAAAYfEZGymkLOX41eyOyE3AwaRqQoGimaQg000C0voSs1qAQAAAAD9////A6CGAQAAAAAAFgAUi8nZR+TQrdwvTDS4NxA060ez0wUUDAMAAAAAACIAIKYIfE+EpV3DkBRymhjgiVUTnUOEVZ0f0qSKHZXXRqQlQA0DAAAAAAAZdqkUwT/WKU0b57lBClU49LTvEPxZTueIrBMrGABPAQJXVIMAAAAAAAAAAADWRNzdekrLQyNV4BCsSl+VWUDIKpdncxt9idxC6zzaxAJy+qL5i3bMnWVe8oHAes2nXDCpkNw6Unts+SqPWmuKgARL8hIJTwECV1SDAdJM1ZGAAAABmcTWyJP6Gt3sawEhGBE34lw4GUMzuMVyFbPPHm1+evECoj3a5pi7YJW4uANb3R6UR59mwpZ52Bkx4P4HqkNhSe4E5U2yWk8BAldUgwHSTNWRgAAAALYKhQia0IUKb/6FkKPhUGTmPu/QIIE98uSmsyCc499fAsixTykX1VfNSClwwRTD40V+CdJWOXgCJ3ouTRUZq5+MBAHZMVwAAQEqIKEHAAAAAAAAIKlI1/pqu7l+MXea5UODAStBPVOCHH/TlJAPa0Q8Yd7uIgIDB6PEHQftl21l4hPoI9AoQJN0decJtBJT6F6XDjyxZnRHMEQCIC975frTmPGjV2KTM58idNInrxd5hpCqGtAP+D2WclzFAiAAyy6f/1viOoYnGMZlpQNfFyeD6bMVjq6DZz9sWa3DwAEBBWlSIQKp3LVw6CgMdB8JAywVgJW3qjsM5AGtoDDy1HuZnwIGBiEDB6PEHQftl21l4hPoI9AoQJN0decJtBJT6F6XDjyxZnQhA1IbCkXgQvCMzQOvR/2IuyB7VBTg4wu4eZ/KMRoGMjoZU64iBgMHo8QdB+2XbWXiE+gj0ChAk3R15wm0ElPoXpcOPLFmdAwB2TFcAAAAAAEAAAAiBgNSGwpF4ELwjM0Dr0f9iLsge1QU4OMLuHmfyjEaBjI6GQzlTbJaAAAAAAEAAAAiBgKp3LVw6CgMdB8JAywVgJW3qjsM5AGtoDDy1HuZnwIGBgxL8hIJAAAAAAEAAAAAAAEBaVIhAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcIQIsyigs/dnMHzh9MJhmHWi8nIo5rGvXLDDbV5p4V9CFmyEC2Q48iXOES5zAs4SUxIUVoxnuw6wkiflvb1XmqmkSpghTriICAtkOPIlzhEucwLOElMSFFaMZ7sOsJIn5b29V5qppEqYIDAHZMVwBAAAAAAAAACICAizKKCz92cwfOH0wmGYdaLycijmsa9csMNtXmnhX0IWbDOVNsloBAAAAAAAAACICAgWCn5UiV3EiypypvrZ/lM8v4K0NF+BSoRBwHPQSjTOcDEvyEgkBAAAAAAAAAAAA')
tx.add_info_from_wallet(wallet)
raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
self.assertEqual('45505446ff000200000000010187c4646ca690b397e357b23b2137030691a90a068a6690834d340b4be84acd6a0100000000fdffffff03a0860100000000001600148bc9d947e4d0addc2f4c34b8371034eb47b3d305140c030000000000220020a6087c4f84a55dc39014729a18e08955139d4384559d1fd2a48a1d95d746a425400d0300000000001976a914c13fd6294d1be7b9410a5538f4b4ef10fc594ee788acfeffffffff20a10700000000000000050001ff47304402202f7be5fad398f1a3576293339f2274d227af17798690aa1ad00ff83d96725cc5022000cb2e9fff5be23a862718c665a5035f172783e9b3158eae83673f6c59adc3c00101fffd0201524c53ff02575483000000000000000000d644dcdd7a4acb432355e010ac4a5f955940c82a9767731b7d89dc42eb3cdac40272faa2f98b76cc9d655ef281c07acda75c30a990dc3a527b6cf92a8f5a6b8a80000001004c53ff0257548301d24cd59180000000b60a85089ad0850a6ffe8590a3e15064e63eefd020813df2e4a6b3209ce3df5f02c8b14f2917d557cd482970c114c3e3457e09d256397802277a2e4d1519ab9f8c000001004c53ff0257548301d24cd5918000000199c4d6c893fa1addec6b0121181137e25c38194333b8c57215b3cf1e6d7e7af102a23ddae698bb6095b8b8035bdd1e94479f66c29679d81931e0fe07aa436149ee0000010053ae132b1800',
raw_tx)

2
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')

275
electrum/tests/test_wallet_vertical.py

File diff suppressed because one or more lines are too long

1959
electrum/transaction.py

File diff suppressed because it is too large

6
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):

507
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,
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
@ -305,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer):
def get_master_public_key(self):
return None
def basename(self):
def basename(self) -> str:
return os.path.basename(self.storage.path)
def test_addresses_sanity(self):
@ -392,15 +405,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,13 +436,16 @@ 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)
@ -480,7 +509,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 +519,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 +565,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 +705,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 +785,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 +905,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 +939,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 +960,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,25 +974,30 @@ 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, fee=None, change_addr=None,
domain=None, rbf=False, nonlocal_only=False, tx_version=None) -> 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
@ -973,10 +1007,9 @@ class Abstract_Wallet(AddressSynchronizer):
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 +1023,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 +1033,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 +1079,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 +1121,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 +1153,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 +1189,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 +1200,84 @@ 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) -> 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_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_value_bytes = txin_value.to_bytes(8, byteorder="little", signed=True)
txin.witness_utxo = txin_value_bytes + bfh(bitcoin.address_to_script(address))
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)
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) -> 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)
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,38 +1301,46 @@ 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) -> 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
if isinstance(self, Deterministic_Wallet):
if not txout.pubkeys or len(txout.pubkeys) != len(txout.bip32_paths):
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txout.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]
xfp_bytes = bfh(ks.get_root_fingerprint())
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
der_full = der_prefix + list(der_suffix)
txout.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
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
tx.add_info_from_wallet(self)
# sign. start with ready keystores.
for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
try:
@ -1423,7 +1491,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 +1501,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 +1644,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 +1667,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):
@ -1684,9 +1754,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())
@ -1795,11 +1862,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 +1885,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 +1899,21 @@ 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):
assert self.is_mine(address)
if txin.script_type in ('unknown', '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]
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 +1923,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 +2007,23 @@ 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):
assert self.is_mine(address)
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txin.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]
xfp_bytes = bfh(ks.get_root_fingerprint())
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
der_full = der_prefix + list(der_suffix)
txin.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
def create_new_address(self, for_change=False):
assert type(for_change) is bool
with self.lock:
@ -1995,8 +2074,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 +2104,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 +2115,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 +2142,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)
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)
redeem_script = self.pubkeys_to_redeem_script(pubkeys)
return redeem_script
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 +2230,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']

Loading…
Cancel
Save