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 typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, NamedTuple, Sequence
from . import bitcoin from . import bitcoin
from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY from .bitcoin import COINBASE_MATURITY
from .util import profiler, bfh, TxMinedInfo from .util import profiler, bfh, TxMinedInfo
from .transaction import Transaction, TxOutput from .transaction import Transaction, TxOutput, TxInput, PartialTxInput, TxOutpoint
from .synchronizer import Synchronizer from .synchronizer import Synchronizer
from .verifier import SPV from .verifier import SPV
from .blockchain import hash_header from .blockchain import hash_header
@ -125,12 +125,12 @@ class AddressSynchronizer(Logger):
"""Return number of transactions where address is involved.""" """Return number of transactions where address is involved."""
return len(self._history_local.get(addr, ())) return len(self._history_local.get(addr, ()))
def get_txin_address(self, txi) -> Optional[str]: def get_txin_address(self, txin: TxInput) -> Optional[str]:
addr = txi.get('address') if isinstance(txin, PartialTxInput):
if addr and addr != "(pubkey)": if txin.address:
return addr return txin.address
prevout_hash = txi.get('prevout_hash') prevout_hash = txin.prevout.txid.hex()
prevout_n = txi.get('prevout_n') prevout_n = txin.prevout.out_idx
for addr in self.db.get_txo_addresses(prevout_hash): for addr in self.db.get_txo_addresses(prevout_hash):
l = self.db.get_txo_addr(prevout_hash, addr) l = self.db.get_txo_addr(prevout_hash, addr)
for n, v, is_cb in l: for n, v, is_cb in l:
@ -138,14 +138,8 @@ class AddressSynchronizer(Logger):
return addr return addr
return None return None
def get_txout_address(self, txo: TxOutput): def get_txout_address(self, txo: TxOutput) -> Optional[str]:
if txo.type == TYPE_ADDRESS: return txo.address
addr = txo.address
elif txo.type == TYPE_PUBKEY:
addr = bitcoin.public_key_to_p2pkh(bfh(txo.address))
else:
addr = None
return addr
def load_unverified_transactions(self): def load_unverified_transactions(self):
# review transactions that are in the history # review transactions that are in the history
@ -183,7 +177,7 @@ class AddressSynchronizer(Logger):
if self.synchronizer: if self.synchronizer:
self.synchronizer.add(address) 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 """Returns a set of transaction hashes from the wallet history that are
directly conflicting with tx, i.e. they have common outpoints being directly conflicting with tx, i.e. they have common outpoints being
spent with tx. spent with tx.
@ -194,10 +188,10 @@ class AddressSynchronizer(Logger):
conflicting_txns = set() conflicting_txns = set()
with self.transaction_lock: with self.transaction_lock:
for txin in tx.inputs(): for txin in tx.inputs():
if txin['type'] == 'coinbase': if txin.is_coinbase():
continue continue
prevout_hash = txin['prevout_hash'] prevout_hash = txin.prevout.txid.hex()
prevout_n = txin['prevout_n'] prevout_n = txin.prevout.out_idx
spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n) spending_tx_hash = self.db.get_spent_outpoint(prevout_hash, prevout_n)
if spending_tx_hash is None: if spending_tx_hash is None:
continue continue
@ -213,7 +207,7 @@ class AddressSynchronizer(Logger):
conflicting_txns -= {tx_hash} conflicting_txns -= {tx_hash}
return conflicting_txns 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.""" """Returns whether the tx was successfully added to the wallet history."""
assert tx_hash, tx_hash assert tx_hash, tx_hash
assert tx, tx assert tx, tx
@ -226,7 +220,7 @@ class AddressSynchronizer(Logger):
# BUT we track is_mine inputs in a txn, and during subsequent calls # 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 # of add_transaction tx, we might learn of more-and-more inputs of
# being is_mine, as we roll the gap_limit forward # 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 tx_height = self.get_tx_height(tx_hash).height
if not allow_unrelated: if not allow_unrelated:
# note that during sync, if the transactions are not properly sorted, # 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 self._get_addr_balance_cache.pop(addr, None) # invalidate cache
return return
for txi in tx.inputs(): for txi in tx.inputs():
if txi['type'] == 'coinbase': if txi.is_coinbase():
continue continue
prevout_hash = txi['prevout_hash'] prevout_hash = txi.prevout.txid.hex()
prevout_n = txi['prevout_n'] prevout_n = txi.prevout.out_idx
ser = prevout_hash + ':%d' % prevout_n ser = txi.prevout.to_str()
self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash) self.db.set_spent_outpoint(prevout_hash, prevout_n, tx_hash)
add_value_from_prev_output() add_value_from_prev_output()
# add outputs # add outputs
@ -310,10 +304,10 @@ class AddressSynchronizer(Logger):
if tx is not None: if tx is not None:
# if we have the tx, this branch is faster # if we have the tx, this branch is faster
for txin in tx.inputs(): for txin in tx.inputs():
if txin['type'] == 'coinbase': if txin.is_coinbase():
continue continue
prevout_hash = txin['prevout_hash'] prevout_hash = txin.prevout.txid.hex()
prevout_n = txin['prevout_n'] prevout_n = txin.prevout.out_idx
self.db.remove_spent_outpoint(prevout_hash, prevout_n) self.db.remove_spent_outpoint(prevout_hash, prevout_n)
else: else:
# expensive but always works # expensive but always works
@ -572,7 +566,7 @@ class AddressSynchronizer(Logger):
return cached_local_height return cached_local_height
return self.network.get_local_height() if self.network else self.db.get('stored_height', 0) 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: with self.lock:
self.add_transaction(tx.txid(), tx) self.add_transaction(tx.txid(), tx)
self.future_tx[tx.txid()] = num_blocks self.future_tx[tx.txid()] = num_blocks
@ -649,9 +643,9 @@ class AddressSynchronizer(Logger):
if self.is_mine(addr): if self.is_mine(addr):
is_mine = True is_mine = True
is_relevant = 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: for n, v, cb in d:
if n == txin['prevout_n']: if n == txin.prevout.out_idx:
value = v value = v
break break
else: else:
@ -736,23 +730,19 @@ class AddressSynchronizer(Logger):
sent[txi] = height sent[txi] = height
return received, sent 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) coins, spent = self.get_addr_io(address)
for txi in spent: for txi in spent:
coins.pop(txi) coins.pop(txi)
out = {} out = {}
for txo, v in coins.items(): for prevout_str, v in coins.items():
tx_height, value, is_cb = v tx_height, value, is_cb = v
prevout_hash, prevout_n = txo.split(':') prevout = TxOutpoint.from_str(prevout_str)
x = { utxo = PartialTxInput(prevout=prevout)
'address':address, utxo._trusted_address = address
'value':value, utxo._trusted_value_sats = value
'prevout_n':int(prevout_n), utxo.block_height = tx_height
'prevout_hash':prevout_hash, out[prevout] = utxo
'height':tx_height,
'coinbase':is_cb
}
out[txo] = x
return out return out
# return the total amount ever received by an address # return the total amount ever received by an address
@ -799,7 +789,8 @@ class AddressSynchronizer(Logger):
@with_local_height_cached @with_local_height_cached
def get_utxos(self, domain=None, *, excluded_addresses=None, 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 = [] coins = []
if domain is None: if domain is None:
domain = self.get_addresses() domain = self.get_addresses()
@ -809,14 +800,15 @@ class AddressSynchronizer(Logger):
mempool_height = self.get_local_height() + 1 # height of next block mempool_height = self.get_local_height() + 1 # height of next block
for addr in domain: for addr in domain:
utxos = self.get_addr_utxo(addr) utxos = self.get_addr_utxo(addr)
for x in utxos.values(): for utxo in utxos.values():
if confirmed_only and x['height'] <= 0: if confirmed_only and utxo.block_height <= 0:
continue continue
if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL: if nonlocal_only and utxo.block_height == TX_HEIGHT_LOCAL:
continue 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 continue
coins.append(x) coins.append(utxo)
continue continue
return coins 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 bitcoin
from . import keystore from . import keystore
from . import mnemonic 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 .keystore import bip44_derivation, purpose48_derivation
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet, from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
wallet_types, Wallet, Abstract_Wallet) wallet_types, Wallet, Abstract_Wallet)
@ -230,7 +230,7 @@ class BaseWizard(Logger):
assert bitcoin.is_private_key(pk) assert bitcoin.is_private_key(pk)
txin_type, pubkey = k.import_privkey(pk, None) txin_type, pubkey = k.import_privkey(pk, None)
addr = bitcoin.pubkey_to_address(txin_type, pubkey) 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) self.keystores.append(k)
else: else:
return self.terminate() return self.terminate()
@ -420,16 +420,19 @@ class BaseWizard(Logger):
from .keystore import hardware_keystore from .keystore import hardware_keystore
try: try:
xpub = self.plugin.get_xpub(device_info.device.id_, derivation, xtype, self) 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: except ScriptTypeNotSupported:
raise # this is handled in derivation_dialog raise # this is handled in derivation_dialog
except BaseException as e: except BaseException as e:
self.logger.exception('') self.logger.exception('')
self.show_error(e) self.show_error(e)
return return
xfp = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower()
d = { d = {
'type': 'hardware', 'type': 'hardware',
'hw_type': name, 'hw_type': name,
'derivation': derivation, 'derivation': derivation,
'root_fingerprint': xfp,
'xpub': xpub, 'xpub': xpub,
'label': device_info.label, 'label': device_info.label,
} }

33
electrum/bip32.py

@ -116,7 +116,7 @@ class BIP32Node(NamedTuple):
eckey: Union[ecc.ECPubkey, ecc.ECPrivkey] eckey: Union[ecc.ECPubkey, ecc.ECPrivkey]
chaincode: bytes chaincode: bytes
depth: int = 0 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 child_number: bytes = b'\x00'*4
@classmethod @classmethod
@ -161,7 +161,18 @@ class BIP32Node(NamedTuple):
eckey=ecc.ECPrivkey(master_k), eckey=ecc.ECPrivkey(master_k),
chaincode=master_c) 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: 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(): if not self.is_private():
raise Exception("cannot serialize as xprv; private key missing") raise Exception("cannot serialize as xprv; private key missing")
payload = (xprv_header(self.xtype, net=net) + payload = (xprv_header(self.xtype, net=net) +
@ -172,9 +183,13 @@ class BIP32Node(NamedTuple):
bytes([0]) + bytes([0]) +
self.eckey.get_secret_bytes()) self.eckey.get_secret_bytes())
assert len(payload) == 78, f"unexpected xprv payload len {len(payload)}" assert len(payload) == 78, f"unexpected xprv payload len {len(payload)}"
return EncodeBase58Check(payload) return payload
def to_xpub(self, *, net=None) -> str: 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) + payload = (xpub_header(self.xtype, net=net) +
bytes([self.depth]) + bytes([self.depth]) +
self.fingerprint + self.fingerprint +
@ -182,7 +197,7 @@ class BIP32Node(NamedTuple):
self.chaincode + self.chaincode +
self.eckey.get_public_key_bytes(compressed=True)) self.eckey.get_public_key_bytes(compressed=True))
assert len(payload) == 78, f"unexpected xpub payload len {len(payload)}" assert len(payload) == 78, f"unexpected xpub payload len {len(payload)}"
return EncodeBase58Check(payload) return payload
def to_xkey(self, *, net=None) -> str: def to_xkey(self, *, net=None) -> str:
if self.is_private(): if self.is_private():
@ -190,6 +205,12 @@ class BIP32Node(NamedTuple):
else: else:
return self.to_xpub(net=net) 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': def convert_to_public(self) -> 'BIP32Node':
if not self.is_private(): if not self.is_private():
return self return self
@ -248,6 +269,12 @@ class BIP32Node(NamedTuple):
fingerprint=fingerprint, fingerprint=fingerprint,
child_number=child_number) 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): def xpub_type(x):
return BIP32Node.from_xkey(x).xtype return BIP32Node.from_xkey(x).xtype

23
electrum/bitcoin.py

@ -45,6 +45,7 @@ COIN = 100000000
TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000 TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000
# supported types of transaction outputs # supported types of transaction outputs
# TODO kill these with fire
TYPE_ADDRESS = 0 TYPE_ADDRESS = 0
TYPE_PUBKEY = 1 TYPE_PUBKEY = 1
TYPE_SCRIPT = 2 TYPE_SCRIPT = 2
@ -237,6 +238,8 @@ def script_num_to_hex(i: int) -> str:
def var_int(i: int) -> str: def var_int(i: int) -> str:
# https://en.bitcoin.it/wiki/Protocol_specification#Variable_length_integer # 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: if i<0xfd:
return int_to_hex(i) return int_to_hex(i)
elif i<=0xffff: elif i<=0xffff:
@ -372,24 +375,28 @@ def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
else: else:
raise NotImplementedError(txin_type) 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 net is None: net = constants.net
if txin_type == 'p2sh': 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': 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': elif txin_type == 'p2wsh-p2sh':
scriptSig = p2wsh_nested_script(redeem_script) # given scriptcode is a witness_script
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net) redeem_script = p2wsh_nested_script(scriptcode)
return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net)
else: else:
raise NotImplementedError(txin_type) raise NotImplementedError(txin_type)
def script_to_address(script: str, *, net=None) -> str: def script_to_address(script: str, *, net=None) -> str:
from .transaction import get_address_from_output_script from .transaction import get_address_from_output_script
t, addr = get_address_from_output_script(bfh(script), net=net) return get_address_from_output_script(bfh(script), net=net)
assert t == TYPE_ADDRESS
return addr
def address_to_script(addr: str, *, net=None) -> str: def address_to_script(addr: str, *, net=None) -> str:
if net is None: net = constants.net if net is None: net = constants.net

81
electrum/coinchooser.py

@ -24,11 +24,11 @@
# SOFTWARE. # SOFTWARE.
from collections import defaultdict from collections import defaultdict
from math import floor, log10 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 decimal import Decimal
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address from .bitcoin import sha256, COIN, is_address
from .transaction import Transaction, TxOutput from .transaction import Transaction, TxOutput, PartialTransaction, PartialTxInput, PartialTxOutput
from .util import NotEnoughFunds from .util import NotEnoughFunds
from .logging import Logger from .logging import Logger
@ -73,21 +73,21 @@ class PRNG:
class Bucket(NamedTuple): class Bucket(NamedTuple):
desc: str desc: str
weight: int # as in BIP-141 weight: int # as in BIP-141
value: int # in satoshis value: int # in satoshis
effective_value: int # estimate of value left after subtracting fees. in satoshis effective_value: int # estimate of value left after subtracting fees. in satoshis
coins: List[dict] # UTXOs coins: List[PartialTxInput] # UTXOs
min_height: int # min block height where a coin was confirmed min_height: int # min block height where a coin was confirmed
witness: bool # whether any coin uses segwit witness: bool # whether any coin uses segwit
class ScoredCandidate(NamedTuple): class ScoredCandidate(NamedTuple):
penalty: float penalty: float
tx: Transaction tx: PartialTransaction
buckets: List[Bucket] 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''' '''Remove buckets that are unnecessary in achieving the spend amount'''
if sufficient_funds([], bucket_value_sum=0): if sufficient_funds([], bucket_value_sum=0):
# none of the buckets are needed # none of the buckets are needed
@ -108,26 +108,27 @@ class CoinChooserBase(Logger):
def __init__(self): def __init__(self):
Logger.__init__(self) Logger.__init__(self)
def keys(self, coins): def keys(self, coins: Sequence[PartialTxInput]) -> Sequence[str]:
raise NotImplementedError raise NotImplementedError
def bucketize_coins(self, coins, *, fee_estimator_vb): def bucketize_coins(self, coins: Sequence[PartialTxInput], *, fee_estimator_vb):
keys = self.keys(coins) keys = self.keys(coins)
buckets = defaultdict(list) buckets = defaultdict(list) # type: Dict[str, List[PartialTxInput]]
for key, coin in zip(keys, coins): for key, coin in zip(keys, coins):
buckets[key].append(coin) buckets[key].append(coin)
# fee_estimator returns fee to be paid, for given vbytes. # fee_estimator returns fee to be paid, for given vbytes.
# guess whether it is just returning a constant as follows. # guess whether it is just returning a constant as follows.
constant_fee = fee_estimator_vb(2000) == fee_estimator_vb(200) 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) 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 # note that we're guessing whether the tx uses segwit based
# on this single bucket # on this single bucket
weight = sum(Transaction.estimated_input_weight(coin, witness) weight = sum(Transaction.estimated_input_weight(coin, witness)
for coin in coins) for coin in coins)
value = sum(coin['value'] for coin in coins) value = sum(coin.value_sats() for coin in coins)
min_height = min(coin['height'] 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, # the fee estimator is typically either a constant or a linear function,
# so the "function:" effective_value(bucket) will be homomorphic for addition # so the "function:" effective_value(bucket) will be homomorphic for addition
# i.e. effective_value(b1) + effective_value(b2) = effective_value(b1 + b2) # 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())) 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 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 # Break change up if bigger than max_change
output_amounts = [o.value for o in tx.outputs()] output_amounts = [o.value for o in tx.outputs()]
# Don't split change of less than 0.02 BTC # Don't split change of less than 0.02 BTC
@ -205,7 +208,8 @@ class CoinChooserBase(Logger):
return amounts 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) amounts = self._change_amounts(tx, len(change_addrs), fee_estimator_numchange)
assert min(amounts) >= 0 assert min(amounts) >= 0
assert len(change_addrs) >= len(amounts) assert len(change_addrs) >= len(amounts)
@ -213,21 +217,23 @@ class CoinChooserBase(Logger):
# If change is above dust threshold after accounting for the # If change is above dust threshold after accounting for the
# size of the change output, add it to the transaction. # size of the change output, add it to the transaction.
amounts = [amount for amount in amounts if amount >= dust_threshold] 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)] for addr, amount in zip(change_addrs, amounts)]
return change return change
def _construct_tx_from_selected_buckets(self, *, buckets, base_tx, change_addrs, def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket],
fee_estimator_w, dust_threshold, base_weight): 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 # 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.add_inputs([coin for b in buckets for coin in b.coins])
tx_weight = self._get_tx_weight(buckets, base_weight=base_weight) tx_weight = self._get_tx_weight(buckets, base_weight=base_weight)
# change is sent back to sending address unless specified # change is sent back to sending address unless specified
if not change_addrs: 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" # note: this is not necessarily the final "first input address"
# because the inputs had not been sorted at this point # because the inputs had not been sorted at this point
assert is_address(change_addrs[0]) assert is_address(change_addrs[0])
@ -240,7 +246,7 @@ class CoinChooserBase(Logger):
return tx, change 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 """Given a collection of buckets, return the total weight of the
resulting transaction. resulting transaction.
base_weight is the weight of the tx that includes the fixed (non-change) base_weight is the weight of the tx that includes the fixed (non-change)
@ -260,8 +266,9 @@ class CoinChooserBase(Logger):
return total_weight return total_weight
def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator_vb, def make_tx(self, *, coins: Sequence[PartialTxInput], inputs: List[PartialTxInput],
dust_threshold): 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 """Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is 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' assert outputs, 'tx outputs cannot be empty'
# Deterministic randomness from coins # Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins] utxos = [c.prevout.serialize_to_network() for c in coins]
self.p = PRNG(''.join(sorted(utxos))) self.p = PRNG(b''.join(sorted(utxos)))
# Copy the outputs so when adding change we don't modify "outputs" # 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() input_value = base_tx.input_value()
# Weight of the transaction with no inputs and no change # Weight of the transaction with no inputs and no change
@ -331,14 +338,15 @@ class CoinChooserBase(Logger):
return tx 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: penalty_func: Callable[[List[Bucket]], ScoredCandidate]) -> ScoredCandidate:
raise NotImplemented('To be subclassed') raise NotImplemented('To be subclassed')
class CoinChooserRandom(CoinChooserBase): 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.''' '''Returns a list of bucket sets.'''
if not buckets: if not buckets:
raise NotEnoughFunds() raise NotEnoughFunds()
@ -373,7 +381,8 @@ class CoinChooserRandom(CoinChooserBase):
candidates = [[buckets[n] for n in c] for c in candidates] candidates = [[buckets[n] for n in c] for c in candidates]
return [strip_unneeded(c, sufficient_funds) 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. """Returns a list of bucket sets preferring confirmed coins.
Any bucket can be: Any bucket can be:
@ -433,13 +442,13 @@ class CoinChooserPrivacy(CoinChooserRandom):
""" """
def keys(self, coins): 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): def penalty_func(self, base_tx, *, tx_from_buckets):
min_change = min(o.value for o in base_tx.outputs()) * 0.75 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 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) # Penalize using many buckets (~inputs)
badness = len(buckets) - 1 badness = len(buckets) - 1
tx, change_outputs = tx_from_buckets(buckets) tx, change_outputs = tx_from_buckets(buckets)

97
electrum/commands.py

@ -35,16 +35,17 @@ import asyncio
import inspect import inspect
from functools import wraps, partial from functools import wraps, partial
from decimal import Decimal from decimal import Decimal
from typing import Optional, TYPE_CHECKING, Dict from typing import Optional, TYPE_CHECKING, Dict, List
from .import util, ecc 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 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 .util import standardize_path
from . import bitcoin 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 .bip32 import BIP32Node
from .i18n import _ 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 .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier from .synchronizer import Notifier
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text 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): async def listunspent(self, wallet: Abstract_Wallet = None):
"""List unspent outputs. Returns the list of unspent transaction """List unspent outputs. Returns the list of unspent transaction
outputs in your wallet.""" outputs in your wallet."""
l = copy.deepcopy(wallet.get_utxos()) coins = []
for i in l: for txin in wallet.get_utxos():
v = i["value"] d = txin.to_json()
i["value"] = str(Decimal(v)/COIN) if v is not None else None v = d.pop("value_sats")
return l d["value"] = str(Decimal(v)/COIN) if v is not None else None
coins.append(d)
return coins
@command('n') @command('n')
async def getaddressunspent(self, address): async def getaddressunspent(self, address):
@ -320,46 +323,50 @@ class Commands:
Outputs must be a list of {'address':address, 'value':satoshi_amount}. Outputs must be a list of {'address':address, 'value':satoshi_amount}.
""" """
keypairs = {} keypairs = {}
inputs = jsontx.get('inputs') inputs = [] # type: List[PartialTxInput]
outputs = jsontx.get('outputs')
locktime = jsontx.get('lockTime', 0) locktime = jsontx.get('lockTime', 0)
for txin in inputs: for txin_dict in jsontx.get('inputs'):
if txin.get('output'): if txin_dict.get('prevout_hash') is not None and txin_dict.get('prevout_n') is not None:
prevout_hash, prevout_n = txin['output'].split(':') prevout = TxOutpoint(txid=bfh(txin_dict['prevout_hash']), out_idx=int(txin_dict['prevout_n']))
txin['prevout_n'] = int(prevout_n) elif txin_dict.get('output'):
txin['prevout_hash'] = prevout_hash prevout = TxOutpoint.from_str(txin_dict['output'])
sec = txin.get('privkey') 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: if sec:
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed) pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
keypairs[pubkey] = privkey, compressed keypairs[pubkey] = privkey, compressed
txin['type'] = txin_type txin.script_type = txin_type
txin['x_pubkeys'] = [pubkey] txin.pubkeys = [bfh(pubkey)]
txin['signatures'] = [None] txin.num_sig = 1
txin['num_sig'] = 1 inputs.append(txin)
outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs] outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout['value']))
tx = Transaction.from_io(inputs, outputs, locktime=locktime) for txout in jsontx.get('outputs')]
tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime)
tx.sign(keypairs) tx.sign(keypairs)
return tx.as_dict() return tx.serialize()
@command('wp') @command('wp')
async def signtransaction(self, tx, privkey=None, password=None, wallet: Abstract_Wallet = None): 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.""" """Sign a transaction. The wallet keys will be used unless a private key is provided."""
tx = Transaction(tx) tx = PartialTransaction(tx)
if privkey: if privkey:
txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey) txin_type, privkey2, compressed = bitcoin.deserialize_privkey(privkey)
pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex() pubkey = ecc.ECPrivkey(privkey2).get_public_key_bytes(compressed=compressed).hex()
tx.sign({pubkey:(privkey2, compressed)}) tx.sign({pubkey:(privkey2, compressed)})
else: else:
wallet.sign_transaction(tx, password) wallet.sign_transaction(tx, password)
return tx.as_dict() return tx.serialize()
@command('') @command('')
async def deserialize(self, tx): async def deserialize(self, tx):
"""Deserialize a serialized transaction""" """Deserialize a serialized transaction"""
tx = Transaction(tx) tx = tx_from_any(tx)
return tx.deserialize(force_full_parse=True) return tx.to_json()
@command('n') @command('n')
async def broadcast(self, tx): async def broadcast(self, tx):
@ -392,9 +399,9 @@ class Commands:
if isinstance(address, str): if isinstance(address, str):
address = address.strip() address = address.strip()
if is_address(address): if is_address(address):
return wallet.export_private_key(address, password)[0] return wallet.export_private_key(address, password)
domain = address 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') @command('w')
async def ismine(self, address, wallet: Abstract_Wallet = None): async def ismine(self, address, wallet: Abstract_Wallet = None):
@ -513,8 +520,13 @@ class Commands:
privkeys = privkey.split() privkeys = privkey.split()
self.nocheck = nocheck self.nocheck = nocheck
#dest = self._resolver(destination) #dest = self._resolver(destination)
tx = sweep(privkeys, self.network, self.config, destination, tx_fee, imax) tx = sweep(privkeys,
return tx.as_dict() if tx else None network=self.network,
config=self.config,
to_address=destination,
fee=tx_fee,
imax=imax)
return tx.serialize() if tx else None
@command('wp') @command('wp')
async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None): async def signmessage(self, address, message, password=None, wallet: Abstract_Wallet = None):
@ -541,17 +553,20 @@ class Commands:
for address, amount in outputs: for address, amount in outputs:
address = self._resolver(address, wallet) address = self._resolver(address, wallet)
amount = satoshis(amount) 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) coins = wallet.get_spendable_coins(domain_addr)
if domain_coins is not None: 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: if feerate is not None:
fee_per_kb = 1000 * Decimal(feerate) fee_per_kb = 1000 * Decimal(feerate)
fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb) fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
else: else:
fee_estimator = fee 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: if locktime is not None:
tx.locktime = locktime tx.locktime = locktime
if rbf is None: if rbf is None:
@ -581,7 +596,7 @@ class Commands:
rbf=rbf, rbf=rbf,
password=password, password=password,
locktime=locktime) locktime=locktime)
return tx.as_dict() return tx.serialize()
@command('wp') @command('wp')
async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None, 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, rbf=rbf,
password=password, password=password,
locktime=locktime) locktime=locktime)
return tx.as_dict() return tx.serialize()
@command('w') @command('w')
async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None): 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") raise Exception("Unknown transaction")
if tx.txid() != txid: if tx.txid() != txid:
raise Exception("Mismatching txid") raise Exception("Mismatching txid")
return tx.as_dict() return tx.serialize()
@command('') @command('')
async def encrypt(self, pubkey, message) -> str: async def encrypt(self, pubkey, message) -> str:
@ -960,7 +975,7 @@ class Commands:
chan_id, _ = channel_id_from_funding_tx(txid, int(index)) chan_id, _ = channel_id_from_funding_tx(txid, int(index))
chan = wallet.lnworker.channels[chan_id] chan = wallet.lnworker.channels[chan_id]
tx = chan.force_close_tx() tx = chan.force_close_tx()
return tx.as_dict() return tx.serialize()
def eval_bool(x: str) -> bool: def eval_bool(x: str) -> bool:
if x == 'false': return False if x == 'false': return False
@ -1037,7 +1052,7 @@ command_options = {
# don't use floats because of rounding errors # 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))) json_loads = lambda x: json.loads(x, parse_float=lambda x: str(Decimal(x)))
arg_types = { arg_types = {
'num': int, 'num': int,
@ -1046,7 +1061,7 @@ arg_types = {
'year': int, 'year': int,
'from_height': int, 'from_height': int,
'to_height': int, 'to_height': int,
'tx': tx_from_str, 'tx': convert_tx_str_to_hex,
'pubkeys': json_loads, 'pubkeys': json_loads,
'jsontx': json_loads, 'jsontx': json_loads,
'inputs': json_loads, 'inputs': json_loads,

10
electrum/ecc.py

@ -25,6 +25,7 @@
import base64 import base64
import hashlib import hashlib
import functools
from typing import Union, Tuple, Optional from typing import Union, Tuple, Optional
import ecdsa import ecdsa
@ -181,6 +182,7 @@ class _PubkeyForPointAtInfinity:
point = ecdsa.ellipticcurve.INFINITY point = ecdsa.ellipticcurve.INFINITY
@functools.total_ordering
class ECPubkey(object): class ECPubkey(object):
def __init__(self, b: Optional[bytes]): def __init__(self, b: Optional[bytes]):
@ -257,6 +259,14 @@ class ECPubkey(object):
def __ne__(self, other): def __ne__(self, other):
return not (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: def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: sha256d(msg_magic(x))) -> None:
assert_bytes(message) assert_bytes(message)
h = algo(message) h = algo(message)

9
electrum/gui/kivy/main_window.py

@ -9,7 +9,6 @@ import threading
import asyncio import asyncio
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from electrum.bitcoin import TYPE_ADDRESS
from electrum.storage import WalletStorage, StorageReadWriteError from electrum.storage import WalletStorage, StorageReadWriteError
from electrum.wallet import Wallet, InternalAddressCorruption from electrum.wallet import Wallet, InternalAddressCorruption
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
@ -855,7 +854,7 @@ class ElectrumWindow(App):
self._trigger_update_status() self._trigger_update_status()
def get_max_amount(self): def get_max_amount(self):
from electrum.transaction import TxOutput from electrum.transaction import PartialTxOutput
if run_hook('abort_send', self): if run_hook('abort_send', self):
return '' return ''
inputs = self.wallet.get_spendable_coins(None) inputs = self.wallet.get_spendable_coins(None)
@ -866,9 +865,9 @@ class ElectrumWindow(App):
addr = str(self.send_screen.screen.address) addr = str(self.send_screen.screen.address)
if not addr: if not addr:
addr = self.wallet.dummy_address() addr = self.wallet.dummy_address()
outputs = [TxOutput(TYPE_ADDRESS, addr, '!')] outputs = [PartialTxOutput.from_address_and_value(addr, '!')]
try: try:
tx = self.wallet.make_unsigned_transaction(inputs, outputs) tx = self.wallet.make_unsigned_transaction(coins=inputs, outputs=outputs)
except NoDynamicFeeEstimates as e: except NoDynamicFeeEstimates as e:
Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e))) Clock.schedule_once(lambda dt, bound_e=e: self.show_error(str(bound_e)))
return '' return ''
@ -1199,7 +1198,7 @@ class ElectrumWindow(App):
if not self.wallet.can_export(): if not self.wallet.can_export():
return return
try: try:
key = str(self.wallet.export_private_key(addr, password)[0]) key = str(self.wallet.export_private_key(addr, password))
pk_label.data = key pk_label.data = key
except InvalidPassword: except InvalidPassword:
self.show_error("Invalid PIN") 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.app import App
from kivy.clock import Clock from kivy.clock import Clock
from kivy.factory import Factory from kivy.factory import Factory
@ -8,6 +10,9 @@ from kivy.uix.boxlayout import BoxLayout
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
from electrum.transaction import TxOutput
class AnimatedPopup(Factory.Popup): class AnimatedPopup(Factory.Popup):
@ -202,13 +207,13 @@ class OutputList(RecycleView):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(OutputList, self).__init__(**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 = [] res = []
for o in outputs: for o in outputs:
value = self.app.format_amount_and_units(o.value) 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 self.data = res

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

@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import NamedTuple, Callable from typing import NamedTuple, Callable, TYPE_CHECKING
from kivy.app import App from kivy.app import App
from kivy.factory import Factory from kivy.factory import Factory
@ -17,6 +17,10 @@ from electrum.util import InvalidPassword
from electrum.address_synchronizer import TX_HEIGHT_LOCAL from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.wallet import CannotBumpFee from electrum.wallet import CannotBumpFee
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
from electrum.transaction import Transaction
Builder.load_string(''' Builder.load_string('''
@ -121,9 +125,9 @@ class TxDialog(Factory.Popup):
def __init__(self, app, tx): def __init__(self, app, tx):
Factory.Popup.__init__(self) Factory.Popup.__init__(self)
self.app = app self.app = app # type: ElectrumWindow
self.wallet = self.app.wallet self.wallet = self.app.wallet
self.tx = tx self.tx = tx # type: Transaction
self._action_button_fn = lambda btn: None self._action_button_fn = lambda btn: None
def on_open(self): def on_open(self):
@ -166,7 +170,7 @@ class TxDialog(Factory.Popup):
self.fee_str = _('unknown') self.fee_str = _('unknown')
self.feerate_str = _('unknown') self.feerate_str = _('unknown')
self.can_sign = self.wallet.can_sign(self.tx) 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.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL
self.update_action_button() self.update_action_button()
@ -252,7 +256,7 @@ class TxDialog(Factory.Popup):
def show_qr(self): def show_qr(self):
from electrum.bitcoin import base_encode, bfh from electrum.bitcoin import base_encode, bfh
raw_tx = str(self.tx) raw_tx = self.tx.serialize()
text = bfh(raw_tx) text = bfh(raw_tx)
text = base_encode(text, base=43) text = base_encode(text, base=43)
self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx) self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=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.factory import Factory
from kivy.utils import platform 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 profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum import bitcoin, constants 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 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.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 from electrum.plugin import run_hook
@ -276,8 +275,7 @@ class SendScreen(CScreen):
return return
# try to decode as transaction # try to decode as transaction
try: try:
raw_tx = tx_from_str(data) tx = tx_from_any(data)
tx = Transaction(raw_tx)
tx.deserialize() tx.deserialize()
except: except:
tx = None tx = None
@ -313,7 +311,7 @@ class SendScreen(CScreen):
if not bitcoin.is_address(address): if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
return 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) return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
def do_save(self): def do_save(self):
@ -353,11 +351,11 @@ class SendScreen(CScreen):
def _do_pay_onchain(self, invoice, rbf): def _do_pay_onchain(self, invoice, rbf):
# make unsigned transaction # make unsigned transaction
outputs = invoice['outputs'] # type: List[TxOutput] outputs = invoice['outputs'] # type: List[PartialTxOutput]
amount = sum(map(lambda x: x.value, outputs)) amount = sum(map(lambda x: x.value, outputs))
coins = self.app.wallet.get_spendable_coins(None) coins = self.app.wallet.get_spendable_coins(None)
try: try:
tx = self.app.wallet.make_unsigned_transaction(coins, outputs, None) tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
except NotEnoughFunds: except NotEnoughFunds:
self.app.show_error(_("Not enough funds")) self.app.show_error(_("Not enough funds"))
return return

12
electrum/gui/qt/address_dialog.py

@ -84,16 +84,20 @@ class AddressDialog(WindowModalDialog):
pubkey_e.setReadOnly(True) pubkey_e.setReadOnly(True)
vbox.addWidget(pubkey_e) vbox.addWidget(pubkey_e)
try: redeem_script = self.wallet.get_redeem_script(address)
redeem_script = self.wallet.pubkeys_to_redeem_script(pubkeys)
except BaseException as e:
redeem_script = None
if redeem_script: if redeem_script:
vbox.addWidget(QLabel(_("Redeem Script") + ':')) vbox.addWidget(QLabel(_("Redeem Script") + ':'))
redeem_e = ShowQRTextEdit(text=redeem_script) redeem_e = ShowQRTextEdit(text=redeem_script)
redeem_e.addCopyButton(self.app) redeem_e.addCopyButton(self.app)
vbox.addWidget(redeem_e) 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"))) vbox.addWidget(QLabel(_("History")))
addr_hist_model = AddressHistoryModel(self.parent, self.address) addr_hist_model = AddressHistoryModel(self.parent, self.address)
self.hw = HistoryList(self.parent, addr_hist_model) 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 from functools import partial
import queue import queue
import asyncio 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.QtGui import QPixmap, QKeySequence, QIcon, QCursor, QFont
from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal from PyQt5.QtCore import Qt, QRect, QStringListModel, QSize, pyqtSignal
@ -50,7 +50,7 @@ from PyQt5.QtWidgets import (QMessageBox, QComboBox, QSystemTrayIcon, QTabWidget
import electrum import electrum
from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands, from electrum import (keystore, simple_config, ecc, constants, util, bitcoin, commands,
coinchooser, paymentrequest) 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.plugin import run_hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import (format_time, format_satoshis, format_fee_satoshis, 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) InvalidBitcoinURI, InvoiceError)
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import PaymentFailure, SENT, RECEIVED 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.address_synchronizer import AddTransactionException
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
sweep_preparations, InternalAddressCorruption) sweep_preparations, InternalAddressCorruption)
@ -922,7 +923,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def show_transaction(self, tx, *, invoice=None, tx_desc=None): def show_transaction(self, tx, *, invoice=None, tx_desc=None):
'''tx_desc is set only for txs created in the Send tab''' '''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): def create_receive_tab(self):
# A 4-column grid layout. All the stretch is in the last column. # 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): def update_fee(self):
self.require_fee_update = True self.require_fee_update = True
def get_payto_or_dummy(self): def get_payto_or_dummy(self) -> bytes:
r = self.payto_e.get_recipient() r = self.payto_e.get_destination_scriptpubkey()
if r: if r:
return 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): def do_update_fee(self):
'''Recalculate the fee. If the fee was manually input, retain it, but '''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() coins = self.get_coins()
if not outputs: if not outputs:
_type, addr = self.get_payto_or_dummy() scriptpubkey = self.get_payto_or_dummy()
outputs = [TxOutput(_type, addr, amount)] outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]
is_sweep = bool(self.tx_external_keypairs) is_sweep = bool(self.tx_external_keypairs)
make_tx = lambda fee_est: \ make_tx = lambda fee_est: \
self.wallet.make_unsigned_transaction( self.wallet.make_unsigned_transaction(
coins, outputs, coins=coins,
fixed_fee=fee_est, is_sweep=is_sweep) outputs=outputs,
fee=fee_est,
is_sweep=is_sweep)
try: try:
tx = make_tx(fee_estimator) tx = make_tx(fee_estimator)
self.not_enough_funds = False self.not_enough_funds = False
@ -1546,7 +1549,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
menu.addAction(_("Remove"), lambda: self.from_list_delete(item)) menu.addAction(_("Remove"), lambda: self.from_list_delete(item))
menu.exec_(self.from_list.viewport().mapToGlobal(position)) 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.pay_from = list(coins)
self.redraw_from_list() self.redraw_from_list()
@ -1555,12 +1558,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.from_label.setHidden(len(self.pay_from) == 0) self.from_label.setHidden(len(self.pay_from) == 0)
self.from_list.setHidden(len(self.pay_from) == 0) self.from_list.setHidden(len(self.pay_from) == 0)
def format(x): def format(txin: PartialTxInput):
h = x.get('prevout_hash') h = txin.prevout.txid.hex()
return h[0:10] + '...' + h[-10:] + ":%d"%x.get('prevout_n') + '\t' + "%s"%x.get('address') + '\t' 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: 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)) item.setFont(0, QFont(MONOSPACE_FONT))
self.from_list.addTopLevelItem(item) self.from_list.addTopLevelItem(item)
@ -1620,14 +1625,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
fee_estimator = None fee_estimator = None
return fee_estimator return fee_estimator
def read_outputs(self): def read_outputs(self) -> List[PartialTxOutput]:
if self.payment_request: if self.payment_request:
outputs = self.payment_request.get_outputs() outputs = self.payment_request.get_outputs()
else: else:
outputs = self.payto_e.get_outputs(self.max_button.isChecked()) outputs = self.payto_e.get_outputs(self.max_button.isChecked())
return outputs 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. """Returns whether there are errors with outputs.
Also shows error dialog to user if so. Also shows error dialog to user if so.
""" """
@ -1636,12 +1641,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return True return True
for o in outputs: for o in outputs:
if o.address is None: if o.scriptpubkey is None:
self.show_error(_('Bitcoin Address is None')) self.show_error(_('Bitcoin Address is None'))
return True 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: if o.value is None:
self.show_error(_('Invalid Amount')) self.show_error(_('Invalid Amount'))
return True return True
@ -1749,20 +1751,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return return
elif invoice['type'] == PR_TYPE_ONCHAIN: elif invoice['type'] == PR_TYPE_ONCHAIN:
message = invoice['message'] message = invoice['message']
outputs = invoice['outputs'] outputs = invoice['outputs'] # type: List[PartialTxOutput]
else: else:
raise Exception('unknown invoice type') raise Exception('unknown invoice type')
if run_hook('abort_send', self): if run_hook('abort_send', self):
return return
outputs = [TxOutput(*x) for x in outputs] for txout in outputs:
assert isinstance(txout, PartialTxOutput)
fee_estimator = self.get_send_fee_estimator() fee_estimator = self.get_send_fee_estimator()
coins = self.get_coins() coins = self.get_coins()
try: try:
is_sweep = bool(self.tx_external_keypairs) is_sweep = bool(self.tx_external_keypairs)
tx = self.wallet.make_unsigned_transaction( tx = self.wallet.make_unsigned_transaction(
coins, outputs, fixed_fee=fee_estimator, coins=coins,
outputs=outputs,
fee=fee_estimator,
is_sweep=is_sweep) is_sweep=is_sweep)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e: except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
self.show_message(str(e)) self.show_message(str(e))
@ -1837,7 +1842,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def sign_tx(self, tx, callback, password): def sign_tx(self, tx, callback, password):
self.sign_tx_with_password(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 '''Sign the transaction in a separate thread. When done, calls
the callback with a success code of True or False. 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 on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
if self.tx_external_keypairs: if self.tx_external_keypairs:
# can sign directly # can sign directly
task = partial(Transaction.sign, tx, self.tx_external_keypairs) task = partial(tx.sign, self.tx_external_keypairs)
else: else:
task = partial(self.wallet.sign_transaction, tx, password) task = partial(self.wallet.sign_transaction, tx, password)
msg = _('Signing transaction...') msg = _('Signing transaction...')
WaitingDialog(self, msg, task, on_success, on_failure) 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(): def broadcast_thread():
# non-GUI thread # non-GUI thread
@ -1879,7 +1884,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if pr: if pr:
self.payment_request = None self.payment_request = None
refund_address = self.wallet.get_receiving_address() 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) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
ack_status, ack_msg = fut.result(timeout=20) ack_status, ack_msg = fut.result(timeout=20)
self.logger.info(f"Payment ACK: {ack_status}. Ack message: {ack_msg}") 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.utxo_list.update()
self.update_fee() 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.wallet.set_frozen_state_of_coins(utxos, freeze)
self.utxo_list.update() self.utxo_list.update()
self.update_fee() self.update_fee()
@ -2124,7 +2129,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
else: else:
return self.wallet.get_spendable_coins(None) 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_pay_from(coins)
self.set_onchain(len(coins) > 0) self.set_onchain(len(coins) > 0)
self.show_send_tab() self.show_send_tab()
@ -2527,7 +2532,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not address: if not address:
return return
try: try:
pk, redeem_script = self.wallet.export_private_key(address, password) pk = self.wallet.export_private_key(address, password)
except Exception as e: except Exception as e:
self.logger.exception('') self.logger.exception('')
self.show_message(repr(e)) self.show_message(repr(e))
@ -2542,11 +2547,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
keys_e = ShowQRTextEdit(text=pk) keys_e = ShowQRTextEdit(text=pk)
keys_e.addCopyButton(self.app) keys_e.addCopyButton(self.app)
vbox.addWidget(keys_e) vbox.addWidget(keys_e)
if redeem_script: # if redeem_script:
vbox.addWidget(QLabel(_("Redeem Script") + ':')) # vbox.addWidget(QLabel(_("Redeem Script") + ':'))
rds_e = ShowQRTextEdit(text=redeem_script) # rds_e = ShowQRTextEdit(text=redeem_script)
rds_e.addCopyButton(self.app) # rds_e.addCopyButton(self.app)
vbox.addWidget(rds_e) # vbox.addWidget(rds_e)
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))
d.setLayout(vbox) d.setLayout(vbox)
d.exec_() d.exec_()
@ -2718,11 +2723,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
d = PasswordDialog(parent, msg) d = PasswordDialog(parent, msg)
return d.run() return d.run()
def tx_from_text(self, txt) -> Optional[Transaction]: def tx_from_text(self, txt: Union[str, bytes]) -> Union[None, 'PartialTransaction', 'Transaction']:
from electrum.transaction import tx_from_str from electrum.transaction import tx_from_any
try: try:
tx = tx_from_str(txt) return tx_from_any(txt)
return Transaction(tx)
except BaseException as e: except BaseException as e:
self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e)) self.show_critical(_("Electrum was unable to parse your transaction") + ":\n" + repr(e))
return return
@ -2752,14 +2756,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_transaction(tx) self.show_transaction(tx)
def read_tx_from_file(self) -> Optional[Transaction]: 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: if not fileName:
return return
try: try:
with open(fileName, "r") as f: 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: 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
return self.tx_from_text(file_content) return self.tx_from_text(file_content)
@ -2831,7 +2836,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
time.sleep(0.1) time.sleep(0.1)
if done or cancelled: if done or cancelled:
break break
privkey = self.wallet.export_private_key(addr, password)[0] privkey = self.wallet.export_private_key(addr, password)
private_keys[addr] = privkey private_keys[addr] = privkey
self.computing_privkeys_signal.emit() self.computing_privkeys_signal.emit()
if not cancelled: if not cancelled:
@ -3130,7 +3135,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addLayout(Buttons(CloseButton(d))) vbox.addLayout(Buttons(CloseButton(d)))
d.exec_() 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() total_size = parent_tx.estimated_size() + new_tx.estimated_size()
parent_txid = parent_tx.txid() parent_txid = parent_tx.txid()
assert parent_txid assert parent_txid
@ -3257,7 +3262,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
new_tx.set_rbf(False) new_tx.set_rbf(False)
self.show_transaction(new_tx, tx_desc=tx_label) 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() win = self.top_level_window()
try: try:
if not self.wallet.add_transaction(tx.txid(), tx): if not self.wallet.add_transaction(tx.txid(), tx):

40
electrum/gui/qt/paytoedit.py

@ -25,13 +25,13 @@
import re import re
from decimal import Decimal from decimal import Decimal
from typing import NamedTuple, Sequence from typing import NamedTuple, Sequence, Optional, List
from PyQt5.QtGui import QFontMetrics from PyQt5.QtGui import QFontMetrics
from electrum import bitcoin from electrum import bitcoin
from electrum.util import bfh 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.bitcoin import opcodes
from electrum.logging import Logger from electrum.logging import Logger
from electrum.lnaddr import LnDecodeException from electrum.lnaddr import LnDecodeException
@ -65,12 +65,12 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.heightMax = 150 self.heightMax = 150
self.c = None self.c = None
self.textChanged.connect(self.check_text) self.textChanged.connect(self.check_text)
self.outputs = [] self.outputs = [] # type: List[PartialTxOutput]
self.errors = [] # type: Sequence[PayToLineError] self.errors = [] # type: Sequence[PayToLineError]
self.is_pr = False self.is_pr = False
self.is_alias = False self.is_alias = False
self.update_size() self.update_size()
self.payto_address = None self.payto_scriptpubkey = None # type: Optional[bytes]
self.lightning_invoice = None self.lightning_invoice = None
self.previous_payto = '' self.previous_payto = ''
@ -86,19 +86,19 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def setExpired(self): def setExpired(self):
self.setStyleSheet(util.ColorScheme.RED.as_stylesheet(True)) 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(',') x, y = line.split(',')
out_type, out = self.parse_output(x) scriptpubkey = self.parse_output(x)
amount = self.parse_amount(y) 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: try:
address = self.parse_address(x) address = self.parse_address(x)
return bitcoin.TYPE_ADDRESS, address return bfh(bitcoin.address_to_script(address))
except: except:
script = self.parse_script(x) script = self.parse_script(x)
return bitcoin.TYPE_SCRIPT, script return bfh(script)
def parse_script(self, x): def parse_script(self, x):
script = '' script = ''
@ -131,9 +131,9 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
return return
# filter out empty lines # filter out empty lines
lines = [i for i in self.lines() if i] lines = [i for i in self.lines() if i]
outputs = [] outputs = [] # type: List[PartialTxOutput]
total = 0 total = 0
self.payto_address = None self.payto_scriptpubkey = None
self.lightning_invoice = None self.lightning_invoice = None
if len(lines) == 1: if len(lines) == 1:
data = lines[0] data = lines[0]
@ -152,10 +152,10 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.lightning_invoice = lower self.lightning_invoice = lower
return return
try: try:
self.payto_address = self.parse_output(data) self.payto_scriptpubkey = self.parse_output(data)
except: except:
pass pass
if self.payto_address: if self.payto_scriptpubkey:
self.win.set_onchain(True) self.win.set_onchain(True)
self.win.lock_amount(False) self.win.lock_amount(False)
return return
@ -177,7 +177,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.win.max_button.setChecked(is_max) self.win.max_button.setChecked(is_max)
self.outputs = outputs self.outputs = outputs
self.payto_address = None self.payto_scriptpubkey = None
if self.win.max_button.isChecked(): if self.win.max_button.isChecked():
self.win.do_update_fee() self.win.do_update_fee()
@ -188,18 +188,16 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
def get_errors(self) -> Sequence[PayToLineError]: def get_errors(self) -> Sequence[PayToLineError]:
return self.errors return self.errors
def get_recipient(self): def get_destination_scriptpubkey(self) -> Optional[bytes]:
return self.payto_address return self.payto_scriptpubkey
def get_outputs(self, is_max): def get_outputs(self, is_max):
if self.payto_address: if self.payto_scriptpubkey:
if is_max: if is_max:
amount = '!' amount = '!'
else: else:
amount = self.amount_edit.get_amount() amount = self.amount_edit.get_amount()
self.outputs = [PartialTxOutput(scriptpubkey=self.payto_scriptpubkey, value=amount)]
_type, addr = self.payto_address
self.outputs = [TxOutput(_type, addr, amount)]
return self.outputs[:] return self.outputs[:]

84
electrum/gui/qt/transaction_dialog.py

@ -26,8 +26,8 @@
import sys import sys
import copy import copy
import datetime import datetime
import json
import traceback import traceback
import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import QSize, Qt from PyQt5.QtCore import QSize, Qt
@ -42,11 +42,11 @@ from electrum.i18n import _
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum import simple_config from electrum import simple_config
from electrum.util import bfh 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 electrum.logging import get_logger
from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton, from .util import (MessageBoxMixin, read_QIcon, Buttons, CopyButton,
MONOSPACE_FONT, ColorScheme, ButtonsLineEdit) MONOSPACE_FONT, ColorScheme, ButtonsLineEdit, text_dialog)
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
@ -60,9 +60,9 @@ _logger = get_logger(__name__)
dialogs = [] # Otherwise python randomly garbage collects the dialogs... 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: 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: except SerializationError as e:
_logger.exception('unable to deserialize the transaction') _logger.exception('unable to deserialize the transaction')
parent.show_critical(_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)) 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): 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. '''Transactions in the wallet will show their description.
Pass desc to give a description for txs not yet in the wallet. 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. # 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, # as a result, e.g. we might learn an imported address tx is segwit,
# in which case it's ok to display txid # 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.setMinimumWidth(950)
self.setWindowTitle(_("Transaction")) self.setWindowTitle(_("Transaction"))
@ -123,6 +123,9 @@ class TxDialog(QDialog, MessageBoxMixin):
self.broadcast_button = b = QPushButton(_("Broadcast")) self.broadcast_button = b = QPushButton(_("Broadcast"))
b.clicked.connect(self.do_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")) self.save_button = b = QPushButton(_("Save"))
save_button_disabled = not tx.is_complete() save_button_disabled = not tx.is_complete()
b.setDisabled(save_button_disabled) b.setDisabled(save_button_disabled)
@ -152,7 +155,10 @@ class TxDialog(QDialog, MessageBoxMixin):
self.export_actions_button.setPopupMode(QToolButton.InstantPopup) self.export_actions_button.setPopupMode(QToolButton.InstantPopup)
# Action buttons # 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 # Transaction sharing buttons
self.sharing_buttons = [self.export_actions_button, self.save_button] self.sharing_buttons = [self.export_actions_button, self.save_button]
@ -190,7 +196,7 @@ class TxDialog(QDialog, MessageBoxMixin):
self.close() self.close()
def show_qr(self): def show_qr(self):
text = bfh(str(self.tx)) text = self.tx.serialize_as_bytes()
text = base_encode(text, base=43) text = base_encode(text, base=43)
try: try:
self.main_window.show_qrcode(text, 'Transaction', parent=self) self.main_window.show_qrcode(text, 'Transaction', parent=self)
@ -222,15 +228,43 @@ class TxDialog(QDialog, MessageBoxMixin):
self.saved = True self.saved = True
self.main_window.pop_top_level_window(self) self.main_window.pop_top_level_window(self)
def export(self): def export(self):
name = 'signed_%s.txn' % (self.tx.txid()[0:8]) if self.tx.is_complete() else 'unsigned.txn' if isinstance(self.tx, PartialTransaction):
fileName = self.main_window.getSaveFileName(_("Select where to save your signed transaction"), name, "*.txn") self.tx.finalize_psbt()
if fileName: 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: with open(fileName, "w+") as f:
f.write(json.dumps(self.tx.as_dict(), indent=4) + '\n') network_tx_hex = self.tx.serialize_to_network()
self.show_message(_("Transaction exported successfully")) f.write(network_tx_hex + '\n')
self.saved = True 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): def update(self):
desc = self.desc desc = self.desc
@ -319,19 +353,19 @@ class TxDialog(QDialog, MessageBoxMixin):
i_text.setFont(QFont(MONOSPACE_FONT)) i_text.setFont(QFont(MONOSPACE_FONT))
i_text.setReadOnly(True) i_text.setReadOnly(True)
cursor = i_text.textCursor() cursor = i_text.textCursor()
for x in self.tx.inputs(): for txin in self.tx.inputs():
if x['type'] == 'coinbase': if txin.is_coinbase():
cursor.insertText('coinbase') cursor.insertText('coinbase')
else: else:
prevout_hash = x.get('prevout_hash') prevout_hash = txin.prevout.txid.hex()
prevout_n = x.get('prevout_n') prevout_n = txin.prevout.out_idx
cursor.insertText(prevout_hash + ":%-4d " % prevout_n, ext) 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: if addr is None:
addr = '' addr = ''
cursor.insertText(addr, text_format(addr)) cursor.insertText(addr, text_format(addr))
if x.get('value'): if isinstance(txin, PartialTxInput) and txin.value_sats() is not None:
cursor.insertText(format_amount(x['value']), ext) cursor.insertText(format_amount(txin.value_sats()), ext)
cursor.insertBlock() cursor.insertBlock()
vbox.addWidget(i_text) vbox.addWidget(i_text)
@ -340,8 +374,8 @@ class TxDialog(QDialog, MessageBoxMixin):
o_text.setFont(QFont(MONOSPACE_FONT)) o_text.setFont(QFont(MONOSPACE_FONT))
o_text.setReadOnly(True) o_text.setReadOnly(True)
cursor = o_text.textCursor() cursor = o_text.textCursor()
for o in self.tx.get_outputs_for_UI(): for o in self.tx.outputs():
addr, v = o.address, o.value addr, v = o.get_ui_address_str(), o.value
cursor.insertText(addr, text_format(addr)) cursor.insertText(addr, text_format(addr))
if v is not None: if v is not None:
cursor.insertText('\t', ext) 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): def get_parent_main_window(widget):
"""Returns a reference to the ElectrumWindow this widget belongs to.""" """Returns a reference to the ElectrumWindow this widget belongs to."""
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
from .transaction_dialog import TxDialog
for _ in range(100): for _ in range(100):
if widget is None: if widget is None:
return None return None
if not isinstance(widget, ElectrumWindow): if isinstance(widget, ElectrumWindow):
widget = widget.parentWidget()
else:
return widget return widget
elif isinstance(widget, TxDialog):
return widget.main_window
else:
widget = widget.parentWidget()
return None 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 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from typing import Optional, List from typing import Optional, List, Dict
from enum import IntEnum from enum import IntEnum
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -31,9 +31,11 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
from PyQt5.QtWidgets import QAbstractItemView, QMenu from PyQt5.QtWidgets import QAbstractItemView, QMenu
from electrum.i18n import _ from electrum.i18n import _
from electrum.transaction import PartialTxInput
from .util import MyTreeView, ColorScheme, MONOSPACE_FONT from .util import MyTreeView, ColorScheme, MONOSPACE_FONT
class UTXOList(MyTreeView): class UTXOList(MyTreeView):
class Columns(IntEnum): class Columns(IntEnum):
@ -64,21 +66,21 @@ class UTXOList(MyTreeView):
def update(self): def update(self):
self.wallet = self.parent.wallet self.wallet = self.parent.wallet
utxos = self.wallet.get_utxos() utxos = self.wallet.get_utxos()
self.utxo_dict = {} self.utxo_dict = {} # type: Dict[str, PartialTxInput]
self.model().clear() self.model().clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for idx, x in enumerate(utxos): for idx, utxo in enumerate(utxos):
self.insert_utxo(idx, x) self.insert_utxo(idx, utxo)
self.filter() self.filter()
def insert_utxo(self, idx, x): def insert_utxo(self, idx, utxo: PartialTxInput):
address = x['address'] address = utxo.address
height = x.get('height') height = utxo.block_height
name = x.get('prevout_hash') + ":%d"%x.get('prevout_n') name = utxo.prevout.to_str()
name_short = x.get('prevout_hash')[:16] + '...' + ":%d"%x.get('prevout_n') name_short = utxo.prevout.txid.hex()[:16] + '...' + ":%d" % utxo.prevout.out_idx
self.utxo_dict[name] = x self.utxo_dict[name] = utxo
label = self.wallet.get_label(x.get('prevout_hash')) label = self.wallet.get_label(utxo.prevout.txid.hex())
amount = self.parent.format_amount(x['value'], whitespaces=True) amount = self.parent.format_amount(utxo.value_sats(), whitespaces=True)
labels = [name_short, address, label, amount, '%d'%height] labels = [name_short, address, label, amount, '%d'%height]
utxo_item = [QStandardItem(x) for x in labels] utxo_item = [QStandardItem(x) for x in labels]
self.set_editability(utxo_item) self.set_editability(utxo_item)
@ -89,7 +91,7 @@ class UTXOList(MyTreeView):
if self.wallet.is_frozen_address(address): if self.wallet.is_frozen_address(address):
utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True)) utxo_item[self.Columns.ADDRESS].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.ADDRESS].setToolTip(_('Address is frozen')) 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].setBackground(ColorScheme.BLUE.as_color(True))
utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}") utxo_item[self.Columns.OUTPOINT].setToolTip(f"{name}\n{_('Coin is frozen')}")
else: else:
@ -114,26 +116,26 @@ class UTXOList(MyTreeView):
menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins)) menu.addAction(_("Spend"), lambda: self.parent.spend_coins(coins))
assert len(coins) >= 1, len(coins) assert len(coins) >= 1, len(coins)
if len(coins) == 1: if len(coins) == 1:
utxo_dict = coins[0] utxo = coins[0]
addr = utxo_dict['address'] addr = utxo.address
txid = utxo_dict['prevout_hash'] txid = utxo.prevout.txid.hex()
# "Details" # "Details"
tx = self.wallet.db.get_transaction(txid) tx = self.wallet.db.get_transaction(txid)
if tx: if tx:
label = self.wallet.get_label(txid) or None # Prefer None if empty (None hides the Description: field in the window) 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 ..." # "Copy ..."
idx = self.indexAt(position) idx = self.indexAt(position)
if not idx.isValid(): if not idx.isValid():
return return
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
# "Freeze coin" # "Freeze coin"
if not self.wallet.is_frozen_coin(utxo_dict): if not self.wallet.is_frozen_coin(utxo):
menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo_dict], True)) menu.addAction(_("Freeze Coin"), lambda: self.parent.set_frozen_state_of_coins([utxo], True))
else: else:
menu.addSeparator() menu.addSeparator()
menu.addAction(_("Coin is frozen"), lambda: None).setEnabled(False) 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() menu.addSeparator()
# "Freeze address" # "Freeze address"
if not self.wallet.is_frozen_address(addr): if not self.wallet.is_frozen_address(addr):
@ -146,9 +148,9 @@ class UTXOList(MyTreeView):
else: else:
# multiple items selected # multiple items selected
menu.addSeparator() menu.addSeparator()
addrs = [utxo_dict['address'] for utxo_dict in coins] addrs = [utxo.address for utxo in coins]
is_coin_frozen = [self.wallet.is_frozen_coin(utxo_dict) for utxo_dict in coins] is_coin_frozen = [self.wallet.is_frozen_coin(utxo) for utxo in coins]
is_addr_frozen = [self.wallet.is_frozen_address(utxo_dict['address']) for utxo_dict in coins] is_addr_frozen = [self.wallet.is_frozen_address(utxo.address) for utxo in coins]
if not all(is_coin_frozen): if not all(is_coin_frozen):
menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True)) menu.addAction(_("Freeze Coins"), lambda: self.parent.set_frozen_state_of_coins(coins, True))
if any(is_coin_frozen): if any(is_coin_frozen):

9
electrum/gui/stdio.py

@ -5,8 +5,8 @@ import logging
from electrum import WalletStorage, Wallet from electrum import WalletStorage, Wallet
from electrum.util import format_satoshis from electrum.util import format_satoshis
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS from electrum.bitcoin import is_address, COIN
from electrum.transaction import TxOutput from electrum.transaction import PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed from electrum.network import TxBroadcastError, BestEffortRequestFailed
from electrum.logging import console_stderr_handler from electrum.logging import console_stderr_handler
@ -197,8 +197,9 @@ class ElectrumGui:
if c == "n": return if c == "n": return
try: try:
tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
password, self.config, fee) password=password,
fee=fee)
except Exception as e: except Exception as e:
print(repr(e)) print(repr(e))
return return

9
electrum/gui/text.py

@ -9,8 +9,8 @@ import logging
import electrum import electrum
from electrum.util import format_satoshis from electrum.util import format_satoshis
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS from electrum.bitcoin import is_address, COIN
from electrum.transaction import TxOutput from electrum.transaction import PartialTxOutput
from electrum.wallet import Wallet from electrum.wallet import Wallet
from electrum.storage import WalletStorage from electrum.storage import WalletStorage
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
@ -360,8 +360,9 @@ class ElectrumGui:
else: else:
password = None password = None
try: try:
tx = self.wallet.mktx([TxOutput(TYPE_ADDRESS, self.str_recipient, amount)], tx = self.wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(self.str_recipient, amount)],
password, self.config, fee) password=password,
fee=fee)
except Exception as e: except Exception as e:
self.show_message(repr(e)) self.show_message(repr(e))
return return

61
electrum/json_db.py

@ -28,7 +28,7 @@ import json
import copy import copy
import threading import threading
from collections import defaultdict 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 . import util, bitcoin
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo from .util import profiler, WalletFileException, multisig_type, TxMinedInfo
@ -40,15 +40,11 @@ from .logging import Logger
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # 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 # old versions from overwriting new format
class JsonDBJsonEncoder(util.MyEncoder): JsonDBJsonEncoder = util.MyEncoder
def default(self, obj):
if isinstance(obj, Transaction):
return str(obj)
return super().default(obj)
class TxFeesValue(NamedTuple): class TxFeesValue(NamedTuple):
@ -217,6 +213,7 @@ class JsonDB(Logger):
self._convert_version_17() self._convert_version_17()
self._convert_version_18() self._convert_version_18()
self._convert_version_19() self._convert_version_19()
self._convert_version_20()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks() self._after_upgrade_tasks()
@ -425,10 +422,10 @@ class JsonDB(Logger):
for txid, raw_tx in transactions.items(): for txid, raw_tx in transactions.items():
tx = Transaction(raw_tx) tx = Transaction(raw_tx)
for txin in tx.inputs(): for txin in tx.inputs():
if txin['type'] == 'coinbase': if txin.is_coinbase():
continue continue
prevout_hash = txin['prevout_hash'] prevout_hash = txin.prevout.txid.hex()
prevout_n = txin['prevout_n'] prevout_n = txin.prevout.out_idx
spent_outpoints[prevout_hash][str(prevout_n)] = txid spent_outpoints[prevout_hash][str(prevout_n)] = txid
self.put('spent_outpoints', spent_outpoints) self.put('spent_outpoints', spent_outpoints)
@ -448,10 +445,34 @@ class JsonDB(Logger):
self.put('tx_fees', None) self.put('tx_fees', None)
self.put('seed_version', 19) self.put('seed_version', 19)
# def _convert_version_20(self): def _convert_version_20(self):
# TODO for "next" upgrade: # store 'derivation' (prefix) and 'root_fingerprint' in all xpub-based keystores
# - move "pw_hash_version" from keystore to storage if not self._is_upgrade_method_needed(19, 19):
# pass 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): def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
@ -758,16 +779,16 @@ class JsonDB(Logger):
@modifier @modifier
def add_change_address(self, addr): 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) self.change_addresses.append(addr)
@modifier @modifier
def add_receiving_address(self, addr): 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) self.receiving_addresses.append(addr)
@locked @locked
def get_address_index(self, address): def get_address_index(self, address) -> Optional[Sequence[int]]:
return self._addr_to_addr_index.get(address) return self._addr_to_addr_index.get(address)
@modifier @modifier
@ -801,11 +822,11 @@ class JsonDB(Logger):
self.data['addresses'][name] = [] self.data['addresses'][name] = []
self.change_addresses = self.data['addresses']['change'] self.change_addresses = self.data['addresses']['change']
self.receiving_addresses = self.data['addresses']['receiving'] 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): 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): 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 @profiler
def _load_transactions(self): def _load_transactions(self):

280
electrum/keystore.py

@ -26,16 +26,15 @@
from unicodedata import normalize from unicodedata import normalize
import hashlib 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 . import bitcoin, ecc, constants, bip32
from .bitcoin import (deserialize_privkey, serialize_privkey, from .bitcoin import deserialize_privkey, serialize_privkey
public_key_to_p2pkh)
from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, 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 .ecc import string_to_number, number_to_string
from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, 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, from .util import (InvalidPassword, WalletFileException,
BitcoinException, bh2u, bfh, inv_dict) BitcoinException, bh2u, bfh, inv_dict)
from .mnemonic import Mnemonic, load_wordlist, seed_type, is_seed from .mnemonic import Mnemonic, load_wordlist, seed_type, is_seed
@ -43,7 +42,7 @@ from .plugin import run_hook
from .logging import Logger from .logging import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .transaction import Transaction from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
class KeyStore(Logger): class KeyStore(Logger):
@ -67,25 +66,19 @@ class KeyStore(Logger):
"""Returns whether the keystore can be encrypted with a password.""" """Returns whether the keystore can be encrypted with a password."""
raise NotImplementedError() raise NotImplementedError()
def get_tx_derivations(self, tx): def get_tx_derivations(self, tx: 'PartialTransaction') -> Dict[str, Union[Sequence[int], str]]:
keypairs = {} keypairs = {}
for txin in tx.inputs(): for txin in tx.inputs():
num_sig = txin.get('num_sig') if txin.is_complete():
if num_sig is None:
continue continue
x_signatures = txin['signatures'] for pubkey in txin.pubkeys:
signatures = [sig for sig in x_signatures if sig] if pubkey in txin.part_sigs:
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:
# this pubkey already signed # this pubkey already signed
continue continue
derivation = self.get_pubkey_derivation(x_pubkey) derivation = self.get_pubkey_derivation(pubkey, txin)
if not derivation: if not derivation:
continue continue
keypairs[x_pubkey] = derivation keypairs[pubkey.hex()] = derivation
return keypairs return keypairs
def can_sign(self, tx): def can_sign(self, tx):
@ -108,9 +101,64 @@ class KeyStore(Logger):
def decrypt_message(self, sequence, message, password) -> bytes: def decrypt_message(self, sequence, message, password) -> bytes:
raise NotImplementedError() # implemented by subclasses 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 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): class Software_KeyStore(KeyStore):
@ -210,14 +258,10 @@ class Imported_KeyStore(Software_KeyStore):
raise InvalidPassword() raise InvalidPassword()
return privkey, compressed return privkey, compressed
def get_pubkey_derivation(self, x_pubkey): def get_pubkey_derivation(self, pubkey, txin, *, only_der_suffix=True):
if x_pubkey[0:2] in ['02', '03', '04']: if pubkey.hex() in self.keypairs:
if x_pubkey in self.keypairs.keys(): return pubkey.hex()
return x_pubkey return None
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 update_password(self, old_password, new_password): def update_password(self, old_password, new_password):
self.check_password(old_password) self.check_password(old_password)
@ -230,7 +274,6 @@ class Imported_KeyStore(Software_KeyStore):
self.pw_hash_version = PW_HASH_VERSION_LATEST self.pw_hash_version = PW_HASH_VERSION_LATEST
class Deterministic_KeyStore(Software_KeyStore): class Deterministic_KeyStore(Software_KeyStore):
def __init__(self, d): def __init__(self, d):
@ -277,15 +320,54 @@ class Deterministic_KeyStore(Software_KeyStore):
class Xpub: class Xpub:
def __init__(self): def __init__(self, *, derivation_prefix: str = None, root_fingerprint: str = None):
self.xpub = None self.xpub = None
self.xpub_receive = None self.xpub_receive = None
self.xpub_change = 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): def get_master_public_key(self):
return self.xpub 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 xpub = self.xpub_change if for_change else self.xpub_receive
if xpub is None: if xpub is None:
rootnode = BIP32Node.from_xkey(self.xpub) rootnode = BIP32Node.from_xkey(self.xpub)
@ -301,54 +383,13 @@ class Xpub:
node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence) node = BIP32Node.from_xkey(xpub).subkey_at_public_derivation(sequence)
return node.eckey.get_public_key_hex(compressed=True) 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): class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
type = 'bip32' type = 'bip32'
def __init__(self, d): 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) Deterministic_KeyStore.__init__(self, d)
self.xpub = d.get('xpub') self.xpub = d.get('xpub')
self.xprv = d.get('xprv') self.xprv = d.get('xprv')
@ -360,6 +401,8 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
d = Deterministic_KeyStore.dump(self) d = Deterministic_KeyStore.dump(self)
d['xpub'] = self.xpub d['xpub'] = self.xpub
d['xprv'] = self.xprv d['xprv'] = self.xprv
d['derivation'] = self.get_derivation_prefix()
d['root_fingerprint'] = self.get_root_fingerprint()
return d return d
def get_master_private_key(self, password): def get_master_private_key(self, password):
@ -388,14 +431,20 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
def is_watching_only(self): def is_watching_only(self):
return self.xprv is None 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.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): def add_xprv_from_seed(self, bip32_seed, xtype, derivation):
rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype) rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype)
node = rootnode.subkey_at_private_derivation(derivation) 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): def get_private_key(self, sequence, password):
xprv = self.get_master_private_key(password) xprv = self.get_master_private_key(password)
@ -415,6 +464,7 @@ class Old_KeyStore(Deterministic_KeyStore):
def __init__(self, d): def __init__(self, d):
Deterministic_KeyStore.__init__(self, d) Deterministic_KeyStore.__init__(self, d)
self.mpk = d.get('mpk') self.mpk = d.get('mpk')
self._root_fingerprint = None
def get_hex_seed(self, password): def get_hex_seed(self, password):
return pw_decode(self.seed, password, version=self.pw_hash_version).encode('utf8') 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() public_key = master_public_key + z*ecc.generator()
return public_key.get_public_key_hex(compressed=False) 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) return self.get_pubkey_from_mpk(self.mpk, for_change, n)
def get_private_key_from_stretched_exponent(self, for_change, n, secexp): 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): def get_master_public_key(self):
return self.mpk return self.mpk
def get_xpubkey(self, for_change, n): def get_derivation_prefix(self) -> str:
s = ''.join(map(lambda x: bitcoin.int_to_hex(x,2), (for_change, n))) return 'm'
return 'fe' + self.mpk + s
@classmethod def get_root_fingerprint(self) -> str:
def parse_xpubkey(self, x_pubkey): if self._root_fingerprint is None:
assert x_pubkey[0:2] == 'fe' master_public_key = ecc.ECPubkey(bfh('04'+self.mpk))
pk = x_pubkey[2:] xfp = hash_160(master_public_key.get_public_key_bytes(compressed=True))[0:4]
mpk = pk[0:128] self._root_fingerprint = xfp.hex().lower()
dd = pk[128:] return self._root_fingerprint
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 update_password(self, old_password, new_password): def update_password(self, old_password, new_password):
self.check_password(old_password) self.check_password(old_password)
@ -554,14 +588,13 @@ class Hardware_KeyStore(KeyStore, Xpub):
type = 'hardware' type = 'hardware'
def __init__(self, d): def __init__(self, d):
Xpub.__init__(self) Xpub.__init__(self, derivation_prefix=d.get('derivation'), root_fingerprint=d.get('root_fingerprint'))
KeyStore.__init__(self) KeyStore.__init__(self)
# Errors and other user interaction is done through the wallet's # Errors and other user interaction is done through the wallet's
# handler. The handler is per-window and preserved across # handler. The handler is per-window and preserved across
# device reconnects # device reconnects
self.xpub = d.get('xpub') self.xpub = d.get('xpub')
self.label = d.get('label') self.label = d.get('label')
self.derivation = d.get('derivation')
self.handler = None self.handler = None
run_hook('init_keystore', self) run_hook('init_keystore', self)
@ -582,7 +615,8 @@ class Hardware_KeyStore(KeyStore, Xpub):
'type': self.type, 'type': self.type,
'hw_type': self.hw_type, 'hw_type': self.hw_type,
'xpub': self.xpub, 'xpub': self.xpub,
'derivation':self.derivation, 'derivation': self.get_derivation_prefix(),
'root_fingerprint': self.get_root_fingerprint(),
'label':self.label, 'label':self.label,
} }
@ -704,40 +738,6 @@ def xtype_from_derivation(derivation: str) -> str:
return 'standard' 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 = {} hw_keystores = {}
def register_keystore(hw_type, constructor): def register_keystore(hw_type, constructor):
@ -861,14 +861,12 @@ def from_old_mpk(mpk):
def from_xpub(xpub): def from_xpub(xpub):
k = BIP32_KeyStore({}) k = BIP32_KeyStore({})
k.xpub = xpub k.add_xpub(xpub)
return k return k
def from_xprv(xprv): def from_xprv(xprv):
xpub = bip32.xpub_from_xprv(xprv)
k = BIP32_KeyStore({}) k = BIP32_KeyStore({})
k.xprv = xprv k.add_xprv(xprv)
k.xpub = xpub
return k return k
def from_master_key(text): def from_master_key(text):

48
electrum/lnchannel.py

@ -32,10 +32,9 @@ import time
from . import ecc from . import ecc
from .util import bfh, bh2u from .util import bfh, bh2u
from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
from .bitcoin import redeem_script_to_address from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d from .crypto import sha256, sha256d
from .transaction import Transaction from .transaction import Transaction, PartialTransaction
from .logging import Logger from .logging import Logger
from .lnonion import decode_onion_error from .lnonion import decode_onion_error
@ -528,19 +527,19 @@ class Channel(Logger):
ctx = self.make_commitment(subject, point, ctn) ctx = self.make_commitment(subject, point, ctn)
return secret, ctx 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) secret, ctx = self.get_secret_and_commitment(subject, ctn)
return ctx return ctx
def get_next_commitment(self, subject: HTLCOwner) -> Transaction: def get_next_commitment(self, subject: HTLCOwner) -> PartialTransaction:
ctn = self.get_next_ctn(subject) ctn = self.get_next_ctn(subject)
return self.get_commitment(subject, ctn) 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) ctn = self.get_latest_ctn(subject)
return self.get_commitment(subject, ctn) 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) ctn = self.get_oldest_unrevoked_ctn(subject)
return self.get_commitment(subject, ctn) return self.get_commitment(subject, ctn)
@ -603,7 +602,7 @@ class Channel(Logger):
self.hm.recv_fail(htlc_id) self.hm.recv_fail(htlc_id)
def pending_local_fee(self): 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): def update_fee(self, feerate: int, from_us: bool):
# feerate uses sat/kw # feerate uses sat/kw
@ -658,7 +657,7 @@ class Channel(Logger):
def __str__(self): def __str__(self):
return str(self.serialize()) 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 assert type(subject) is HTLCOwner
feerate = self.get_feerate(subject, ctn) feerate = self.get_feerate(subject, ctn)
other = REMOTE if LOCAL == subject else LOCAL other = REMOTE if LOCAL == subject else LOCAL
@ -717,21 +716,20 @@ class Channel(Logger):
onchain_fees, onchain_fees,
htlcs=htlcs) 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, 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 """ """ cooperative close """
_, outputs = make_commitment_outputs({ _, outputs = make_commitment_outputs(
fees_per_participant={
LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0, LOCAL: fee_sat * 1000 if self.constraints.is_initiator else 0,
REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0, REMOTE: fee_sat * 1000 if not self.constraints.is_initiator else 0,
}, },
self.balance(LOCAL), local_amount_msat=self.balance(LOCAL),
self.balance(REMOTE), remote_amount_msat=self.balance(REMOTE),
(TYPE_SCRIPT, bh2u(local_script)), local_script=bh2u(local_script),
(TYPE_SCRIPT, bh2u(remote_script)), remote_script=bh2u(remote_script),
[], self.config[LOCAL].dust_limit_sat) htlcs=[],
dust_limit_sat=self.config[LOCAL].dust_limit_sat)
closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey, closing_tx = make_closing_tx(self.config[LOCAL].multisig_key.pubkey,
self.config[REMOTE].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]) sig = ecc.sig_string_from_der_sig(der_sig[:-1])
return sig, closing_tx return sig, closing_tx
def signature_fits(self, tx): def signature_fits(self, tx: PartialTransaction):
remote_sig = self.config[LOCAL].current_commitment_signature remote_sig = self.config[LOCAL].current_commitment_signature
preimage_hex = tx.serialize_preimage(0) preimage_hex = tx.serialize_preimage(0)
pre_hash = sha256d(bfh(preimage_hex)) msg_hash = sha256d(bfh(preimage_hex))
assert remote_sig 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 return res
def force_close_tx(self): def force_close_tx(self):
tx = self.get_latest_commitment(LOCAL) tx = self.get_latest_commitment(LOCAL)
assert self.signature_fits(tx) 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)}) 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 = self.config[LOCAL].current_commitment_signature
remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01" remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01"
sigs = tx._inputs[0]["signatures"] tx.add_signature_to_txin(txin_idx=0,
none_idx = sigs.index(None) signing_pubkey=self.config[REMOTE].multisig_key.pubkey.hex(),
tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) sig=remote_sig.hex())
assert tx.is_complete() assert tx.is_complete()
return tx return tx

28
electrum/lnpeer.py

@ -11,7 +11,7 @@ import asyncio
import os import os
import time import time
from functools import partial 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 traceback
import sys import sys
from datetime import datetime 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 .ecc import sig_string_from_r_and_s, get_r_and_s_from_sig_string, der_sig_from_sig_string
from . import constants from . import constants
from .util import bh2u, bfh, log_exceptions, list_enabled_bits, ignore_exceptions, chunks, SilentTaskGroup 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 .logging import Logger
from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment, from .lnonion import (new_onion_packet, decode_onion_error, OnionFailureCode, calc_hops_data_for_payment,
process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage, process_onion_packet, OnionPacket, construct_onion_error, OnionRoutingFailureMessage,
@ -48,7 +48,7 @@ from .interface import GracefulDisconnect, NetworkException
from .lnrouter import fee_for_edge_msat from .lnrouter import fee_for_edge_msat
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnworker import LNWorker from .lnworker import LNWorker, LNGossip, LNWallet
from .lnrouter import RouteEdge 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): 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.initialized = asyncio.Event()
self.querying = asyncio.Event() self.querying = asyncio.Event()
self.transport = transport self.transport = transport
@ -483,8 +483,8 @@ class Peer(Logger):
push_msat: int, temp_channel_id: bytes) -> Channel: push_msat: int, temp_channel_id: bytes) -> Channel:
wallet = self.lnworker.wallet wallet = self.lnworker.wallet
# dry run creating funding tx to see if we even have enough funds # 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)], funding_tx_test = wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(wallet.dummy_address(), funding_sat)],
password, nonlocal_only=True) password=password, nonlocal_only=True)
await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT) await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT)
feerate = self.lnworker.current_feerate_per_kw() feerate = self.lnworker.current_feerate_per_kw()
local_config = self.make_local_config(funding_sat, push_msat, LOCAL) local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
@ -563,8 +563,8 @@ class Peer(Logger):
# create funding tx # create funding tx
redeem_script = funding_output_script(local_config, remote_config) redeem_script = funding_output_script(local_config, remote_config)
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
funding_output = TxOutput(bitcoin.TYPE_ADDRESS, funding_address, funding_sat) funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat)
funding_tx = wallet.mktx([funding_output], password, nonlocal_only=True) funding_tx = wallet.mktx(outputs=[funding_output], password=password, nonlocal_only=True)
funding_txid = funding_tx.txid() funding_txid = funding_tx.txid()
funding_index = funding_tx.outputs().index(funding_output) funding_index = funding_tx.outputs().index(funding_output)
# remote commitment transaction # remote commitment transaction
@ -691,7 +691,7 @@ class Peer(Logger):
outp = funding_tx.outputs()[funding_idx] outp = funding_tx.outputs()[funding_idx]
redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL]) redeem_script = funding_output_script(chan.config[REMOTE], chan.config[LOCAL])
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) 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') chan.set_state('DISCONNECTED')
raise Exception('funding outpoint mismatch') raise Exception('funding outpoint mismatch')
@ -1485,11 +1485,13 @@ class Peer(Logger):
break break
# TODO: negotiate better # TODO: negotiate better
our_fee = their_fee our_fee = their_fee
# index of our_sig
i = chan.get_local_index()
# add signatures # 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(txin_idx=0,
closing_tx.add_signature_to_txin(0, 1-i, bh2u(der_sig_from_sig_string(their_sig) + b'\x01')) 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 # broadcast
await self.network.broadcast_transaction(closing_tx) await self.network.broadcast_transaction(closing_tx)
return closing_tx.txid() 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 enum import Enum, auto
from .util import bfh, bh2u 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 . import ecc
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script, 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, 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, get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED, RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction) 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 .simple_config import SimpleConfig
from .logging import get_logger from .logging import get_logger
@ -254,7 +255,7 @@ def create_sweeptxs_for_our_ctx(*, chan: 'Channel', ctx: Transaction,
is_revocation=False, is_revocation=False,
config=chan.lnworker.config) config=chan.lnworker.config)
# side effect # 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, csv_delay=0,
cltv_expiry=htlc_tx.locktime, cltv_expiry=htlc_tx.locktime,
gen_tx=lambda: htlc_tx) 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) gen_tx = create_sweeptx_for_their_revoked_ctx(chan, ctx, per_commitment_secret, chan.sweep_address)
if gen_tx: if gen_tx:
tx = 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, csv_delay=0,
cltv_expiry=0, cltv_expiry=0,
gen_tx=gen_tx) 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)) local_htlc_sig = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
txin = htlc_tx.inputs()[0] txin = htlc_tx.inputs()[0]
witness_program = bfh(Transaction.get_preimage_script(txin)) 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 return witness_script, htlc_tx
def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep_address: str, def create_sweeptx_their_ctx_htlc(ctx: Transaction, witness_script: bytes, sweep_address: str,
preimage: Optional[bytes], output_idx: int, preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool, 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 assert type(cltv_expiry) is int
preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered) preimage = preimage or b'' # preimage is required iff (not is_revocation and htlc is offered)
val = ctx.outputs()[output_idx].value val = ctx.outputs()[output_idx].value
sweep_inputs = [{ prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
'scriptSig': '', txin = PartialTxInput(prevout=prevout)
'type': 'p2wsh', txin._trusted_value_sats = val
'signatures': [], txin.witness_script = witness_script
'num_sig': 0, txin.script_sig = b''
'prevout_n': output_idx, sweep_inputs = [txin]
'prevout_hash': ctx.txid(),
'value': val,
'coinbase': False,
'preimage_script': bh2u(witness_script),
}]
tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation) 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) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee outvalue = val - fee
if outvalue <= dust_threshold(): return None if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry) tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2, locktime=cltv_expiry)
sig = bfh(tx.sign_txin(0, privkey)) sig = bfh(tx.sign_txin(0, privkey))
if not is_revocation: if not is_revocation:
witness = construct_witness([sig, preimage, witness_script]) witness = construct_witness([sig, preimage, witness_script])
else: else:
revocation_pubkey = privkey_to_pubkey(privkey) revocation_pubkey = privkey_to_pubkey(privkey)
witness = construct_witness([sig, revocation_pubkey, witness_script]) witness = construct_witness([sig, revocation_pubkey, witness_script])
tx.inputs()[0]['witness'] = witness tx.inputs()[0].witness = bfh(witness)
assert tx.is_complete() assert tx.is_complete()
return tx return tx
def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int, def create_sweeptx_their_ctx_to_remote(sweep_address: str, ctx: Transaction, output_idx: int,
our_payment_privkey: ecc.ECPrivkey, 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) our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
val = ctx.outputs()[output_idx].value val = ctx.outputs()[output_idx].value
sweep_inputs = [{ prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
'type': 'p2wpkh', txin = PartialTxInput(prevout=prevout)
'x_pubkeys': [our_payment_pubkey], txin._trusted_value_sats = val
'num_sig': 1, txin.script_type = 'p2wpkh'
'prevout_n': output_idx, txin.pubkeys = [bfh(our_payment_pubkey)]
'prevout_hash': ctx.txid(), txin.num_sig = 1
'value': val, sweep_inputs = [txin]
'coinbase': False,
'signatures': [None],
}]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee outvalue = val - fee
if outvalue <= dust_threshold(): return None if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs) sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs)
sweep_tx.set_rbf(True) sweep_tx.set_rbf(True)
sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)}) sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)})
if not sweep_tx.is_complete(): 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, def create_sweeptx_ctx_to_local(*, sweep_address: str, ctx: Transaction, output_idx: int, witness_script: str,
privkey: bytes, is_revocation: bool, config: SimpleConfig, 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 """Create a txn that sweeps the 'to_local' output of a commitment
transaction into our wallet. 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 ^ is_revocation: tells us which ^
""" """
val = ctx.outputs()[output_idx].value val = ctx.outputs()[output_idx].value
sweep_inputs = [{ prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
'scriptSig': '', txin = PartialTxInput(prevout=prevout)
'type': 'p2wsh', txin._trusted_value_sats = val
'signatures': [], txin.script_sig = b''
'num_sig': 0, txin.witness_script = bfh(witness_script)
'prevout_n': output_idx, sweep_inputs = [txin]
'prevout_hash': ctx.txid(),
'value': val,
'coinbase': False,
'preimage_script': witness_script,
}]
if not is_revocation: if not is_revocation:
assert isinstance(to_self_delay, int) 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 tx_size_bytes = 121 # approx size of to_local -> p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee outvalue = val - fee
if outvalue <= dust_threshold(): if outvalue <= dust_threshold():
return None return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) sweep_tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = sweep_tx.sign_txin(0, privkey) sig = sweep_tx.sign_txin(0, privkey)
witness = construct_witness([sig, int(is_revocation), witness_script]) 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 return sweep_tx
def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(*, def create_sweeptx_that_spends_htlctx_that_spends_htlc_in_ctx(*,
htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str, htlc_tx: Transaction, htlctx_witness_script: bytes, sweep_address: str,
privkey: bytes, is_revocation: bool, to_self_delay: int, privkey: bytes, is_revocation: bool, to_self_delay: int,
config: SimpleConfig) -> Optional[Transaction]: config: SimpleConfig) -> Optional[PartialTransaction]:
val = htlc_tx.outputs()[0].value val = htlc_tx.outputs()[0].value
sweep_inputs = [{ prevout = TxOutpoint(txid=bfh(htlc_tx.txid()), out_idx=0)
'scriptSig': '', txin = PartialTxInput(prevout=prevout)
'type': 'p2wsh', txin._trusted_value_sats = val
'signatures': [], txin.script_sig = b''
'num_sig': 0, txin.witness_script = htlctx_witness_script
'prevout_n': 0, sweep_inputs = [txin]
'prevout_hash': htlc_tx.txid(),
'value': val,
'coinbase': False,
'preimage_script': bh2u(htlctx_witness_script),
}]
if not is_revocation: if not is_revocation:
assert isinstance(to_self_delay, int) 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 tx_size_bytes = 200 # TODO
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True) fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee outvalue = val - fee
if outvalue <= dust_threshold(): return None if outvalue <= dust_threshold(): return None
sweep_outputs = [TxOutput(TYPE_ADDRESS, sweep_address, outvalue)] sweep_outputs = [PartialTxOutput.from_address_and_value(sweep_address, outvalue)]
tx = Transaction.from_io(sweep_inputs, sweep_outputs, version=2) tx = PartialTransaction.from_io(sweep_inputs, sweep_outputs, version=2)
sig = bfh(tx.sign_txin(0, privkey)) sig = bfh(tx.sign_txin(0, privkey))
witness = construct_witness([sig, int(is_revocation), htlctx_witness_script]) 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() assert tx.is_complete()
return tx return tx

105
electrum/lnutil.py

@ -10,11 +10,11 @@ import re
from .util import bfh, bh2u, inv_dict from .util import bfh, bh2u, inv_dict
from .crypto import sha256 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 .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
from . import ecc, bitcoin, crypto, transaction from . import ecc, bitcoin, crypto, transaction
from .transaction import opcodes, TxOutput, Transaction from .bitcoin import push_script, redeem_script_to_address, address_to_script
from .bitcoin import push_script, redeem_script_to_address, TYPE_ADDRESS
from . import segwit_addr from . import segwit_addr
from .i18n import _ from .i18n import _
from .lnaddr import lndecode from .lnaddr import lndecode
@ -97,6 +97,7 @@ class ScriptHtlc(NamedTuple):
htlc: 'UpdateAddHtlc' htlc: 'UpdateAddHtlc'
# FIXME duplicate of TxOutpoint in transaction.py??
class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])): class Outpoint(NamedTuple("Outpoint", [('txid', str), ('output_index', int)])):
def to_str(self): def to_str(self):
return "{}:{}".format(self.txid, self.output_index) 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 fee = fee // 1000 * 1000
final_amount_sat = (amount_msat - fee) // 1000 final_amount_sat = (amount_msat - fee) // 1000
assert final_amount_sat > 0, final_amount_sat 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 return script, output
def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes, 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])) 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, 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_txid) is str
assert type(htlc_output_index) is int assert type(htlc_output_index) is int
assert type(amount_msat) is int assert type(amount_msat) is int
assert type(witness_script) is str assert type(witness_script) is str
c_inputs = [{ txin = PartialTxInput(prevout=TxOutpoint(txid=bfh(htlc_output_txid), out_idx=htlc_output_index),
'scriptSig': '', nsequence=0)
'type': 'p2wsh', txin.witness_script = bfh(witness_script)
'signatures': [], txin.script_sig = b''
'num_sig': 0, txin._trusted_value_sats = amount_msat // 1000
'prevout_n': htlc_output_index, c_inputs = [txin]
'prevout_hash': htlc_output_txid,
'value': amount_msat // 1000,
'coinbase': False,
'sequence': 0x0,
'preimage_script': witness_script,
}]
return c_inputs 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 assert type(cltv_expiry) is int
c_outputs = [output] 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 return tx
def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes, 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', def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner',
htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int, 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 amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
for_us = subject == LOCAL for_us = subject == LOCAL
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us) 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 return witness_script_of_htlc_tx_output, htlc_tx
def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes, 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)]) pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
# commitment tx input # commitment tx input
c_input = { prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)
'type': 'p2wsh', c_input = PartialTxInput(prevout=prevout)
'x_pubkeys': pubkeys, c_input.script_type = 'p2wsh'
'signatures': [None, None], c_input.pubkeys = [bfh(pk) for pk in pubkeys]
'num_sig': 2, c_input.num_sig = 2
'prevout_n': funding_pos, c_input._trusted_value_sats = funding_sat
'prevout_hash': funding_txid,
'value': funding_sat,
'coinbase': False,
}
return c_input return c_input
class HTLCOwner(IntFlag): class HTLCOwner(IntFlag):
@ -504,18 +495,18 @@ RECEIVED = Direction.RECEIVED
LOCAL = HTLCOwner.LOCAL LOCAL = HTLCOwner.LOCAL
REMOTE = HTLCOwner.REMOTE REMOTE = HTLCOwner.REMOTE
def make_commitment_outputs(fees_per_participant: Mapping[HTLCOwner, int], local_amount: int, remote_amount: int, def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int,
local_tupl, remote_tupl, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[TxOutput], List[TxOutput]]: local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
to_local_amt = local_amount - fees_per_participant[LOCAL] to_local_amt = local_amount_msat - fees_per_participant[LOCAL]
to_local = TxOutput(*local_tupl, to_local_amt // 1000) to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000)
to_remote_amt = remote_amount - fees_per_participant[REMOTE] to_remote_amt = remote_amount_msat - fees_per_participant[REMOTE]
to_remote = TxOutput(*remote_tupl, to_remote_amt // 1000) to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000)
non_htlc_outputs = [to_local, to_remote] non_htlc_outputs = [to_local, to_remote]
htlc_outputs = [] htlc_outputs = []
for script, htlc in htlcs: for script, htlc in htlcs:
htlc_outputs.append(TxOutput(bitcoin.TYPE_ADDRESS, addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
bitcoin.redeem_script_to_address('p2wsh', bh2u(script)), htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)),
htlc.amount_msat // 1000)) value=htlc.amount_msat // 1000))
# trim outputs # trim outputs
c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_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, delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount, funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, fees_per_participant, 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, c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
funding_pos, funding_txid, funding_sat) funding_pos, funding_txid, funding_sat)
obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint) obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint)
locktime = (0x20 << 24) + (obs & 0xffffff) locktime = (0x20 << 24) + (obs & 0xffffff)
sequence = (0x80 << 24) + (obs >> 24) sequence = (0x80 << 24) + (obs >> 24)
c_input['sequence'] = sequence c_input.nsequence = sequence
c_inputs = [c_input] c_inputs = [c_input]
@ -555,13 +546,19 @@ def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
htlcs = list(htlcs) htlcs = list(htlcs)
htlcs.sort(key=lambda x: x.htlc.cltv_expiry) htlcs.sort(key=lambda x: x.htlc.cltv_expiry)
htlc_outputs, c_outputs_filtered = make_commitment_outputs(fees_per_participant, local_amount, remote_amount, htlc_outputs, c_outputs_filtered = make_commitment_outputs(
(bitcoin.TYPE_ADDRESS, local_address), (bitcoin.TYPE_ADDRESS, remote_address), htlcs, dust_limit_sat) 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) assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)
# create commitment tx # 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 return tx
def make_commitment_output_to_local_witness_script( 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: def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str:
return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey)) return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
def sign_and_get_sig_string(tx, local_config, remote_config): def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config):
pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)}) 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 = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey]
sig = bytes.fromhex(tx.inputs()[0]["signatures"][sig_index])
sig_64 = sig_string_from_der_sig(sig[:-1]) sig_64 = sig_string_from_der_sig(sig[:-1])
return sig_64 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') mask = int.from_bytes(sha256(funder + fundee)[-6:], 'big')
return ctn ^ mask 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: fundee_payment_basepoint: bytes) -> int:
tx.deserialize() tx.deserialize()
locktime = tx.locktime locktime = tx.locktime
sequence = tx.inputs()[txin_index]['sequence'] sequence = tx.inputs()[txin_index].nsequence
obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff) obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint) 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, def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
funding_txid: bytes, funding_pos: int, funding_sat: int, funding_txid: str, funding_pos: int, funding_sat: int,
outputs: List[TxOutput]) -> Transaction: outputs: List[PartialTxOutput]) -> PartialTransaction:
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey, c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
funding_pos, funding_txid, funding_sat) funding_pos, funding_txid, funding_sat)
c_input['sequence'] = 0xFFFF_FFFF c_input.nsequence = 0xFFFF_FFFF
tx = Transaction.from_io([c_input], outputs, locktime=0, version=2) tx = PartialTransaction.from_io([c_input], outputs, locktime=0, version=2)
return tx return tx

6
electrum/lnwatcher.py

@ -77,9 +77,11 @@ class SweepStore(SqlDB):
return set([r[0] for r in c.fetchall()]) return set([r[0] for r in c.fetchall()])
@sql @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 = 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() self.conn.commit()
@sql @sql

2
electrum/lnworker.py

@ -375,7 +375,7 @@ class LNWallet(LNWorker):
for ctn in range(watchtower_ctn + 1, current_ctn): for ctn in range(watchtower_ctn + 1, current_ctn):
sweeptxs = chan.create_sweeptxs(ctn) sweeptxs = chan.create_sweeptxs(ctn)
for tx in sweeptxs: 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'): def start_network(self, network: 'Network'):
self.lnwatcher = LNWatcher(network) self.lnwatcher = LNWatcher(network)

5
electrum/network.py

@ -64,6 +64,7 @@ if TYPE_CHECKING:
from .channel_db import ChannelDB from .channel_db import ChannelDB
from .lnworker import LNGossip from .lnworker import LNGossip
from .lnwatcher import WatchTower from .lnwatcher import WatchTower
from .transaction import Transaction
_logger = get_logger(__name__) _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]) return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
@best_effort_reliable @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: if timeout is None:
timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent) timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent)
try: 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 # note: both 'out' and exception messages are untrusted input from the server
except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError): except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError):
raise # pass-through raise # pass-through

23
electrum/paymentrequest.py

@ -25,7 +25,7 @@
import hashlib import hashlib
import sys import sys
import time import time
from typing import Optional from typing import Optional, List
import asyncio import asyncio
import urllib.parse 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 bh2u, bfh, export_meta, import_meta, make_aiohttp_session
from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
from .crypto import sha256 from .crypto import sha256
from .bitcoin import TYPE_ADDRESS from .bitcoin import address_to_script
from .transaction import TxOutput from .transaction import PartialTxOutput
from .network import Network from .network import Network
from .logging import get_logger, Logger from .logging import get_logger, Logger
@ -128,7 +128,7 @@ class PaymentRequest:
return str(self.raw) return str(self.raw)
def parse(self, r): def parse(self, r):
self.outputs = [] self.outputs = [] # type: List[PartialTxOutput]
if self.error: if self.error:
return return
self.id = bh2u(sha256(r)[0:16]) self.id = bh2u(sha256(r)[0:16])
@ -141,12 +141,12 @@ class PaymentRequest:
self.details = pb2.PaymentDetails() self.details = pb2.PaymentDetails()
self.details.ParseFromString(self.data.serialized_payment_details) self.details.ParseFromString(self.data.serialized_payment_details)
for o in self.details.outputs: for o in self.details.outputs:
type_, addr = transaction.get_address_from_output_script(o.script) addr = transaction.get_address_from_output_script(o.script)
if type_ != TYPE_ADDRESS: if not addr:
# TODO maybe rm restriction but then get_requestor and get_id need changes # TODO maybe rm restriction but then get_requestor and get_id need changes
self.error = "only addresses are allowed as outputs" self.error = "only addresses are allowed as outputs"
return 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.memo = self.details.memo
self.payment_url = self.details.payment_url self.payment_url = self.details.payment_url
@ -252,8 +252,9 @@ class PaymentRequest:
def get_address(self): def get_address(self):
o = self.outputs[0] o = self.outputs[0]
assert o.type == TYPE_ADDRESS addr = o.address
return o.address assert addr
return addr
def get_requestor(self): def get_requestor(self):
return self.requestor if self.requestor else self.get_address() return self.requestor if self.requestor else self.get_address()
@ -278,7 +279,7 @@ class PaymentRequest:
paymnt.merchant_data = pay_det.merchant_data paymnt.merchant_data = pay_det.merchant_data
paymnt.transactions.append(bfh(raw_tx)) paymnt.transactions.append(bfh(raw_tx))
ref_out = paymnt.refund_to.add() 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" paymnt.memo = "Paid using Electrum"
pm = paymnt.SerializeToString() pm = paymnt.SerializeToString()
payurl = urllib.parse.urlparse(pay_det.payment_url) payurl = urllib.parse.urlparse(pay_det.payment_url)
@ -326,7 +327,7 @@ def make_unsigned_request(req):
if amount is None: if amount is None:
amount = 0 amount = 0
memo = req['memo'] memo = req['memo']
script = bfh(Transaction.pay_script(TYPE_ADDRESS, addr)) script = bfh(address_to_script(addr))
outputs = [(script, amount)] outputs = [(script, amount)]
pd = pb2.PaymentDetails() pd = pb2.PaymentDetails()
for script, amount in outputs: for script, amount in outputs:

2
electrum/plugin.py

@ -449,7 +449,7 @@ class DeviceMgr(ThreadJob):
handler.update_status(False) handler.update_status(False)
devices = self.scan_devices() devices = self.scan_devices()
xpub = keystore.xpub xpub = keystore.xpub
derivation = keystore.get_derivation() derivation = keystore.get_derivation_prefix()
client = self.client_by_xpub(plugin, xpub, handler, devices) client = self.client_by_xpub(plugin, xpub, handler, devices)
if client is None and force_pair: if client is None and force_pair:
info = self.select_device(plugin, handler, keystore, devices) 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 from io import BytesIO
import sys import sys
import platform import platform
from typing import TYPE_CHECKING
from PyQt5.QtWidgets import (QComboBox, QGridLayout, QLabel, QPushButton) 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.i18n import _
from electrum.logging import get_logger from electrum.logging import get_logger
if TYPE_CHECKING:
from electrum.gui.qt.transaction_dialog import TxDialog
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -71,12 +75,12 @@ class Plugin(BasePlugin):
return bool(d.exec_()) return bool(d.exec_())
@hook @hook
def transaction_dialog(self, dialog): def transaction_dialog(self, dialog: 'TxDialog'):
b = QPushButton() b = QPushButton()
b.setIcon(read_QIcon("speaker.png")) b.setIcon(read_QIcon("speaker.png"))
def handler(): def handler():
blob = json.dumps(dialog.tx.as_dict()) blob = dialog.tx.serialize()
self._send(parent=dialog, blob=blob) self._send(parent=dialog, blob=blob)
b.clicked.connect(handler) b.clicked.connect(handler)
dialog.sharing_buttons.insert(-1, b) 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. # Coldcard Electrum plugin main code.
# #
# #
from struct import pack, unpack import os, time, io
import os, sys, time, io
import traceback import traceback
from typing import TYPE_CHECKING
import struct
from electrum import bip32
from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import Device, hook from electrum.plugin import Device, hook
from electrum.keystore import Hardware_KeyStore from electrum.keystore import Hardware_KeyStore
from electrum.transaction import Transaction, multisig_script from electrum.transaction import PartialTransaction
from electrum.wallet import Standard_Wallet, Multisig_Wallet from electrum.wallet import Standard_Wallet, Multisig_Wallet, Abstract_Wallet
from electrum.util import bfh, bh2u, versiontuple, UserFacingException from electrum.util import bfh, bh2u, versiontuple, UserFacingException
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
from electrum.logging import get_logger 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 import HW_PluginBase
from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available
from .basic_psbt import BasicPSBT if TYPE_CHECKING:
from .build_psbt import (build_psbt, xfp2str, unpacked_xfp_path, from electrum.keystore import Xpub
merge_sigs_from_psbt, xfp_for_keystore)
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -86,7 +88,7 @@ class CKCCClient:
return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint), return '<CKCCClient: xfp=%s label=%r>' % (xfp2str(self.dev.master_fingerprint),
self.label()) 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) ex = (expected_xfp, expected_xpub)
if self._expected_device == ex: if self._expected_device == ex:
@ -213,7 +215,7 @@ class CKCCClient:
# poll device... if user has approved, will get tuple: (addr, sig) else None # poll device... if user has approved, will get tuple: (addr, sig) else None
return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=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: # Multiple steps to sign:
# - upload binary # - upload binary
# - start signing UX # - start signing UX
@ -242,6 +244,8 @@ class Coldcard_KeyStore(Hardware_KeyStore):
hw_type = 'coldcard' hw_type = 'coldcard'
device = 'Coldcard' device = 'Coldcard'
plugin: 'ColdcardPlugin'
def __init__(self, d): def __init__(self, d):
Hardware_KeyStore.__init__(self, d) Hardware_KeyStore.__init__(self, d)
# Errors and other user interaction is done through the wallet's # 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.force_watching_only = False
self.ux_busy = False self.ux_busy = False
# for multisig I need to know what wallet this keystore is part of # we need to know at least the fingerprint of the master xpub to verify against MiTM
# 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
# - device reports these value during encryption setup process # - device reports these value during encryption setup process
# - full xpub value now optional # - full xpub value now optional
lab = d['label'] lab = d['label']
if hasattr(lab, 'xfp'): self.ckcc_xpub = getattr(lab, 'xpub', None) or d.get('ckcc_xpub', None)
# 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)
def dump(self): def dump(self):
# our additions to the stored data about keystore -- only during creation? # our additions to the stored data about keystore -- only during creation?
d = Hardware_KeyStore.dump(self) d = Hardware_KeyStore.dump(self)
d['ckcc_xfp'] = self.ckcc_xfp
d['ckcc_xpub'] = self.ckcc_xpub d['ckcc_xpub'] = self.ckcc_xpub
return d return d
def get_derivation(self):
return self.derivation
def get_client(self): def get_client(self):
# called when user tries to do something like view address, sign somthing. # called when user tries to do something like view address, sign somthing.
# - not called during probing/setup # - not called during probing/setup
# - will fail if indicated device can't produce the xpub (at derivation) expected # - will fail if indicated device can't produce the xpub (at derivation) expected
rv = self.plugin.get_client(self) rv = self.plugin.get_client(self)
if rv: 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 return rv
@ -332,7 +315,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
return b'' return b''
client = self.get_client() client = self.get_client()
path = self.get_derivation() + ("/%d/%d" % sequence) path = self.get_derivation_prefix() + ("/%d/%d" % sequence)
try: try:
cl = self.get_client() cl = self.get_client()
try: try:
@ -372,28 +355,23 @@ class Coldcard_KeyStore(Hardware_KeyStore):
return b'' return b''
@wrap_busy @wrap_busy
def sign_transaction(self, tx: Transaction, password): def sign_transaction(self, tx, password):
# Build a PSBT in memory, upload it for signing. # Upload PSBT for signing.
# - we can also work offline (without paired device present) # - we can also work offline (without paired device present)
if tx.is_complete(): if tx.is_complete():
return return
assert self.my_wallet, "Not clear which wallet associated with this Coldcard"
client = self.get_client() 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 = tx.serialize_as_bytes()
raw_psbt = build_psbt(tx, self.my_wallet)
cc_finalize = not (type(self.my_wallet) is Multisig_Wallet)
try: try:
try: try:
self.handler.show_message("Authorize Transaction...") self.handler.show_message("Authorize Transaction...")
client.sign_transaction_start(raw_psbt, cc_finalize) client.sign_transaction_start(raw_psbt)
while 1: while 1:
# How to kill some time, without locking UI? # How to kill some time, without locking UI?
@ -420,18 +398,11 @@ class Coldcard_KeyStore(Hardware_KeyStore):
self.give_error(e, True) self.give_error(e, True)
return return
if cc_finalize: tx2 = PartialTransaction.from_raw_psbt(raw_resp)
# We trust the coldcard to re-serialize final transaction ready to go # apply partial signatures back into txn
tx.update(bh2u(raw_resp)) tx.combine_with_other_psbt(tx2)
else: # caller's logic looks at tx now and if it's sufficiently signed,
# apply partial signatures back into txn # will send it if that's the user's intent.
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.
@staticmethod @staticmethod
def _encode_txin_type(txin_type): def _encode_txin_type(txin_type):
@ -447,7 +418,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
@wrap_busy @wrap_busy
def show_address(self, sequence, txin_type): def show_address(self, sequence, txin_type):
client = self.get_client() 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) addr_fmt = self._encode_txin_type(txin_type)
try: try:
try: try:
@ -573,7 +544,7 @@ class ColdcardPlugin(HW_PluginBase):
xpub = client.get_xpub(derivation, xtype) xpub = client.get_xpub(derivation, xtype)
return xpub 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) # Acquire a connection to the hardware device (via USB)
devmgr = self.device_manager() devmgr = self.device_manager()
handler = keystore.handler handler = keystore.handler
@ -586,9 +557,10 @@ class ColdcardPlugin(HW_PluginBase):
return client return client
@staticmethod @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 # Build the text file Coldcard needs to understand the multisig wallet
# it is participating in. All involved Coldcards can share same file. # it is participating in. All involved Coldcards can share same file.
assert isinstance(wallet, Multisig_Wallet)
print('# Exported from Electrum', file=fp) print('# Exported from Electrum', file=fp)
print(f'Name: {name:.20s}', file=fp) print(f'Name: {name:.20s}', file=fp)
@ -597,12 +569,10 @@ class ColdcardPlugin(HW_PluginBase):
xpubs = [] xpubs = []
derivs = set() derivs = set()
for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): for xpub, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()):
xfp = xfp_for_keystore(ks) der_prefix = ks.get_derivation_prefix()
dd = getattr(ks, 'derivation', 'm') xpubs.append( (ks.get_root_fingerprint(), xpub, der_prefix) )
derivs.add(der_prefix)
xpubs.append( (xfp2str(xfp), xp, dd) )
derivs.add(dd)
# Derivation doesn't matter too much to the Coldcard, since it # Derivation doesn't matter too much to the Coldcard, since it
# uses key path data from PSBT or USB request as needed. However, # uses key path data from PSBT or USB request as needed. However,
@ -613,14 +583,14 @@ class ColdcardPlugin(HW_PluginBase):
print('', file=fp) print('', file=fp)
assert len(xpubs) == wallet.n assert len(xpubs) == wallet.n
for xfp, xp, dd in xpubs: for xfp, xpub, der_prefix in xpubs:
if derivs: if derivs:
# show as a comment if unclear # 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: if keystore is None:
keystore = wallet.get_keystore() keystore = wallet.get_keystore()
if not self.show_address_helper(wallet, address, keystore): if not self.show_address_helper(wallet, address, keystore):
@ -633,50 +603,38 @@ class ColdcardPlugin(HW_PluginBase):
sequence = wallet.get_address_index(address) sequence = wallet.get_address_index(address)
keystore.show_address(sequence, txin_type) keystore.show_address(sequence, txin_type)
elif type(wallet) is Multisig_Wallet: 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 # More involved for P2SH/P2WSH addresses: need M, and all public keys, and their
# derivation paths. Must construct script, and track fingerprints+paths for # derivation paths. Must construct script, and track fingerprints+paths for
# all those keys # all those keys
pubkeys = wallet.get_public_keys(address) pubkey_deriv_info = wallet.get_public_keys_with_deriv_info(address)
pubkeys = sorted([pk for pk in list(pubkey_deriv_info)])
xfps = [] xfp_paths = []
for xp, ks in zip(wallet.get_master_public_keys(), wallet.get_keystores()): for pubkey_hex in pubkey_deriv_info:
path = "%s/%d/%d" % (getattr(ks, 'derivation', 'm'), ks, der_suffix = pubkey_deriv_info[pubkey_hex]
*wallet.get_address_index(address)) xfp_int = xfp_int_for_keystore(ks)
der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
# need master XFP for each co-signers der_full = der_prefix + list(der_suffix)
ks_xfp = xfp_for_keystore(ks) xfp_paths.append([xfp_int] + der_full)
xfps.append(unpacked_xfp_path(ks_xfp, path))
# put into BIP45 (sorted) order script = bfh(wallet.pubkeys_to_scriptcode(pubkeys))
pkx = list(sorted(zip(pubkeys, xfps)))
script = bfh(multisig_script([pk for pk,xfp in pkx], wallet.m)) keystore.show_p2sh_address(wallet.m, script, xfp_paths, txin_type)
keystore.show_p2sh_address(wallet.m, script, [xfp for pk,xfp in pkx], txin_type)
else: else:
keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
return return
@classmethod
def link_wallet(cls, wallet): def xfp_int_for_keystore(keystore: Xpub) -> int:
# PROBLEM: wallet.sign_transaction() does not pass in the wallet to the individual xfp = keystore.get_root_fingerprint()
# keystores, and we need to know about our co-signers at that time. return int.from_bytes(bfh(xfp), byteorder="little", signed=False)
# 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 def xfp2str(xfp: int) -> str:
# *is* a PSBT and not an electrum tx object # Standardized way to show an xpub's fingerprint... it's a 4-byte string
for ks in wallet.get_keystores(): # and not really an integer. Used to show as '0x%08x' but that's wrong endian.
if type(ks) == Coldcard_KeyStore: return struct.pack('<I', xfp).hex().lower()
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)
# EOF # 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.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available 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 CC_DEBUG = False
@ -73,135 +68,11 @@ class Plugin(ColdcardPlugin, QtPluginBase):
ColdcardPlugin.export_ms_wallet(wallet, f, basename) ColdcardPlugin.export_ms_wallet(wallet, f, basename)
main_window.show_message(_("Wallet setup file exported successfully")) 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): def show_settings_dialog(self, window, keystore):
# When they click on the icon for CC we come here. # When they click on the icon for CC we come here.
# - doesn't matter if device not connected, continue # - doesn't matter if device not connected, continue
CKCCSettingsDialog(window, self, keystore).exec_() 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): class Coldcard_Handler(QtHandlerBase):
setup_signal = pyqtSignal() setup_signal = pyqtSignal()
@ -307,7 +178,7 @@ class CKCCSettingsDialog(WindowModalDialog):
def show_placeholders(self, unclear_arg): def show_placeholders(self, unclear_arg):
# device missing, so hide lots of detail. # 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.serial.setText('(not connected)')
self.fw_version.setText('') self.fw_version.setText('')
self.fw_built.setText('') self.fw_built.setText('')

48
electrum/plugins/cosigner_pool/qt.py

@ -25,23 +25,25 @@
import time import time
from xmlrpc.client import ServerProxy from xmlrpc.client import ServerProxy
from typing import TYPE_CHECKING, Union, List, Tuple
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import QPushButton from PyQt5.QtWidgets import QPushButton
from electrum import util, keystore, ecc, crypto from electrum import util, keystore, ecc, crypto
from electrum import transaction from electrum import transaction
from electrum.transaction import Transaction, PartialTransaction, tx_from_any
from electrum.bip32 import BIP32Node from electrum.bip32 import BIP32Node
from electrum.plugin import BasePlugin, hook from electrum.plugin import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.wallet import Multisig_Wallet from electrum.wallet import Multisig_Wallet
from electrum.util import bh2u, bfh 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 from electrum.gui.qt.util import WaitingDialog
import sys if TYPE_CHECKING:
import traceback from electrum.gui.qt.main_window import ElectrumWindow
server = ServerProxy('https://cosigner.electrum.org/', allow_none=True) server = ServerProxy('https://cosigner.electrum.org/', allow_none=True)
@ -97,8 +99,8 @@ class Plugin(BasePlugin):
self.listener = None self.listener = None
self.obj = QReceiveSignalObject() self.obj = QReceiveSignalObject()
self.obj.cosigner_receive_signal.connect(self.on_receive) self.obj.cosigner_receive_signal.connect(self.on_receive)
self.keys = [] self.keys = [] # type: List[Tuple[str, str, ElectrumWindow]]
self.cosigner_list = [] self.cosigner_list = [] # type: List[Tuple[ElectrumWindow, str, bytes, str]]
@hook @hook
def init_qt(self, gui): def init_qt(self, gui):
@ -116,10 +118,11 @@ class Plugin(BasePlugin):
def is_available(self): def is_available(self):
return True return True
def update(self, window): def update(self, window: 'ElectrumWindow'):
wallet = window.wallet wallet = window.wallet
if type(wallet) != Multisig_Wallet: if type(wallet) != Multisig_Wallet:
return return
assert isinstance(wallet, Multisig_Wallet) # only here for type-hints in IDE
if self.listener is None: if self.listener is None:
self.logger.info("starting listener") self.logger.info("starting listener")
self.listener = Listener(self) self.listener = Listener(self)
@ -131,7 +134,7 @@ class Plugin(BasePlugin):
self.keys = [] self.keys = []
self.cosigner_list = [] self.cosigner_list = []
for key, keystore in wallet.keystores.items(): 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) pubkey = BIP32Node.from_xkey(xpub).eckey.get_public_key_bytes(compressed=True)
_hash = bh2u(crypto.sha256d(pubkey)) _hash = bh2u(crypto.sha256d(pubkey))
if not keystore.is_watching_only(): if not keystore.is_watching_only():
@ -142,14 +145,14 @@ class Plugin(BasePlugin):
self.listener.set_keyhashes([t[1] for t in self.keys]) self.listener.set_keyhashes([t[1] for t in self.keys])
@hook @hook
def transaction_dialog(self, d): def transaction_dialog(self, d: 'TxDialog'):
d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
b.clicked.connect(lambda: self.do_send(d.tx)) b.clicked.connect(lambda: self.do_send(d.tx))
d.buttons.insert(0, b) d.buttons.insert(0, b)
self.transaction_dialog_update(d) self.transaction_dialog_update(d)
@hook @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): if d.tx.is_complete() or d.wallet.can_sign(d.tx):
d.cosigner_send_button.hide() d.cosigner_send_button.hide()
return return
@ -160,17 +163,14 @@ class Plugin(BasePlugin):
else: else:
d.cosigner_send_button.hide() d.cosigner_send_button.hide()
def cosigner_can_sign(self, tx, cosigner_xpub): def cosigner_can_sign(self, tx: Transaction, cosigner_xpub: str) -> bool:
from electrum.keystore import is_xpubkey, parse_xpubkey if not isinstance(tx, PartialTransaction):
xpub_set = set([]) return False
for txin in tx.inputs(): if tx.is_complete():
for x_pubkey in txin['x_pubkeys']: return False
if is_xpubkey(x_pubkey): return cosigner_xpub in {bip32node.to_xpub() for bip32node in tx.xpubs}
xpub, s = parse_xpubkey(x_pubkey)
xpub_set.add(xpub) def do_send(self, tx: Union[Transaction, PartialTransaction]):
return cosigner_xpub in xpub_set
def do_send(self, tx):
def on_success(result): def on_success(result):
window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' + window.show_message(_("Your transaction was sent to the cosigning pool.") + '\n' +
_("Open your cosigner wallet to retrieve it.")) _("Open your cosigner wallet to retrieve it."))
@ -184,7 +184,7 @@ class Plugin(BasePlugin):
if not self.cosigner_can_sign(tx, xpub): if not self.cosigner_can_sign(tx, xpub):
continue continue
# construct message # construct message
raw_tx_bytes = bfh(str(tx)) raw_tx_bytes = tx.serialize_as_bytes()
public_key = ecc.ECPubkey(K) public_key = ecc.ECPubkey(K)
message = public_key.encrypt_message(raw_tx_bytes).decode('ascii') message = public_key.encrypt_message(raw_tx_bytes).decode('ascii')
# send message # send message
@ -223,12 +223,12 @@ class Plugin(BasePlugin):
return return
try: try:
privkey = BIP32Node.from_xkey(xprv).eckey privkey = BIP32Node.from_xkey(xprv).eckey
message = bh2u(privkey.decrypt_message(message)) message = privkey.decrypt_message(message)
except Exception as e: except Exception as e:
self.logger.exception('') self.logger.exception('')
window.show_error(_('Error decrypting message') + ':\n' + repr(e)) window.show_error(_('Error decrypting message') + ':\n' + repr(e))
return return
self.listener.clear(keyhash) self.listener.clear(keyhash)
tx = transaction.Transaction(message) tx = tx_from_any(message)
show_transaction(tx, window, prompt_if_unsaved=True) show_transaction(tx, parent=window, prompt_if_unsaved=True)

101
electrum/plugins/digitalbitbox/digitalbitbox.py

@ -14,20 +14,21 @@ import re
import struct import struct
import sys import sys
import time import time
import copy
from electrum.crypto import sha256d, EncodeAES_base64, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot 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, from electrum.bitcoin import (TYPE_ADDRESS, push_script, var_int, public_key_to_p2pkh,
is_address) is_address)
from electrum.bip32 import BIP32Node from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath
from electrum import ecc from electrum import ecc
from electrum.ecc import msg_magic from electrum.ecc import msg_magic
from electrum.wallet import Standard_Wallet from electrum.wallet import Standard_Wallet
from electrum import constants from electrum import constants
from electrum.transaction import Transaction from electrum.transaction import Transaction, PartialTransaction, PartialTxInput
from electrum.i18n import _ from electrum.i18n import _
from electrum.keystore import Hardware_KeyStore from electrum.keystore import Hardware_KeyStore
from ..hw_wallet import HW_PluginBase 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.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
from electrum.network import Network from electrum.network import Network
from electrum.logging import get_logger from electrum.logging import get_logger
@ -449,21 +450,13 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
hw_type = 'digitalbitbox' hw_type = 'digitalbitbox'
device = 'DigitalBitbox' device = 'DigitalBitbox'
plugin: 'DigitalBitboxPlugin'
def __init__(self, d): def __init__(self, d):
Hardware_KeyStore.__init__(self, d) Hardware_KeyStore.__init__(self, d)
self.force_watching_only = False self.force_watching_only = False
self.maxInputs = 14 # maximum inputs per single sign command 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): def give_error(self, message, clear_client = False):
if clear_client: if clear_client:
self.client = None self.client = None
@ -478,7 +471,7 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
sig = None sig = None
try: try:
message = message.encode('utf8') 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)) msg_hash = sha256d(msg_magic(message))
inputHash = to_hexstr(msg_hash) inputHash = to_hexstr(msg_hash)
hasharray = [] hasharray = []
@ -540,58 +533,50 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
try: try:
p2pkhTransaction = True p2pkhTransaction = True
derivations = self.get_tx_derivations(tx)
inputhasharray = [] inputhasharray = []
hasharray = [] hasharray = []
pubkeyarray = [] pubkeyarray = []
# Build hasharray from inputs # Build hasharray from inputs
for i, txin in enumerate(tx.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 self.give_error("Coinbase not supported") # should never happen
if txin['type'] != 'p2pkh': if txin.script_type != 'p2pkh':
p2pkhTransaction = False p2pkhTransaction = False
for x_pubkey in txin['x_pubkeys']: my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)
if x_pubkey in derivations: if not inputPath:
index = derivations.get(x_pubkey) self.give_error("No matching pubkey for sign_transaction") # should never happen
inputPath = "%s/%d/%d" % (self.get_derivation(), index[0], index[1]) inputPath = convert_bip32_intpath_to_strpath(inputPath)
inputHash = sha256d(binascii.unhexlify(tx.serialize_preimage(i))) inputHash = sha256d(bfh(tx.serialize_preimage(i)))
hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath} hasharray_i = {'hash': to_hexstr(inputHash), 'keypath': inputPath}
hasharray.append(hasharray_i) hasharray.append(hasharray_i)
inputhasharray.append(inputHash) inputhasharray.append(inputHash)
break
else:
self.give_error("No matching x_key for sign_transaction") # should never happen
# Build pubkeyarray from outputs # Build pubkeyarray from outputs
for o in tx.outputs(): for txout in tx.outputs():
assert o.type == TYPE_ADDRESS assert txout.address
info = tx.output_info.get(o.address) if txout.is_change:
if info is not None: changePubkey, changePath = self.find_my_pubkey_in_txinout(txout)
if info.is_change: assert changePath
index = info.address_index changePath = convert_bip32_intpath_to_strpath(changePath)
changePath = self.get_derivation() + "/%d/%d" % index changePubkey = changePubkey.hex()
changePubkey = self.derive_pubkey(index[0], index[1]) pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath}
pubkeyarray_i = {'pubkey': changePubkey, 'keypath': changePath} pubkeyarray.append(pubkeyarray_i)
pubkeyarray.append(pubkeyarray_i)
# Special serialization of the unsigned transaction for # Special serialization of the unsigned transaction for
# the mobile verification app. # the mobile verification app.
# At the moment, verification only works for p2pkh transactions. # At the moment, verification only works for p2pkh transactions.
if p2pkhTransaction: if p2pkhTransaction:
class CustomTXSerialization(Transaction): tx_copy = copy.deepcopy(tx)
@classmethod # monkey-patch method of tx_copy instance to change serialization
def input_script(self, txin, estimate_size=False): def input_script(self, txin: PartialTxInput, *, estimate_size=False):
if txin['type'] == 'p2pkh': if txin.script_type == 'p2pkh':
return Transaction.get_preimage_script(txin) return Transaction.get_preimage_script(txin)
if txin['type'] == 'p2sh': raise Exception("unsupported type %s" % txin.script_type)
# Multisig verification has partial support, but is disabled. This is the tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction)
# expected serialization though, so we leave it here until we activate it. tx_dbb_serialized = tx_copy.serialize_to_network()
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()
else: else:
# We only need this for the signing echo / verification. # We only need this for the signing echo / verification.
tx_dbb_serialized = None tx_dbb_serialized = None
@ -656,12 +641,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
if len(dbb_signatures) != len(tx.inputs()): if len(dbb_signatures) != len(tx.inputs()):
raise Exception("Incorrect number of transactions signed.") # Should never occur raise Exception("Incorrect number of transactions signed.") # Should never occur
for i, txin in enumerate(tx.inputs()): for i, txin in enumerate(tx.inputs()):
num = txin['num_sig'] for pubkey_bytes in txin.pubkeys:
for pubkey in txin['pubkeys']: if txin.is_complete():
signatures = list(filter(None, txin['signatures'])) break
if len(signatures) == num:
break # txin is complete
ii = txin['pubkeys'].index(pubkey)
signed = dbb_signatures[i] signed = dbb_signatures[i]
if 'recid' in signed: if 'recid' in signed:
# firmware > v2.1.1 # firmware > v2.1.1
@ -673,20 +655,19 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
elif 'pubkey' in signed: elif 'pubkey' in signed:
# firmware <= v2.1.1 # firmware <= v2.1.1
pk = signed['pubkey'] pk = signed['pubkey']
if pk != pubkey: if pk != pubkey_bytes.hex():
continue continue
sig_r = int(signed['sig'][:64], 16) sig_r = int(signed['sig'][:64], 16)
sig_s = 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 = ecc.der_sig_from_r_and_s(sig_r, sig_s)
sig = to_hexstr(sig) + '01' 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: except UserCancelled:
raise raise
except BaseException as e: except BaseException as e:
self.give_error(e, True) self.give_error(e, True)
else: else:
_logger.info("Transaction is_complete {tx.is_complete()}") _logger.info(f"Transaction is_complete {tx.is_complete()}")
tx.raw = tx.serialize()
class DigitalBitboxPlugin(HW_PluginBase): class DigitalBitboxPlugin(HW_PluginBase):
@ -788,11 +769,11 @@ class DigitalBitboxPlugin(HW_PluginBase):
if not self.is_mobile_paired(): 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)) keystore.handler.show_error(_('This function is only available after pairing your {} with a mobile device.').format(self.device))
return 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)) keystore.handler.show_error(_('This function is only available for p2pkh keystores when using {}.').format(self.device))
return return
change, index = wallet.get_address_index(address) 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) xpub = self.get_client(keystore)._get_xpub(keypath)
verify_request_payload = { verify_request_payload = {
"type": 'p2pkh', "type": 'p2pkh',

12
electrum/plugins/digitalbitbox/qt.py

@ -2,7 +2,7 @@ from functools import partial
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import hook 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.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available from ..hw_wallet.plugin import only_hook_if_libraries_available
@ -18,7 +18,7 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
@only_hook_if_libraries_available @only_hook_if_libraries_available
@hook @hook
def receive_menu(self, menu, addrs, wallet): def receive_menu(self, menu, addrs, wallet: Abstract_Wallet):
if type(wallet) is not Standard_Wallet: if type(wallet) is not Standard_Wallet:
return return
@ -29,12 +29,12 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
if not self.is_mobile_paired(): if not self.is_mobile_paired():
return return
if not keystore.is_p2pkh():
return
if len(addrs) == 1: if len(addrs) == 1:
addr = addrs[0]
if wallet.get_txin_type(addr) != 'p2pkh':
return
def show_address(): 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) 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: if TYPE_CHECKING:
from aiohttp import ClientResponse from aiohttp import ClientResponse
from electrum.gui.qt.transaction_dialog import TxDialog
class Plugin(BasePlugin): class Plugin(BasePlugin):
@ -43,13 +44,13 @@ class Plugin(BasePlugin):
button_label = _("Verify GA instant") button_label = _("Verify GA instant")
@hook @hook
def transaction_dialog(self, d): def transaction_dialog(self, d: 'TxDialog'):
d.verify_button = QPushButton(self.button_label) d.verify_button = QPushButton(self.button_label)
d.verify_button.clicked.connect(lambda: self.do_verify(d)) d.verify_button.clicked.connect(lambda: self.do_verify(d))
d.buttons.insert(0, d.verify_button) d.buttons.insert(0, d.verify_button)
self.transaction_dialog_update(d) 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 """Returns the address for given tx which can be used to request
instant confirmation verification from GreenAddress""" instant confirmation verification from GreenAddress"""
for o in d.tx.outputs(): for o in d.tx.outputs():
@ -58,13 +59,13 @@ class Plugin(BasePlugin):
return None return None
@hook @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): if d.tx.is_complete() and self.get_my_addr(d):
d.verify_button.show() d.verify_button.show()
else: else:
d.verify_button.hide() d.verify_button.hide()
def do_verify(self, d): def do_verify(self, d: 'TxDialog'):
tx = d.tx tx = d.tx
wallet = d.wallet wallet = d.wallet
window = d.main_window 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 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
from typing import TYPE_CHECKING, Dict, List, Union, Tuple
from electrum.plugin import BasePlugin, hook from electrum.plugin import BasePlugin, hook
from electrum.i18n import _ from electrum.i18n import _
from electrum.bitcoin import is_address, TYPE_SCRIPT, opcodes from electrum.bitcoin import is_address, TYPE_SCRIPT, opcodes
from electrum.util import bfh, versiontuple, UserFacingException 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): class HW_PluginBase(BasePlugin):
@ -65,7 +72,7 @@ class HW_PluginBase(BasePlugin):
""" """
raise NotImplementedError() 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 pass # implemented in child classes
def show_address_helper(self, wallet, address, keystore=None): def show_address_helper(self, wallet, address, keystore=None):
@ -132,20 +139,12 @@ class HW_PluginBase(BasePlugin):
return self._ignore_outdated_fw return self._ignore_outdated_fw
def is_any_tx_output_on_change_branch(tx: Transaction) -> bool: def is_any_tx_output_on_change_branch(tx: PartialTransaction) -> bool:
if not tx.output_info: return any([txout.is_change for txout in tx.outputs()])
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 trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes: def trezor_validate_op_return_output_and_get_data(output: TxOutput) -> bytes:
if output.type != TYPE_SCRIPT: script = output.scriptpubkey
raise Exception("Unexpected output type: {}".format(output.type))
script = bfh(output.address)
if not (script[0] == opcodes.OP_RETURN and if not (script[0] == opcodes.OP_RETURN and
script[1] == len(script) - 2 and script[1] <= 75): script[1] == len(script) - 2 and script[1] <= 75):
raise UserFacingException(_("Only OP_RETURN scripts, with one constant push, are supported.")) 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:] 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): def only_hook_if_libraries_available(func):
# note: this decorator must wrap @hook, not the other way around, # note: this decorator must wrap @hook, not the other way around,
# as 'hook' uses the name of the function it wraps # 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 from binascii import hexlify, unhexlify
import traceback import traceback
import sys import sys
from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING
from electrum.util import bfh, bh2u, UserCancelled, UserFacingException from electrum.util import bfh, bh2u, UserCancelled, UserFacingException
from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
from electrum.bip32 import BIP32Node from electrum.bip32 import BIP32Node
from electrum import constants from electrum import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.transaction import deserialize, Transaction from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey from electrum.keystore import Hardware_KeyStore
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase 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 # TREZOR initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
@ -23,8 +27,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
hw_type = 'keepkey' hw_type = 'keepkey'
device = 'KeepKey' device = 'KeepKey'
def get_derivation(self): plugin: 'KeepKeyPlugin'
return self.derivation
def get_client(self, force_pair=True): def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair) return self.plugin.get_client(self, force_pair)
@ -34,7 +37,7 @@ class KeepKey_KeyStore(Hardware_KeyStore):
def sign_message(self, sequence, message, password): def sign_message(self, sequence, message, password):
client = self.get_client() 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) address_n = client.expand_path(address_path)
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
return msg_sig.signature return msg_sig.signature
@ -44,22 +47,13 @@ class KeepKey_KeyStore(Hardware_KeyStore):
return return
# previous transactions used as inputs # previous transactions used as inputs
prev_tx = {} prev_tx = {}
# path of the xpubs that are involved
xpub_path = {}
for txin in tx.inputs(): for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin.prevout.txid.hex()
tx_hash = txin['prevout_hash'] if txin.utxo is None and not Transaction.is_segwit_input(txin):
if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Missing previous tx for legacy input.'))
raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin.utxo
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()
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) self.plugin.sign_transaction(self, tx, prev_tx)
class KeepKeyPlugin(HW_PluginBase): class KeepKeyPlugin(HW_PluginBase):
@ -164,7 +158,7 @@ class KeepKeyPlugin(HW_PluginBase):
return client return client
def get_client(self, keystore, force_pair=True): def get_client(self, keystore, force_pair=True) -> Optional['KeepKeyClient']:
devmgr = self.device_manager() devmgr = self.device_manager()
handler = keystore.handler handler = keystore.handler
with devmgr.hid_lock: with devmgr.hid_lock:
@ -306,12 +300,11 @@ class KeepKeyPlugin(HW_PluginBase):
return self.types.PAYTOMULTISIG return self.types.PAYTOMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) 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.prev_tx = prev_tx
self.xpub_path = xpub_path
client = self.get_client(keystore) client = self.get_client(keystore)
inputs = self.tx_inputs(tx, True) inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
outputs = self.tx_outputs(keystore.get_derivation(), tx) outputs = self.tx_outputs(tx, keystore=keystore)
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,
lock_time=tx.locktime, version=tx.version)[0] lock_time=tx.locktime, version=tx.version)[0]
signatures = [(bh2u(x) + '01') for x in signatures] signatures = [(bh2u(x) + '01') for x in signatures]
@ -326,137 +319,112 @@ class KeepKeyPlugin(HW_PluginBase):
if not client.atleast_version(1, 3): if not client.atleast_version(1, 3):
keystore.handler.show_error(_("Your device firmware is too old")) keystore.handler.show_error(_("Your device firmware is too old"))
return return
change, index = wallet.get_address_index(address) deriv_suffix = wallet.get_address_index(address)
derivation = keystore.derivation derivation = keystore.get_derivation_prefix()
address_path = "%s/%d/%d"%(derivation, change, index) address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
address_n = client.expand_path(address_path) 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() xpubs = wallet.get_master_public_keys()
if len(xpubs) == 1: 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])
pubkeys = wallet.get_public_keys(address) pubkeys = wallet.get_public_keys(address)
# sort xpubs using the order of pubkeys # sort xpubs using the order of pubkeys
sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) sorted_pairs = sorted(zip(pubkeys, xpubs))
pubkeys = list(map(f, sorted_xpubs)) multisig = self._make_multisig(
multisig = self.types.MultisigRedeemScriptType( wallet.m,
pubkeys=pubkeys, [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
signatures=[b''] * wallet.n, else:
m=wallet.m, multisig = None
)
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)
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):
def tx_inputs(self, tx, for_sig=False):
inputs = [] inputs = []
for txin in tx.inputs(): for txin in tx.inputs():
txinputtype = self.types.TxInputType() txinputtype = self.types.TxInputType()
if txin['type'] == 'coinbase': if txin.is_coinbase():
prev_hash = b"\x00"*32 prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1 prev_index = 0xffffffff # signed int -1
else: else:
if for_sig: if for_sig:
x_pubkeys = txin['x_pubkeys'] assert isinstance(tx, PartialTransaction)
if len(x_pubkeys) == 1: assert isinstance(txin, PartialTxInput)
x_pubkey = x_pubkeys[0] assert keystore
xpub, s = parse_xpubkey(x_pubkey) xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
txinputtype.address_n.extend(xpub_n + s) script_type = self.get_keepkey_input_script_type(txin.script_type)
txinputtype.script_type = self.get_keepkey_input_script_type(txin['type']) txinputtype = self.types.TxInputType(
else: script_type=script_type,
def f(x_pubkey): multisig=multisig)
xpub, s = parse_xpubkey(x_pubkey) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
return self._make_node_path(xpub, s) if full_path:
pubkeys = list(map(f, x_pubkeys)) txinputtype.address_n = full_path
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys, prev_hash = txin.prevout.txid
signatures=map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures')), prev_index = txin.prevout.out_idx
m=txin.get('num_sig'),
) if txin.value_sats() is not None:
script_type = self.get_keepkey_input_script_type(txin['type']) txinputtype.amount = txin.value_sats()
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']
txinputtype.prev_hash = prev_hash txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index txinputtype.prev_index = prev_index
if txin.get('scriptSig') is not None: if txin.script_sig is not None:
script_sig = bfh(txin['scriptSig']) txinputtype.script_sig = txin.script_sig
txinputtype.script_sig = script_sig
txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) txinputtype.sequence = txin.nsequence
inputs.append(txinputtype) inputs.append(txinputtype)
return inputs 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(): def create_output_by_derivation():
script_type = self.get_keepkey_output_script_type(info.script_type) script_type = self.get_keepkey_output_script_type(txout.script_type)
if len(xpubs) == 1: xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
txoutputtype = self.types.TxOutputType( my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
amount=amount, assert full_path
script_type=script_type, txoutputtype = self.types.TxOutputType(
address_n=address_n, multisig=multisig,
) amount=txout.value,
else: address_n=full_path,
address_n = self.client_class.expand_path("/%d/%d" % index) script_type=script_type)
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)
return txoutputtype return txoutputtype
def create_output_by_address(): def create_output_by_address():
txoutputtype = self.types.TxOutputType() txoutputtype = self.types.TxOutputType()
txoutputtype.amount = amount txoutputtype.amount = txout.value
if _type == TYPE_SCRIPT: if address:
txoutputtype.script_type = self.types.PAYTOOPRETURN
txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
elif _type == TYPE_ADDRESS:
txoutputtype.script_type = self.types.PAYTOADDRESS txoutputtype.script_type = self.types.PAYTOADDRESS
txoutputtype.address = address 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 return txoutputtype
outputs = [] outputs = []
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for o in tx.outputs(): for txout in tx.outputs():
_type, address, amount = o.type, o.address, o.value address = txout.address
use_create_by_derivation = False use_create_by_derivation = False
info = tx.output_info.get(address) if txout.is_mine and not has_change:
if info is not None and not has_change:
index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
# prioritise hiding outputs on the 'change' branch from user # prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed # 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 use_create_by_derivation = True
has_change = True has_change = True
@ -468,20 +436,20 @@ class KeepKeyPlugin(HW_PluginBase):
return outputs return outputs
def electrum_tx_to_txtype(self, tx): def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
t = self.types.TransactionType() t = self.types.TransactionType()
if tx is None: if tx is None:
# probably for segwit input and we don't need this prev txn # probably for segwit input and we don't need this prev txn
return t return t
d = deserialize(tx.raw) tx.deserialize()
t.version = d['version'] t.version = tx.version
t.lock_time = d['lockTime'] t.lock_time = tx.locktime
inputs = self.tx_inputs(tx) inputs = self.tx_inputs(tx)
t.inputs.extend(inputs) t.inputs.extend(inputs)
for vout in d['outputs']: for out in tx.outputs():
o = t.bin_outputs.add() o = t.bin_outputs.add()
o.amount = vout['value'] o.amount = out.value
o.script_pubkey = bfh(vout['scriptPubKey']) o.script_pubkey = out.scriptpubkey
return t return t
# This function is called from the TREZOR libraries (via tx_api) # 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 import ecc
from electrum.bitcoin import TYPE_ADDRESS, int_to_hex, var_int, is_segwit_script_type 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.i18n import _
from electrum.keystore import Hardware_KeyStore 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.wallet import Standard_Wallet
from electrum.util import bfh, bh2u, versiontuple, UserFacingException from electrum.util import bfh, bh2u, versiontuple, UserFacingException
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
@ -217,6 +217,8 @@ class Ledger_KeyStore(Hardware_KeyStore):
hw_type = 'ledger' hw_type = 'ledger'
device = 'Ledger' device = 'Ledger'
plugin: 'LedgerPlugin'
def __init__(self, d): def __init__(self, d):
Hardware_KeyStore.__init__(self, d) Hardware_KeyStore.__init__(self, d)
# Errors and other user interaction is done through the wallet's # Errors and other user interaction is done through the wallet's
@ -231,9 +233,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
obj['cfg'] = self.cfg obj['cfg'] = self.cfg
return obj return obj
def get_derivation(self):
return self.derivation
def get_client(self): def get_client(self):
return self.plugin.get_client(self).dongleObject return self.plugin.get_client(self).dongleObject
@ -260,13 +259,6 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.signing = False self.signing = False
return wrapper 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): def decrypt_message(self, pubkey, message, password):
raise UserFacingException(_('Encryption and decryption are currently not supported for {}').format(self.device)) 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() message_hash = hashlib.sha256(message).hexdigest().upper()
# prompt for the PIN before displaying the dialog if necessary # prompt for the PIN before displaying the dialog if necessary
client = self.get_client() 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) self.handler.show_message("Signing message ...\r\nMessage hash: "+message_hash)
try: try:
info = self.get_client().signMessagePrepare(address_path, message) info = self.get_client().signMessagePrepare(address_path, message)
@ -318,16 +310,13 @@ class Ledger_KeyStore(Hardware_KeyStore):
@test_pin_unlocked @test_pin_unlocked
@set_and_unset_signing @set_and_unset_signing
def sign_transaction(self, tx: Transaction, password): def sign_transaction(self, tx, password):
if tx.is_complete(): if tx.is_complete():
return return
client = self.get_client()
inputs = [] inputs = []
inputsPaths = [] inputsPaths = []
pubKeys = []
chipInputs = [] chipInputs = []
redeemScripts = [] redeemScripts = []
signatures = []
changePath = "" changePath = ""
output = None output = None
p2shTransaction = False p2shTransaction = False
@ -336,60 +325,52 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.get_client() # prompt for the PIN before displaying the dialog if necessary self.get_client() # prompt for the PIN before displaying the dialog if necessary
# Fetch inputs of the transaction to sign # Fetch inputs of the transaction to sign
derivations = self.get_tx_derivations(tx)
for txin in tx.inputs(): for txin in tx.inputs():
if txin['type'] == 'coinbase': if txin.is_coinbase():
self.give_error("Coinbase not supported") # should never happen self.give_error("Coinbase not supported") # should never happen
if txin['type'] in ['p2sh']: if txin.script_type in ['p2sh']:
p2shTransaction = True 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(): if not self.get_client_electrum().supports_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True segwitTransaction = True
if txin['type'] in ['p2wpkh', 'p2wsh']: if txin.script_type in ['p2wpkh', 'p2wsh']:
if not self.get_client_electrum().supports_native_segwit(): if not self.get_client_electrum().supports_native_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True segwitTransaction = True
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin)
for i, x_pubkey in enumerate(x_pubkeys): if not full_path:
if x_pubkey in derivations: self.give_error("No matching pubkey for sign_transaction") # should never happen
signingPos = i full_path = convert_bip32_intpath_to_strpath(full_path)
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
redeemScript = Transaction.get_preimage_script(txin) 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): 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)) raise UserFacingException(_('Missing previous tx for legacy input.'))
txin_prev_tx_raw = txin_prev_tx.raw if txin_prev_tx else None txin_prev_tx_raw = txin_prev_tx.serialize() if txin_prev_tx else None
inputs.append([txin_prev_tx_raw, inputs.append([txin_prev_tx_raw,
txin['prevout_n'], txin.prevout.out_idx,
redeemScript, redeemScript,
txin['prevout_hash'], txin.prevout.txid.hex(),
signingPos, my_pubkey,
txin.get('sequence', 0xffffffff - 1), txin.nsequence,
txin.get('value')]) txin.value_sats()])
inputsPaths.append(hwAddress) inputsPaths.append(full_path)
pubKeys.append(pubkeys)
# Sanity check # Sanity check
if p2shTransaction: if p2shTransaction:
for txin in tx.inputs(): 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 self.give_error("P2SH / regular input mixed in same transaction not supported") # should never happen
txOutput = var_int(len(tx.outputs())) txOutput = var_int(len(tx.outputs()))
for o in tx.outputs(): for o in tx.outputs():
output_type, addr, amount = o.type, o.address, o.value txOutput += int_to_hex(o.value, 8)
txOutput += int_to_hex(amount, 8) script = o.scriptpubkey.hex()
script = tx.pay_script(output_type, addr)
txOutput += var_int(len(script)//2) txOutput += var_int(len(script)//2)
txOutput += script txOutput += script
txOutput = bfh(txOutput) txOutput = bfh(txOutput)
@ -403,21 +384,21 @@ class Ledger_KeyStore(Hardware_KeyStore):
self.give_error("Transaction with more than 2 outputs not supported") self.give_error("Transaction with more than 2 outputs not supported")
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for o in tx.outputs(): for txout in tx.outputs():
assert o.type == TYPE_ADDRESS assert txout.address
info = tx.output_info.get(o.address) if txout.is_mine and len(tx.outputs()) > 1 \
if (info is not None) and len(tx.outputs()) > 1 \
and not has_change: and not has_change:
index = info.address_index
# prioritise hiding outputs on the 'change' branch from user # prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed # 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:
changePath = self.get_derivation()[2:] + "/%d/%d"%index my_pubkey, changePath = self.find_my_pubkey_in_txinout(txout)
assert changePath
changePath = convert_bip32_intpath_to_strpath(changePath)
has_change = True has_change = True
else: else:
output = o.address output = txout.address
else: else:
output = o.address output = txout.address
self.handler.show_message(_("Confirm Transaction on your Ledger device...")) self.handler.show_message(_("Confirm Transaction on your Ledger device..."))
try: try:
@ -467,7 +448,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
singleInput, redeemScripts[inputIndex], version=tx.version) singleInput, redeemScripts[inputIndex], version=tx.version)
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+ 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 inputIndex = inputIndex + 1
else: else:
while inputIndex < len(inputs): while inputIndex < len(inputs):
@ -488,7 +472,10 @@ class Ledger_KeyStore(Hardware_KeyStore):
# Sign input with the provided PIN # Sign input with the provided PIN
inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime) inputSignature = self.get_client().untrustedHashSign(inputsPaths[inputIndex], pin, lockTime=tx.locktime)
inputSignature[0] = 0x30 # force for 1.4.9+ 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 inputIndex = inputIndex + 1
firstTransaction = False firstTransaction = False
except UserWarning: except UserWarning:
@ -508,16 +495,11 @@ class Ledger_KeyStore(Hardware_KeyStore):
finally: finally:
self.handler.finished() 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 @test_pin_unlocked
@set_and_unset_signing @set_and_unset_signing
def show_address(self, sequence, txin_type): def show_address(self, sequence, txin_type):
client = self.get_client() 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 ...")) self.handler.show_message(_("Showing address ..."))
segwit = is_segwit_script_type(txin_type) segwit = is_segwit_script_type(txin_type)
segwitNative = txin_type == 'p2wpkh' segwitNative = txin_type == 'p2wpkh'

218
electrum/plugins/safe_t/safe_t.py

@ -1,6 +1,7 @@
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
import traceback import traceback
import sys 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.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException
from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT
@ -8,13 +9,16 @@ from electrum.bip32 import BIP32Node
from electrum import constants from electrum import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import Device from electrum.plugin import Device
from electrum.transaction import deserialize, Transaction from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey from electrum.keystore import Hardware_KeyStore
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
from ..hw_wallet import HW_PluginBase 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 # Safe-T mini initialization methods
TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4) TIM_NEW, TIM_RECOVER, TIM_MNEMONIC, TIM_PRIVKEY = range(0, 4)
@ -24,8 +28,7 @@ class SafeTKeyStore(Hardware_KeyStore):
hw_type = 'safe_t' hw_type = 'safe_t'
device = 'Safe-T mini' device = 'Safe-T mini'
def get_derivation(self): plugin: 'SafeTPlugin'
return self.derivation
def get_client(self, force_pair=True): def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair) return self.plugin.get_client(self, force_pair)
@ -35,7 +38,7 @@ class SafeTKeyStore(Hardware_KeyStore):
def sign_message(self, sequence, message, password): def sign_message(self, sequence, message, password):
client = self.get_client() 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) address_n = client.expand_path(address_path)
msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message) msg_sig = client.sign_message(self.plugin.get_coin_name(), address_n, message)
return msg_sig.signature return msg_sig.signature
@ -45,22 +48,13 @@ class SafeTKeyStore(Hardware_KeyStore):
return return
# previous transactions used as inputs # previous transactions used as inputs
prev_tx = {} prev_tx = {}
# path of the xpubs that are involved
xpub_path = {}
for txin in tx.inputs(): for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin.prevout.txid.hex()
tx_hash = txin['prevout_hash'] if txin.utxo is None and not Transaction.is_segwit_input(txin):
if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Missing previous tx for legacy input.'))
raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin.utxo
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()
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) self.plugin.sign_transaction(self, tx, prev_tx)
class SafeTPlugin(HW_PluginBase): class SafeTPlugin(HW_PluginBase):
@ -148,7 +142,7 @@ class SafeTPlugin(HW_PluginBase):
return client return client
def get_client(self, keystore, force_pair=True): def get_client(self, keystore, force_pair=True) -> Optional['SafeTClient']:
devmgr = self.device_manager() devmgr = self.device_manager()
handler = keystore.handler handler = keystore.handler
with devmgr.hid_lock: with devmgr.hid_lock:
@ -302,12 +296,11 @@ class SafeTPlugin(HW_PluginBase):
return self.types.OutputScriptType.PAYTOMULTISIG return self.types.OutputScriptType.PAYTOMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) 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.prev_tx = prev_tx
self.xpub_path = xpub_path
client = self.get_client(keystore) client = self.get_client(keystore)
inputs = self.tx_inputs(tx, True) inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
outputs = self.tx_outputs(keystore.get_derivation(), tx) outputs = self.tx_outputs(tx, keystore=keystore)
signatures = client.sign_tx(self.get_coin_name(), inputs, outputs, signatures = client.sign_tx(self.get_coin_name(), inputs, outputs,
lock_time=tx.locktime, version=tx.version)[0] lock_time=tx.locktime, version=tx.version)[0]
signatures = [(bh2u(x) + '01') for x in signatures] signatures = [(bh2u(x) + '01') for x in signatures]
@ -322,139 +315,114 @@ class SafeTPlugin(HW_PluginBase):
if not client.atleast_version(1, 0): if not client.atleast_version(1, 0):
keystore.handler.show_error(_("Your device firmware is too old")) keystore.handler.show_error(_("Your device firmware is too old"))
return return
change, index = wallet.get_address_index(address) deriv_suffix = wallet.get_address_index(address)
derivation = keystore.derivation derivation = keystore.get_derivation_prefix()
address_path = "%s/%d/%d"%(derivation, change, index) address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
address_n = client.expand_path(address_path) 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() xpubs = wallet.get_master_public_keys()
if len(xpubs) == 1: 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])
pubkeys = wallet.get_public_keys(address) pubkeys = wallet.get_public_keys(address)
# sort xpubs using the order of pubkeys # sort xpubs using the order of pubkeys
sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) sorted_pairs = sorted(zip(pubkeys, xpubs))
pubkeys = list(map(f, sorted_xpubs)) multisig = self._make_multisig(
multisig = self.types.MultisigRedeemScriptType( wallet.m,
pubkeys=pubkeys, [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
signatures=[b''] * wallet.n, else:
m=wallet.m, multisig = None
)
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)
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):
def tx_inputs(self, tx, for_sig=False):
inputs = [] inputs = []
for txin in tx.inputs(): for txin in tx.inputs():
txinputtype = self.types.TxInputType() txinputtype = self.types.TxInputType()
if txin['type'] == 'coinbase': if txin.is_coinbase():
prev_hash = b"\x00"*32 prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1 prev_index = 0xffffffff # signed int -1
else: else:
if for_sig: if for_sig:
x_pubkeys = txin['x_pubkeys'] assert isinstance(tx, PartialTransaction)
if len(x_pubkeys) == 1: assert isinstance(txin, PartialTxInput)
x_pubkey = x_pubkeys[0] assert keystore
xpub, s = parse_xpubkey(x_pubkey) xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
xpub_n = self.client_class.expand_path(self.xpub_path[xpub]) multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
txinputtype._extend_address_n(xpub_n + s) script_type = self.get_safet_input_script_type(txin.script_type)
txinputtype.script_type = self.get_safet_input_script_type(txin['type']) txinputtype = self.types.TxInputType(
else: script_type=script_type,
def f(x_pubkey): multisig=multisig)
xpub, s = parse_xpubkey(x_pubkey) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
return self._make_node_path(xpub, s) if full_path:
pubkeys = list(map(f, x_pubkeys)) txinputtype.address_n = full_path
multisig = self.types.MultisigRedeemScriptType(
pubkeys=pubkeys, prev_hash = txin.prevout.txid
signatures=list(map(lambda x: bfh(x)[:-1] if x else b'', txin.get('signatures'))), prev_index = txin.prevout.out_idx
m=txin.get('num_sig'),
) if txin.value_sats() is not None:
script_type = self.get_safet_input_script_type(txin['type']) txinputtype.amount = txin.value_sats()
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']
txinputtype.prev_hash = prev_hash txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index txinputtype.prev_index = prev_index
if txin.get('scriptSig') is not None: if txin.script_sig is not None:
script_sig = bfh(txin['scriptSig']) txinputtype.script_sig = txin.script_sig
txinputtype.script_sig = script_sig
txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) txinputtype.sequence = txin.nsequence
inputs.append(txinputtype) inputs.append(txinputtype)
return inputs 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(): def create_output_by_derivation():
script_type = self.get_safet_output_script_type(info.script_type) script_type = self.get_safet_output_script_type(txout.script_type)
if len(xpubs) == 1: xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
address_n = self.client_class.expand_path(derivation + "/%d/%d" % index) multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
txoutputtype = self.types.TxOutputType( my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)
amount=amount, assert full_path
script_type=script_type, txoutputtype = self.types.TxOutputType(
address_n=address_n, multisig=multisig,
) amount=txout.value,
else: address_n=full_path,
address_n = self.client_class.expand_path("/%d/%d" % index) script_type=script_type)
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)
return txoutputtype return txoutputtype
def create_output_by_address(): def create_output_by_address():
txoutputtype = self.types.TxOutputType() txoutputtype = self.types.TxOutputType()
txoutputtype.amount = amount txoutputtype.amount = txout.value
if _type == TYPE_SCRIPT: if address:
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.script_type = self.types.OutputScriptType.PAYTOADDRESS txoutputtype.script_type = self.types.OutputScriptType.PAYTOADDRESS
txoutputtype.address = address 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 return txoutputtype
outputs = [] outputs = []
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for o in tx.outputs(): for txout in tx.outputs():
_type, address, amount = o.type, o.address, o.value address = txout.address
use_create_by_derivation = False use_create_by_derivation = False
info = tx.output_info.get(address) if txout.is_mine and not has_change:
if info is not None and not has_change:
index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
# prioritise hiding outputs on the 'change' branch from user # prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed # because no more than one change address allowed
# note: ^ restriction can be removed once we require fw # note: ^ restriction can be removed once we require fw
# that has https://github.com/trezor/trezor-mcu/pull/306 # 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 use_create_by_derivation = True
has_change = True has_change = True
@ -466,20 +434,20 @@ class SafeTPlugin(HW_PluginBase):
return outputs return outputs
def electrum_tx_to_txtype(self, tx): def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
t = self.types.TransactionType() t = self.types.TransactionType()
if tx is None: if tx is None:
# probably for segwit input and we don't need this prev txn # probably for segwit input and we don't need this prev txn
return t return t
d = deserialize(tx.raw) tx.deserialize()
t.version = d['version'] t.version = tx.version
t.lock_time = d['lockTime'] t.lock_time = tx.locktime
inputs = self.tx_inputs(tx) inputs = self.tx_inputs(tx)
t._extend_inputs(inputs) t._extend_inputs(inputs)
for vout in d['outputs']: for out in tx.outputs():
o = t._add_bin_outputs() o = t._add_bin_outputs()
o.amount = vout['value'] o.amount = out.value
o.script_pubkey = bfh(vout['scriptPubKey']) o.script_pubkey = out.scriptpubkey
return t return t
# This function is called from the TREZOR libraries (via tx_api) # This function is called from the TREZOR libraries (via tx_api)

149
electrum/plugins/trezor/trezor.py

@ -1,6 +1,6 @@
import traceback import traceback
import sys 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.util import bfh, bh2u, versiontuple, UserCancelled, UserFacingException
from electrum.bitcoin import TYPE_ADDRESS, TYPE_SCRIPT 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 import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import Device from electrum.plugin import Device
from electrum.transaction import deserialize, Transaction from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput
from electrum.keystore import Hardware_KeyStore, is_xpubkey, parse_xpubkey from electrum.keystore import Hardware_KeyStore
from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET from electrum.base_wizard import ScriptTypeNotSupported, HWD_SETUP_NEW_WALLET
from electrum.logging import get_logger from electrum.logging import get_logger
from ..hw_wallet import HW_PluginBase 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,
LibraryFoundButUnusable, OutdatedHwFirmwareException) LibraryFoundButUnusable, OutdatedHwFirmwareException,
get_xpubs_and_der_suffixes_from_txinout)
_logger = get_logger(__name__) _logger = get_logger(__name__)
@ -53,8 +54,7 @@ class TrezorKeyStore(Hardware_KeyStore):
hw_type = 'trezor' hw_type = 'trezor'
device = TREZOR_PRODUCT_KEY device = TREZOR_PRODUCT_KEY
def get_derivation(self): plugin: 'TrezorPlugin'
return self.derivation
def get_client(self, force_pair=True): def get_client(self, force_pair=True):
return self.plugin.get_client(self, force_pair) return self.plugin.get_client(self, force_pair)
@ -64,7 +64,7 @@ class TrezorKeyStore(Hardware_KeyStore):
def sign_message(self, sequence, message, password): def sign_message(self, sequence, message, password):
client = self.get_client() 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) msg_sig = client.sign_message(address_path, message)
return msg_sig.signature return msg_sig.signature
@ -73,22 +73,13 @@ class TrezorKeyStore(Hardware_KeyStore):
return return
# previous transactions used as inputs # previous transactions used as inputs
prev_tx = {} prev_tx = {}
# path of the xpubs that are involved
xpub_path = {}
for txin in tx.inputs(): for txin in tx.inputs():
pubkeys, x_pubkeys = tx.get_sorted_pubkeys(txin) tx_hash = txin.prevout.txid.hex()
tx_hash = txin['prevout_hash'] if txin.utxo is None and not Transaction.is_segwit_input(txin):
if txin.get('prev_tx') is None and not Transaction.is_segwit_input(txin): raise UserFacingException(_('Missing previous tx for legacy input.'))
raise UserFacingException(_('Offline signing with {} is not supported for legacy inputs.').format(self.device)) prev_tx[tx_hash] = txin.utxo
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()
self.plugin.sign_transaction(self, tx, prev_tx, xpub_path) self.plugin.sign_transaction(self, tx, prev_tx)
class TrezorInitSettings(NamedTuple): class TrezorInitSettings(NamedTuple):
@ -172,7 +163,7 @@ class TrezorPlugin(HW_PluginBase):
# note that this call can still raise! # note that this call can still raise!
return TrezorClientBase(transport, handler, self) 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() devmgr = self.device_manager()
handler = keystore.handler handler = keystore.handler
with devmgr.hid_lock: with devmgr.hid_lock:
@ -327,11 +318,11 @@ class TrezorPlugin(HW_PluginBase):
return OutputScriptType.PAYTOMULTISIG return OutputScriptType.PAYTOMULTISIG
raise ValueError('unexpected txin type: {}'.format(electrum_txin_type)) 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):
prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx, xpub_path) for txhash, tx in prev_tx.items() } prev_tx = { bfh(txhash): self.electrum_tx_to_txtype(tx) for txhash, tx in prev_tx.items() }
client = self.get_client(keystore) client = self.get_client(keystore)
inputs = self.tx_inputs(tx, xpub_path, True) inputs = self.tx_inputs(tx, for_sig=True, keystore=keystore)
outputs = self.tx_outputs(keystore.get_derivation(), tx) outputs = self.tx_outputs(tx, keystore=keystore)
details = SignTx(lock_time=tx.locktime, version=tx.version) 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, _ = client.sign_tx(self.get_coin_name(), inputs, outputs, details=details, prev_txes=prev_tx)
signatures = [(bh2u(x) + '01') for x in signatures] 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): if not self.show_address_helper(wallet, address, keystore):
return return
deriv_suffix = wallet.get_address_index(address) deriv_suffix = wallet.get_address_index(address)
derivation = keystore.derivation derivation = keystore.get_derivation_prefix()
address_path = "%s/%d/%d"%(derivation, *deriv_suffix) address_path = "%s/%d/%d"%(derivation, *deriv_suffix)
script_type = self.get_trezor_input_script_type(wallet.txin_type) 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)) sorted_pairs = sorted(zip(pubkeys, xpubs))
multisig = self._make_multisig( multisig = self._make_multisig(
wallet.m, wallet.m,
[(xpub, deriv_suffix) for _, xpub in sorted_pairs]) [(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
else: else:
multisig = None multisig = None
client = self.get_client(keystore) client = self.get_client(keystore)
client.show_address(address_path, script_type, multisig) 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 = [] inputs = []
for txin in tx.inputs(): for txin in tx.inputs():
txinputtype = TxInputType() txinputtype = TxInputType()
if txin['type'] == 'coinbase': if txin.is_coinbase():
prev_hash = b"\x00"*32 prev_hash = b"\x00"*32
prev_index = 0xffffffff # signed int -1 prev_index = 0xffffffff # signed int -1
else: else:
if for_sig: if for_sig:
x_pubkeys = txin['x_pubkeys'] assert isinstance(tx, PartialTransaction)
xpubs = [parse_xpubkey(x) for x in x_pubkeys] assert isinstance(txin, PartialTxInput)
multisig = self._make_multisig(txin.get('num_sig'), xpubs, txin.get('signatures')) assert keystore
script_type = self.get_trezor_input_script_type(txin['type']) 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( txinputtype = TxInputType(
script_type=script_type, script_type=script_type,
multisig=multisig) multisig=multisig)
# find which key is mine my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
for xpub, deriv in xpubs: if full_path:
if xpub in xpub_path: txinputtype.address_n = full_path
xpub_n = parse_path(xpub_path[xpub])
txinputtype.address_n = xpub_n + deriv prev_hash = txin.prevout.txid
break prev_index = txin.prevout.out_idx
prev_hash = bfh(txin['prevout_hash']) if txin.value_sats() is not None:
prev_index = txin['prevout_n'] txinputtype.amount = txin.value_sats()
if 'value' in txin:
txinputtype.amount = txin['value']
txinputtype.prev_hash = prev_hash txinputtype.prev_hash = prev_hash
txinputtype.prev_index = prev_index txinputtype.prev_index = prev_index
if txin.get('scriptSig') is not None: if txin.script_sig is not None:
script_sig = bfh(txin['scriptSig']) txinputtype.script_sig = txin.script_sig
txinputtype.script_sig = script_sig
txinputtype.sequence = txin.get('sequence', 0xffffffff - 1) txinputtype.sequence = txin.nsequence
inputs.append(txinputtype) inputs.append(txinputtype)
return inputs return inputs
def _make_multisig(self, m, xpubs, signatures=None): def _make_multisig(self, m, xpubs):
if len(xpubs) == 1: if len(xpubs) == 1:
return None return None
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] 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( return MultisigRedeemScriptType(
pubkeys=pubkeys, pubkeys=pubkeys,
signatures=signatures, signatures=[b''] * len(pubkeys),
m=m) m=m)
def tx_outputs(self, derivation, tx: Transaction): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore' = None):
def create_output_by_derivation(): def create_output_by_derivation():
script_type = self.get_trezor_output_script_type(info.script_type) script_type = self.get_trezor_output_script_type(txout.script_type)
deriv = parse_path("/%d/%d" % index) xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(m, [(xpub, deriv) for xpub in xpubs]) 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( txoutputtype = TxOutputType(
multisig=multisig, multisig=multisig,
amount=amount, amount=txout.value,
address_n=parse_path(derivation + "/%d/%d" % index), address_n=full_path,
script_type=script_type) script_type=script_type)
return txoutputtype return txoutputtype
def create_output_by_address(): def create_output_by_address():
txoutputtype = TxOutputType() txoutputtype = TxOutputType()
txoutputtype.amount = amount txoutputtype.amount = txout.value
if _type == TYPE_SCRIPT: if address:
txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN
txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(o)
elif _type == TYPE_ADDRESS:
txoutputtype.script_type = OutputScriptType.PAYTOADDRESS txoutputtype.script_type = OutputScriptType.PAYTOADDRESS
txoutputtype.address = address txoutputtype.address = address
else:
txoutputtype.script_type = OutputScriptType.PAYTOOPRETURN
txoutputtype.op_return_data = trezor_validate_op_return_output_and_get_data(txout)
return txoutputtype return txoutputtype
outputs = [] outputs = []
has_change = False has_change = False
any_output_on_change_branch = is_any_tx_output_on_change_branch(tx) any_output_on_change_branch = is_any_tx_output_on_change_branch(tx)
for o in tx.outputs(): for txout in tx.outputs():
_type, address, amount = o.type, o.address, o.value address = txout.address
use_create_by_derivation = False use_create_by_derivation = False
info = tx.output_info.get(address) if txout.is_mine and not has_change:
if info is not None and not has_change:
index, xpubs, m = info.address_index, info.sorted_xpubs, info.num_sig
# prioritise hiding outputs on the 'change' branch from user # prioritise hiding outputs on the 'change' branch from user
# because no more than one change address allowed # because no more than one change address allowed
# note: ^ restriction can be removed once we require fw # note: ^ restriction can be removed once we require fw
# that has https://github.com/trezor/trezor-mcu/pull/306 # 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 use_create_by_derivation = True
has_change = True has_change = True
@ -471,17 +452,17 @@ class TrezorPlugin(HW_PluginBase):
return outputs return outputs
def electrum_tx_to_txtype(self, tx, xpub_path): def electrum_tx_to_txtype(self, tx: Optional[Transaction]):
t = TransactionType() t = TransactionType()
if tx is None: if tx is None:
# probably for segwit input and we don't need this prev txn # probably for segwit input and we don't need this prev txn
return t return t
d = deserialize(tx.raw) tx.deserialize()
t.version = d['version'] t.version = tx.version
t.lock_time = d['lockTime'] t.lock_time = tx.locktime
t.inputs = self.tx_inputs(tx, xpub_path) t.inputs = self.tx_inputs(tx)
t.bin_outputs = [ t.bin_outputs = [
TxOutputBinType(amount=vout['value'], script_pubkey=bfh(vout['scriptPubKey'])) TxOutputBinType(amount=o.value, script_pubkey=o.scriptpubkey)
for vout in d['outputs'] for o in tx.outputs()
] ]
return t return t

2
electrum/plugins/trustedcoin/cmdline.py

@ -30,7 +30,7 @@ from .trustedcoin import TrustedCoinPlugin
class Plugin(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): if not isinstance(wallet, self.wallet_class):
return return
if not wallet.can_sign_without_server(): 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 time
import hashlib import hashlib
from collections import defaultdict 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 urljoin
from urllib.parse import quote 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.bitcoin import TYPE_ADDRESS
from electrum.bip32 import BIP32Node, xpub_type from electrum.bip32 import BIP32Node, xpub_type
from electrum.crypto import sha256 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.mnemonic import Mnemonic, seed_type, is_any_2fa_seed_type
from electrum.wallet import Multisig_Wallet, Deterministic_Wallet from electrum.wallet import Multisig_Wallet, Deterministic_Wallet
from electrum.i18n import _ from electrum.i18n import _
@ -50,6 +50,8 @@ from electrum.network import Network
from electrum.base_wizard import BaseWizard, WizardWalletPasswordSetting from electrum.base_wizard import BaseWizard, WizardWalletPasswordSetting
from electrum.logging import Logger from electrum.logging import Logger
from .legacy_tx_format import serialize_tx_in_legacy_format
def get_signing_xpub(xtype): def get_signing_xpub(xtype):
if not constants.net.TESTNET: if not constants.net.TESTNET:
@ -259,6 +261,8 @@ server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VER
class Wallet_2fa(Multisig_Wallet): class Wallet_2fa(Multisig_Wallet):
plugin: 'TrustedCoinPlugin'
wallet_type = '2fa' wallet_type = '2fa'
def __init__(self, storage, *, config): 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)) raise Exception('too high trustedcoin fee ({} for {} txns)'.format(price, n))
return price return price
def make_unsigned_transaction(self, coins, outputs, fixed_fee=None, def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
change_addr=None, is_sweep=False): outputs: List[PartialTxOutput], fee=None,
change_addr: str = None, is_sweep=False) -> PartialTransaction:
mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction( mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction(
self, coins, o, fixed_fee, change_addr) self, coins=coins, outputs=o, fee=fee, change_addr=change_addr)
fee = self.extra_fee() if not is_sweep else 0 extra_fee = self.extra_fee() if not is_sweep else 0
if fee: if extra_fee:
address = self.billing_info['billing_address_segwit'] 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: try:
tx = mk_tx(outputs + [fee_output]) tx = mk_tx(outputs + [fee_output])
except NotEnoughFunds: except NotEnoughFunds:
# TrustedCoin won't charge if the total inputs is # TrustedCoin won't charge if the total inputs is
# lower than their fee # lower than their fee
tx = mk_tx(outputs) tx = mk_tx(outputs)
if tx.input_value() >= fee: if tx.input_value() >= extra_fee:
raise raise
self.logger.info("not charging for this tx") self.logger.info("not charging for this tx")
else: else:
tx = mk_tx(outputs) tx = mk_tx(outputs)
return tx return tx
def on_otp(self, tx, otp): def on_otp(self, tx: PartialTransaction, otp):
if not otp: if not otp:
self.logger.info("sign_transaction: no auth code") self.logger.info("sign_transaction: no auth code")
return return
otp = int(otp) otp = int(otp)
long_user_id, short_id = self.get_user_id() long_user_id, short_id = self.get_user_id()
raw_tx = tx.serialize() raw_tx = serialize_tx_in_legacy_format(tx, wallet=self)
try: try:
r = server.sign(short_id, raw_tx, otp) r = server.sign(short_id, raw_tx, otp)
except TrustedCoinException as e: except TrustedCoinException as e:
@ -350,8 +355,9 @@ class Wallet_2fa(Multisig_Wallet):
else: else:
raise raise
if r: if r:
raw_tx = r.get('transaction') received_raw_tx = r.get('transaction')
tx.update(raw_tx) received_tx = Transaction(received_raw_tx)
tx.combine_with_other_psbt(received_tx)
self.logger.info(f"twofactor: is complete {tx.is_complete()}") self.logger.info(f"twofactor: is complete {tx.is_complete()}")
# reset billing_info # reset billing_info
self.billing_info = None self.billing_info = None
@ -457,15 +463,16 @@ class TrustedCoinPlugin(BasePlugin):
self.logger.info("twofactor: xpub3 not needed") self.logger.info("twofactor: xpub3 not needed")
return return
def wrapper(tx): def wrapper(tx):
assert tx
self.prompt_user_for_otp(wallet, tx, on_success, on_failure) self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
return wrapper return wrapper
@hook @hook
def get_tx_extra_fee(self, wallet, tx): def get_tx_extra_fee(self, wallet, tx: Transaction):
if type(wallet) != Wallet_2fa: if type(wallet) != Wallet_2fa:
return return
for o in tx.outputs(): 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 return o.address, o.value
def finish_requesting(func): def finish_requesting(func):

3
electrum/scripts/bip70.py

@ -7,6 +7,7 @@ import tlslite
from electrum.transaction import Transaction from electrum.transaction import Transaction
from electrum import paymentrequest from electrum import paymentrequest
from electrum import paymentrequest_pb2 as pb2 from electrum import paymentrequest_pb2 as pb2
from electrum.bitcoin import address_to_script
chain_file = 'mychain.pem' chain_file = 'mychain.pem'
cert_file = 'mycert.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: with open(cert_file, 'r') as f:
rsakey = tlslite.utils.python_rsakey.Python_RSAKey.parsePEM(f.read()) 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) 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): def decode(hrp, addr):
"""Decode a segwit address.""" """Decode a segwit address."""
if addr is None:
return (None, None)
hrpgot, data = bech32_decode(addr) hrpgot, data = bech32_decode(addr)
if hrpgot != hrp: if hrpgot != hrp:
return (None, None) 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): async def _get_transaction(self, tx_hash, *, allow_server_not_finding_tx=False):
self._requests_sent += 1 self._requests_sent += 1
try: try:
result = await self.network.get_transaction(tx_hash) raw_tx = await self.network.get_transaction(tx_hash)
except UntrustedServerReturnedError as e: except UntrustedServerReturnedError as e:
# most likely, "No such mempool or blockchain transaction" # most likely, "No such mempool or blockchain transaction"
if allow_server_not_finding_tx: if allow_server_not_finding_tx:
@ -219,7 +219,7 @@ class Synchronizer(SynchronizerBase):
raise raise
finally: finally:
self._requests_answered += 1 self._requests_answered += 1
tx = Transaction(result) tx = Transaction(raw_tx)
try: try:
tx.deserialize() # see if raises tx.deserialize() # see if raises
except Exception as e: 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()})") raise SynchronizerFailure(f"received tx does not match expected txid ({tx_hash} != {tx.txid()})")
tx_height = self.requested_tx.pop(tx_hash) tx_height = self.requested_tx.pop(tx_hash)
self.wallet.receive_tx_callback(tx_hash, tx, tx_height) 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 # callbacks
self.wallet.network.trigger_callback('new_transaction', self.wallet, tx) 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" echo "alice pays"
$alice lnpay $request $alice lnpay $request
sleep 2 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") request=$($bob add_lightning_request 0.01 -m "blah2")
echo "alice pays again" echo "alice pays again"
$alice lnpay $request $alice lnpay $request
@ -224,7 +224,7 @@ if [[ $1 == "breach_with_unspent_htlc" ]]; then
echo "SETTLE_DELAY did not work, $settled != 0" echo "SETTLE_DELAY did not work, $settled != 0"
exit 1 exit 1
fi fi
ctx=$($alice get_channel_ctx $channel | jq '.hex' | tr -d '"') ctx=$($alice get_channel_ctx $channel)
sleep 5 sleep 5
settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length') settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "1" ]]; then if [[ "$settled" != "1" ]]; then
@ -251,7 +251,7 @@ if [[ $1 == "breach_with_spent_htlc" ]]; then
echo "alice pays bob" echo "alice pays bob"
invoice=$($bob add_lightning_request 0.05 -m "test") invoice=$($bob add_lightning_request 0.05 -m "test")
$alice lnpay $invoice --timeout=1 || true $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') settled=$($alice list_channels | jq '.[] | .local_htlcs | .settles | length')
if [[ "$settled" != "0" ]]; then if [[ "$settled" != "0" ]]; then
echo "SETTLE_DELAY did not work, $settled != 0" 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 xkey1, xtype1 in xprvs:
for xkey2, xtype2 in xprvs: for xkey2, xtype2 in xprvs:
self.assertEqual(xkey2, cmds._run('convert_xkey', (xkey1, xtype2))) 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): def test_fee(self):
alice_channel, bob_channel = create_test_channels(253, 10000000000, 5000000000) 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): class TestChannel(ElectrumTestCase):
maxDiff = 999 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, get_compressed_pubkey_from_bech32, split_host_port, ConnStringFormatError,
ScriptHtlc, extract_nodeid, calc_onchain_fees, UpdateAddHtlc) ScriptHtlc, extract_nodeid, calc_onchain_fees, UpdateAddHtlc)
from electrum.util import bh2u, bfh from electrum.util import bh2u, bfh
from electrum.transaction import Transaction from electrum.transaction import Transaction, PartialTransaction
from . import ElectrumTestCase from . import ElectrumTestCase
@ -570,7 +570,7 @@ class TestLNUtil(ElectrumTestCase):
localhtlcsig=bfh(local_sig), localhtlcsig=bfh(local_sig),
payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout
witness_script=htlc) 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) return str(our_htlc_tx)
def test_commitment_tx_with_one_output(self): def test_commitment_tx_with_one_output(self):
@ -669,7 +669,7 @@ class TestLNUtil(ElectrumTestCase):
ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
self.assertEqual(str(our_commit_tx), ref_commit_tx_str) 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 type(remote_pubkey) is bytes
assert len(remote_pubkey) == 33 assert len(remote_pubkey) == 33
assert type(remote_signature) is str assert type(remote_signature) is str
@ -678,10 +678,7 @@ class TestLNUtil(ElectrumTestCase):
assert len(pubkey) == 33 assert len(pubkey) == 33
assert len(privkey) == 33 assert len(privkey) == 33
tx.sign({bh2u(pubkey): (privkey[:-1], True)}) tx.sign({bh2u(pubkey): (privkey[:-1], True)})
pubkeys, _x_pubkeys = tx.get_sorted_pubkeys(tx.inputs()[0]) tx.add_signature_to_txin(txin_idx=0, signing_pubkey=remote_pubkey.hex(), sig=remote_signature + "01")
index_of_pubkey = pubkeys.index(bh2u(remote_pubkey))
tx._inputs[0]["signatures"][index_of_pubkey] = remote_signature + "01"
tx.raw = None
def test_get_compressed_pubkey_from_bech32(self): 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', 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 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.bitcoin import TYPE_ADDRESS
from electrum.keystore import xpubkey_to_address
from electrum.util import bh2u, bfh 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 . import ElectrumTestCase, TestCaseForTestnet
from .test_bitcoin import needs_test_with_all_ecc_implementations from .test_bitcoin import needs_test_with_all_ecc_implementations
unsigned_blob = '45505446ff0001000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000005701ff4c53ff0488b21e03ef2afea18000000089689bff23e1e7fb2f161daa37270a97a3d8c2e537584b2d304ecb47b86d21fc021b010d3bd425f8cf2e04824bfdf1f1f5ff1d51fadd9a41f9e3fb8dd3403b1bfe00000000ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700" v2_blob = "0200000001191601a44a81e061502b7bfbc6eaa1cef6d1e6af5308ef96c9342f71dbf4b9b5000000006b483045022100a6d44d0a651790a477e75334adfb8aae94d6612d01187b2c02526e340a7fd6c8022028bdf7a64a54906b13b145cd5dab21a26bd4b85d6044e9b97bceab5be44c2a9201210253e8e0254b0c95776786e40984c1aa32a7d03efa6bdacdea5f421b774917d346feffffff026b20fa04000000001976a914024db2e87dd7cfd0e5f266c5f212e21a31d805a588aca0860100000000001976a91421919b94ae5cefcdf0271191459157cdb41c4cbf88aca6240700"
signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000" signed_segwit_blob = "01000000000101b66d722484f2db63e827ebf41d02684fed0c6550e85015a6c9d41ef216a8a6f00000000000fdffffff0280c3c90100000000160014b65ce60857f7e7892b983851c2a8e3526d09e4ab64bac30400000000160014c478ebbc0ab2097706a98e10db7cf101839931c4024730440220789c7d47f876638c58d98733c30ae9821c8fa82b470285dcdf6db5994210bf9f02204163418bbc44af701212ad42d884cc613f3d3d831d2d0cc886f767cca6e0235e012103083a6dc250816d771faa60737bfe78b23ad619f6b458e0a1f1688e3a0605e79c00000000"
@ -58,80 +64,35 @@ class TestBCDataStream(ElectrumTestCase):
class TestTransaction(ElectrumTestCase): class TestTransaction(ElectrumTestCase):
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
def test_tx_unsigned(self): def test_tx_update_signatures(self):
expected = { tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA")
'inputs': [{ tx.inputs()[0].script_type = 'p2pkh'
'type': 'p2pkh', tx.inputs()[0].pubkeys = [bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')]
'address': '1446oU3z268EeFgfcwJv6X2VBXHfoYxfuD', tx.inputs()[0].num_sig = 1
'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)
tx.update_signatures(signed_blob_signatures) tx.update_signatures(signed_blob_signatures)
self.assertEqual(tx.raw, signed_blob) self.assertEqual(tx.serialize(), signed_blob)
tx.update(unsigned_blob)
tx.raw = None
blob = str(tx)
self.assertEqual(transaction.deserialize(blob), expected)
@needs_test_with_all_ecc_implementations @needs_test_with_all_ecc_implementations
def test_tx_signed(self): def test_tx_deserialize_for_signed_network_tx(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
}
tx = transaction.Transaction(signed_blob) tx = transaction.Transaction(signed_blob)
self.assertEqual(tx.deserialize(), expected) tx.deserialize()
self.assertEqual(tx.deserialize(), None) self.assertEqual(1, tx.version)
self.assertEqual(tx.as_dict(), {'hex': signed_blob, 'complete': True, 'final': True}) 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) 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_total_size(), 193)
self.assertEqual(tx.estimated_base_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_weight(), 561)
self.assertEqual(tx.estimated_size(), 141) 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): def test_version_field(self):
tx = transaction.Transaction(v2_blob) tx = transaction.Transaction(v2_blob)
self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe") self.assertEqual(tx.txid(), "b97f9180173ab141b61b9f944d841e60feec691d6daab4d4d932b24dd36606fe")
def test_tx_from_str(self): def test_convert_tx_str_to_hex(self):
# json dict
self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',
tx_from_str("""{
"complete": true,
"final": false,
"hex": "020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600"
}
""")
)
# raw hex # raw hex
self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600', self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600',
tx_from_str('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600')) convert_tx_str_to_hex('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600'))
# base43 # base43
self.assertEqual('020000000001012005273af813ba23b0c205e4b145e525c280dd876e061f35bff7db9b2e0043640100000000fdffffff02d885010000000000160014e73f444b8767c84afb46ef4125d8b81d2542a53d00e1f5050000000017a914052ed032f5c74a636ed5059611bb90012d40316c870247304402200c628917673d75f05db893cc377b0a69127f75e10949b35da52aa1b77a14c350022055187adf9a668fdf45fc09002726ba7160e713ed79dddcd20171308273f1a2f1012103cb3e00561c3439ccbacc033a72e0513bcfabff8826de0bc651d661991ade6171049e1600', 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): def test_get_address_from_output_script(self):
# the inverse of this test is in test_bitcoin: test_address_to_script # 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_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 # bech32 native segwit
# test vectors from BIP-0173 # test vectors from BIP-0173
self.assertEqual((ADDR, 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6')) self.assertEqual('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4', addr_from_script('0014751e76e8199196d454941c45d1b3a323f1433bd6'))
self.assertEqual((ADDR, 'bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx'), addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6')) self.assertEqual('bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx', addr_from_script('5128751e76e8199196d454941c45d1b3a323f1433bd6751e76e8199196d454941c45d1b3a323f1433bd6'))
self.assertEqual((ADDR, 'bc1sw50qa3jx3s'), addr_from_script('6002751e')) self.assertEqual('bc1sw50qa3jx3s', addr_from_script('6002751e'))
self.assertEqual((ADDR, 'bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj'), addr_from_script('5210751e76e8199196d454941c45d1b3a323')) self.assertEqual('bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj', addr_from_script('5210751e76e8199196d454941c45d1b3a323'))
# almost but not quite # almost but not quite
self.assertEqual((SCRIPT, '0013751e76e8199196d454941c45d1b3a323f1433b'), addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b')) self.assertEqual(None, addr_from_script('0013751e76e8199196d454941c45d1b3a323f1433b'))
# base58 p2pkh # base58 p2pkh
self.assertEqual((ADDR, '14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG'), addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac')) self.assertEqual('14gcRovpkCoGkCNBivQBvw7eso7eiNAbxG', addr_from_script('76a91428662c67561b95c79d2257d2a93d9d151c977e9188ac'))
self.assertEqual((ADDR, '1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv'), addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac')) self.assertEqual('1BEqfzh4Y3zzLosfGhw1AsqbEKVW6e1qHv', addr_from_script('76a914704f4b81cadb7bf7e68c08cd3657220f680f863c88ac'))
# almost but not quite # almost but not quite
self.assertEqual((SCRIPT, '76a9130000000000000000000000000000000000000088ac'), addr_from_script('76a9130000000000000000000000000000000000000088ac')) self.assertEqual(None, addr_from_script('76a9130000000000000000000000000000000000000088ac'))
# base58 p2sh # base58 p2sh
self.assertEqual((ADDR, '35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT'), addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487')) self.assertEqual('35ZqQJcBQMZ1rsv8aSuJ2wkC7ohUCQMJbT', addr_from_script('a9142a84cf00d47f699ee7bbc1dea5ec1bdecb4ac15487'))
self.assertEqual((ADDR, '3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji'), addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387')) self.assertEqual('3PyjzJ3im7f7bcV724GR57edKDqoZvH7Ji', addr_from_script('a914f47c8954e421031ad04ecd8e7752c9479206b9d387'))
# almost but not quite # almost but not quite
self.assertEqual((SCRIPT, 'a912f47c8954e421031ad04ecd8e7752c947920687'), addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687')) self.assertEqual(None, addr_from_script('a912f47c8954e421031ad04ecd8e7752c947920687'))
# p2pk # p2pk
self.assertEqual((PUBKEY, '0289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8b'), addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) self.assertEqual(None, addr_from_script('210289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))
self.assertEqual((PUBKEY, '045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120'), addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac')) self.assertEqual(None, addr_from_script('41045485b0b076848af1209e788c893522a90f3df77c1abac2ca545846a725e6c3da1f7743f55a1bc3b5f0c7e0ee4459954ec0307022742d60032b13432953eb7120ac'))
# almost but not quite # almost but not quite
self.assertEqual((SCRIPT, '200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'), addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac')) self.assertEqual(None, addr_from_script('200289e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751cac'))
self.assertEqual((SCRIPT, '210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'), addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac')) self.assertEqual(None, addr_from_script('210589e14468d94537493c62e2168318b568912dec0fb95609afd56f2527c2751c8bac'))
##### #####
@ -811,45 +749,54 @@ class TestTransaction(ElectrumTestCase):
# txns from Bitcoin Core ends <--- # txns from Bitcoin Core ends <---
class TestTransactionTestnet(TestCaseForTestnet): class TestLegacyPartialTxFormat(TestCaseForTestnet):
def _run_naive_tests_on_tx(self, raw_tx, txid): def setUp(self):
tx = transaction.Transaction(raw_tx) super().setUp()
self.assertEqual(txid, tx.txid()) self.config = SimpleConfig({'electrum_path': self.electrum_path})
self.assertEqual(raw_tx, tx.serialize())
self.assertTrue(tx.estimated_size() >= 0) def test_trustedcoin_legacy_2fa_psbt_to_legacy_partial_tx(self):
from .test_wallet_vertical import WalletIntegrityHelper
# partial txns using our partial format ---> seed_words = 'kiss live scene rude gate step hip quarter bunker oxygen motor glove'
# NOTE: our partial format contains xpubs, and xpubs have version bytes, self.assertEqual(seed_type(seed_words), '2fa')
# and version bytes encode the network as well; so these are network-sensitive!
xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')
def test_txid_partial_segwit_p2wpkh(self): ks1 = keystore.from_xprv(xprv1)
raw_tx = '45505446ff000100000000010115a847356cbb44be67f345965bb3f2589e2fec1c9a0ada21fd28225dcc602e8f0100000000fdffffff02f6fd1200000000001600149c756aa33f4f89418b33872a973274b5445c727b80969800000000001600140f9de573bc679d040e763d13f0250bd03e625f6ffeffffffff9095ab000000000000000201ff53ff045f1cf6014af5fa07800000002fa3f450ba41799b9b62642979505817783a9b6c656dc11cd0bb4fa362096808026adc616c25a4d0a877d1741eb1db9cef65c15118bd7d5f31bf65f319edda81840100c8000f391400' ks2 = keystore.from_xprv(xprv2)
txid = '63ff7e99d85d8e33f683e6ec84574bdf8f5111078a5fe900893e019f9a7f95c3' long_user_id, short_id = trustedcoin.get_user_id(
self._run_naive_tests_on_tx(raw_tx, txid) {'x1/': {'xpub': xpub1},
'x2/': {'xpub': xpub2}})
def test_txid_partial_segwit_p2wpkh_p2sh_simple(self): xtype = bip32.xpub_type(xpub1)
raw_tx = '45505446ff0001000000000101d0d23a6fbddb21cc664cb81cca96715baa4d6dbe5b7b9bcc6632f1005a7b0b840100000017160014a78a91261e71a681b6312cd184b14503a21f856afdffffff0134410f000000000017a914d6514ca17ecc31952c990daf96e307fbc58529cd87feffffffff40420f000000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c30000020011391400' xpub3 = trustedcoin.make_xpub(trustedcoin.get_signing_xpub(xtype), long_user_id)
txid = '2739f2e7fde9b8ec73fce4aee53722cc7683312d1321ded073284c51fadf44df' ks3 = keystore.from_xpub(xpub3)
self._run_naive_tests_on_tx(raw_tx, txid)
wallet = WalletIntegrityHelper.create_multisig_wallet([ks1, ks2, ks3], '2of3', config=self.config)
def test_txid_partial_segwit_p2wpkh_p2sh_mixed_outputs(self):
raw_tx = '45505446ff00010000000001011dcac788f24b84d771b60c44e1f9b6b83429e50f06e1472d47241922164013b00100000017160014801d28ca6e2bde551112031b6cb75de34f10851ffdffffff0440420f00000000001600140f9de573bc679d040e763d13f0250bd03e625f6fc0c62d000000000017a9142899f6484e477233ce60072fc185ef4c1f2c654487809698000000000017a914d40f85ba3c8fa0f3615bcfa5d6603e36dfc613ef87712d19040000000017a914e38c0cffde769cb65e72cda1c234052ae8d2254187feffffffff6ad1ee040000000000000201ff53ff044a5262033601222e800000001618aa51e49a961f63fd111f64cd4a7e792c1d7168be7a07703de505ebed2cf70286ebbe755767adaa5835f4d78dec1ee30849d69eacfe80b7ee6b1585279536c301000c000f391400' 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')
txid = 'ba5c88e07a4025a39ad3b85247cbd4f556a70d6312b18e04513c7cec9d45d6ac' tx.add_info_from_wallet(wallet)
self._run_naive_tests_on_tx(raw_tx, txid) raw_tx = serialize_tx_in_legacy_format(tx, wallet=wallet)
self.assertEqual('45505446ff000200000001caaac6b5eb916e3067d0224f942fb331ce1dcfb4031cfb479e7941dcf95e409801000000fd53010001ff01ff483045022100d03c35cb293b01da7be0d771cedafa891ed5d70a677cfccd69671356f6bebedc02200b2a576c4cd1c642480756ce1fd660acaf0ba526193a3099e1f81cdd01ad2093014d0201524c53ff043587cf0182078976800000016fa128bdc0a5a46fc7af1afd205db6207191d4c4051b6edbd06413cdc0b1a2df036a4e4cbe7bc466dd653784c86b37749bec3e828029cea34c15690956fd61576f000000004c53ff043587cf0000000000000000009ce327095ab9eebb8227b737f07e8fb66af04b8f3e91c4093c487bd30fe87c24020d210dd3ad80d29c3624a91896d5f61b7b61f332dec4f7c45431aeef805e0fa7000000004c53ff043587cf018207897680000000939f6098ce007ea492c14ea2421ba0fdfef06c21973971865e6e63bce892f7dc0261b2f1ef2dc72d30931ebdfd381914e31ed5a65cda0d470b0ad143d33df8f87b0000000053aefdffffff03a086010000000000160014f9f04b4652a4f6ff3dc55126db1274906d7072f34c0b03000000000017a914f29f7cb897101dc69afd58df9bb2e5b4d4339f4d87400d0300000000001976a914fae2c8965e7e239e9c88ac514de70dec0b0f160888ac122b1800',
def test_txid_partial_issue_5366(self): raw_tx)
raw_tx = '45505446ff000200000000010127523d70642dabd999fb43191ff6763f5b04150ba4cf38d2cfb53edf6a40ac4f0100000000fdffffff013286010000000000160014e79c7ac0b390a9caf52dc002e1095a5fbc042a18feffffffffa08601000000000000000201ff57ff045f1cf60157e9eb7a8000000038fa0b3a9c155ff3390ca0d639783d97af3b3bf66ebb69a31dfe8317fae0a7fe0324bc048fc0002253dfec9d6299711d708175f950ecee8e09db3518a5685741830000ffffcf01010043281700'
txid = 'a0c159616073dc7a4a482092dab4e8516c83dddb769b65919f23f6df63d33eb8' def test_trustedcoin_segwit_2fa_psbt_to_legacy_partial_tx(self):
self._run_naive_tests_on_tx(raw_tx, txid) from .test_wallet_vertical import WalletIntegrityHelper
seed_words = 'universe topic remind silver february ranch shine worth innocent cattle enhance wise'
# end partial txns <--- self.assertEqual(seed_type(seed_words), '2fa_segwit')
xprv1, xpub1, xprv2, xpub2 = trustedcoin.TrustedCoinPlugin.xkeys_from_seed(seed_words, '')
class NetworkMock(object): ks1 = keystore.from_xprv(xprv1)
ks2 = keystore.from_xprv(xprv2)
def __init__(self, unspent): long_user_id, short_id = trustedcoin.get_user_id(
self.unspent = unspent {'x1/': {'xpub': xpub1},
'x2/': {'xpub': xpub2}})
def synchronous_send(self, arg): xtype = bip32.xpub_type(xpub1)
return self.unspent 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] addr0 = wallet.get_receiving_addresses()[0]
self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', addr0) self.assertEqual('bc1q2ccr34wzep58d4239tl3x3734ttle92a8srmuw', addr0)
self.assertEqual('p2wpkh:L4jkdiXszG26SUYvwwJhzGwg37H2nLhrbip7u6crmgNeJysv5FHL', 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())) self.assertEqual(2, len(wallet.get_receiving_addresses()))
# also test addr deletion # also test addr deletion
wallet.delete_address('bc1qnp78h78vp92pwdwq5xvh8eprlga5q8gu66960c') 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): class MyEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
# note: this does not get called for namedtuples :( https://bugs.python.org/issue30343 # 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): if isinstance(obj, Transaction):
return obj.as_dict() return obj.serialize()
if isinstance(obj, TxOutput):
return obj.to_legacy_tuple()
if isinstance(obj, Satoshis): if isinstance(obj, Satoshis):
return str(obj) return str(obj)
if isinstance(obj, Fiat): if isinstance(obj, Fiat):

507
electrum/wallet.py

@ -38,7 +38,7 @@ import operator
from functools import partial from functools import partial
from numbers import Number from numbers import Number
from decimal import Decimal 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 .i18n import _
from .bip32 import BIP32Node from .bip32 import BIP32Node
@ -50,7 +50,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from .simple_config import SimpleConfig 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) is_minikey, relayfee, dust_threshold)
from .crypto import sha256d from .crypto import sha256d
from . import keystore from . import keystore
@ -58,7 +58,8 @@ from .keystore import load_keystore, Hardware_KeyStore, KeyStore
from .util import multisig_type from .util import multisig_type
from .storage import StorageEncryptionVersion, WalletStorage from .storage import StorageEncryptionVersion, WalletStorage
from . import transaction, bitcoin, coinchooser, paymentrequest, ecc, bip32 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 .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) 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): def _append_utxos_to_inputs(inputs: List[PartialTxInput], network: 'Network', pubkey, txin_type, imax):
if txin_type != 'p2pk': if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
address = bitcoin.pubkey_to_address(txin_type, pubkey) address = bitcoin.pubkey_to_address(txin_type, pubkey)
scripthash = bitcoin.address_to_scripthash(address) scripthash = bitcoin.address_to_scripthash(address)
else: elif txin_type == 'p2pk':
script = bitcoin.public_key_to_p2pk_script(pubkey) script = bitcoin.public_key_to_p2pk_script(pubkey)
scripthash = bitcoin.script_to_scripthash(script) 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)) u = network.run_from_another_thread(network.listunspent_for_scripthash(scripthash))
for item in u: for item in u:
if len(inputs) >= imax: if len(inputs) >= imax:
break break
item['address'] = address prevout_str = item['tx_hash'] + ':%d' % item['tx_pos']
item['type'] = txin_type prevout = TxOutpoint.from_str(prevout_str)
item['prevout_hash'] = item['tx_hash'] utxo = PartialTxInput(prevout=prevout)
item['prevout_n'] = int(item['tx_pos']) utxo._trusted_value_sats = int(item['value'])
item['pubkeys'] = [pubkey] utxo._trusted_address = address
item['x_pubkeys'] = [pubkey] utxo.block_height = int(item['height'])
item['signatures'] = [None] utxo.script_type = txin_type
item['num_sig'] = 1 utxo.pubkeys = [bfh(pubkey)]
inputs.append(item) 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 sweep_preparations(privkeys, network: 'Network', imax=100):
def find_utxos_for_privkey(txin_type, privkey, compressed): def find_utxos_for_privkey(txin_type, privkey, compressed):
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=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 keypairs[pubkey] = privkey, compressed
inputs = [] inputs = [] # type: List[PartialTxInput]
keypairs = {} keypairs = {}
for sec in privkeys: for sec in privkeys:
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec) txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
@ -134,24 +140,27 @@ def sweep_preparations(privkeys, network: 'Network', imax=100):
return inputs, keypairs return inputs, keypairs
def sweep(privkeys, network: 'Network', config: 'SimpleConfig', recipient, fee=None, imax=100, def sweep(privkeys, *, network: 'Network', config: 'SimpleConfig',
*, locktime=None, tx_version=None): to_address: str, fee: int = None, imax=100,
locktime=None, tx_version=None) -> PartialTransaction:
inputs, keypairs = sweep_preparations(privkeys, network, imax) 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: if fee is None:
outputs = [TxOutput(TYPE_ADDRESS, recipient, total)] outputs = [PartialTxOutput(scriptpubkey=bfh(bitcoin.address_to_script(to_address)),
tx = Transaction.from_io(inputs, outputs) value=total)]
tx = PartialTransaction.from_io(inputs, outputs)
fee = config.estimate_fee(tx.estimated_size()) fee = config.estimate_fee(tx.estimated_size())
if total - fee < 0: if total - fee < 0:
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee)) raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
if total - fee < dust_threshold(network): 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))) 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: if locktime is None:
locktime = get_locktime_for_new_transaction(network) 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.set_rbf(True)
tx.sign(keypairs) tx.sign(keypairs)
return tx return tx
@ -231,9 +240,13 @@ class Abstract_Wallet(AddressSynchronizer):
self.receive_requests = storage.get('payment_requests', {}) self.receive_requests = storage.get('payment_requests', {})
self.invoices = storage.get('invoices', {}) self.invoices = storage.get('invoices', {})
# convert 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(): for invoice_key, invoice in self.invoices.items():
if invoice.get('type') == PR_TYPE_ONCHAIN: 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 invoice['outputs'] = outputs
self.calc_unused_change_addresses() self.calc_unused_change_addresses()
# save wallet type the first time # save wallet type the first time
@ -305,7 +318,7 @@ class Abstract_Wallet(AddressSynchronizer):
def get_master_public_key(self): def get_master_public_key(self):
return None return None
def basename(self): def basename(self) -> str:
return os.path.basename(self.storage.path) return os.path.basename(self.storage.path)
def test_addresses_sanity(self): def test_addresses_sanity(self):
@ -392,15 +405,28 @@ class Abstract_Wallet(AddressSynchronizer):
def is_change(self, address) -> bool: def is_change(self, address) -> bool:
if not self.is_mine(address): if not self.is_mine(address):
return False return False
return self.get_address_index(address)[0] return self.get_address_index(address)[0] == 1
def get_address_index(self, address): def get_address_index(self, address):
raise NotImplementedError() 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 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(): if self.is_watching_only():
raise Exception(_("This is a watching-only wallet")) raise Exception(_("This is a watching-only wallet"))
if not is_address(address): if not is_address(address):
@ -410,13 +436,16 @@ class Abstract_Wallet(AddressSynchronizer):
index = self.get_address_index(address) index = self.get_address_index(address)
pk, compressed = self.keystore.get_private_key(index, password) pk, compressed = self.keystore.get_private_key(index, password)
txin_type = self.get_txin_type(address) txin_type = self.get_txin_type(address)
redeem_script = self.get_redeem_script(address)
serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type) serialized_privkey = bitcoin.serialize_privkey(pk, compressed, txin_type)
return serialized_privkey, redeem_script return serialized_privkey
def get_public_keys(self, address): def get_public_keys(self, address):
return [self.get_public_key(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): def is_found(self):
return True return True
#return self.history.values() != [[]] * len(self.history) #return self.history.values() != [[]] * len(self.history)
@ -480,7 +509,7 @@ class Abstract_Wallet(AddressSynchronizer):
mempool_depth_bytes=exp_n, 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) confirmed_only = self.config.get('confirmed_only', False)
utxos = self.get_utxos(domain, utxos = self.get_utxos(domain,
excluded_addresses=self.frozen_addresses, 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)] utxos = [utxo for utxo in utxos if not self.is_frozen_coin(utxo)]
return utxos 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 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 raise NotImplementedError() # implemented by subclasses
def dummy_address(self): def dummy_address(self):
@ -536,7 +565,7 @@ class Abstract_Wallet(AddressSynchronizer):
'txpos_in_block': hist_item.tx_mined_status.txpos, '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): if '!' in (x.value for x in outputs):
amount = '!' amount = '!'
else: else:
@ -676,9 +705,9 @@ class Abstract_Wallet(AddressSynchronizer):
tx_fee = item['fee_sat'] tx_fee = item['fee_sat']
item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None
if show_addresses: if show_addresses:
item['inputs'] = list(map(lambda x: dict((k, x[k]) for k in ('prevout_hash', 'prevout_n')), tx.inputs())) item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
item['outputs'] = list(map(lambda x:{'address':x.address, 'value':Satoshis(x.value)}, item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)},
tx.get_outputs_for_UI())) tx.outputs()))
# fixme: use in and out values # fixme: use in and out values
value = item['bc_value'].value value = item['bc_value'].value
if value < 0: if value < 0:
@ -756,10 +785,10 @@ class Abstract_Wallet(AddressSynchronizer):
item['capital_gain'] = Fiat(cg, fx.ccy) item['capital_gain'] = Fiat(cg, fx.ccy)
return item 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) 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): if not self.db.get_txi_addresses(tx_hash):
labels = [] labels = []
for addr in self.db.get_txo_addresses(tx_hash): 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 max_change = self.max_change_outputs if self.multiple_change else 1
return change_addrs[:max_change] return change_addrs[:max_change]
def make_unsigned_transaction(self, coins, outputs, fixed_fee=None, def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
change_addr=None, is_sweep=False): outputs: List[PartialTxOutput], fee=None,
change_addr: str = None, is_sweep=False) -> PartialTransaction:
# check outputs # check outputs
i_max = None i_max = None
for i, o in enumerate(outputs): 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 o.value == '!':
if i_max is not None: if i_max is not None:
raise Exception("More than one output set to spend max") raise Exception("More than one output set to spend max")
i_max = i 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() raise NoDynamicFeeEstimates()
for item in coins: for item in coins:
self.add_input_info(item) self.add_input_info(item)
# Fee estimator # Fee estimator
if fixed_fee is None: if fee is None:
fee_estimator = self.config.estimate_fee fee_estimator = self.config.estimate_fee
elif isinstance(fixed_fee, Number): elif isinstance(fee, Number):
fee_estimator = lambda size: fixed_fee fee_estimator = lambda size: fee
elif callable(fixed_fee): elif callable(fee):
fee_estimator = fixed_fee fee_estimator = fee
else: else:
raise Exception('Invalid argument fixed_fee: %s' % fixed_fee) raise Exception(f'Invalid argument fee: {fee}')
if i_max is None: if i_max is None:
# Let the coin chooser select the coins to spend # 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() base_tx = self.get_unconfirmed_base_tx_for_batching()
if self.config.get('batch_rbf', False) and base_tx: 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: # 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 is_local = self.get_tx_height(base_tx.txid()).height == TX_HEIGHT_LOCAL
base_tx = Transaction(base_tx.serialize()) base_tx = PartialTransaction.from_tx(base_tx)
base_tx.deserialize(force_full_parse=True) base_tx.add_info_from_wallet(self)
base_tx.remove_signatures()
base_tx.add_inputs_info(self)
base_tx_fee = base_tx.get_fee() base_tx_fee = base_tx.get_fee()
relayfeerate = Decimal(self.relayfee()) / 1000 relayfeerate = Decimal(self.relayfee()) / 1000
original_fee_estimator = fee_estimator original_fee_estimator = fee_estimator
@ -935,8 +960,12 @@ class Abstract_Wallet(AddressSynchronizer):
old_change_addrs = [] old_change_addrs = []
# change address. if empty, coin_chooser will set it # change address. if empty, coin_chooser will set it
change_addrs = self.get_change_addresses_for_new_transaction(change_addr or old_change_addrs) 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, tx = coin_chooser.make_tx(coins=coins,
fee_estimator, self.dust_threshold()) inputs=txi,
outputs=list(outputs) + txo,
change_addrs=change_addrs,
fee_estimator_vb=fee_estimator,
dust_threshold=self.dust_threshold())
else: else:
# "spend max" branch # "spend max" branch
# note: This *will* spend inputs with negative effective value (if there are any). # note: This *will* spend inputs with negative effective value (if there are any).
@ -945,25 +974,30 @@ class Abstract_Wallet(AddressSynchronizer):
# forever. see #5433 # forever. see #5433
# note: Actually it might be the case that not all UTXOs from the wallet are # note: Actually it might be the case that not all UTXOs from the wallet are
# being spent if the user manually selected UTXOs. # being spent if the user manually selected UTXOs.
sendable = sum(map(lambda x:x['value'], coins)) sendable = sum(map(lambda c: c.value_sats(), coins))
outputs[i_max] = outputs[i_max]._replace(value=0) outputs[i_max].value = 0
tx = Transaction.from_io(coins, outputs[:]) tx = PartialTransaction.from_io(list(coins), list(outputs))
fee = fee_estimator(tx.estimated_size()) fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee amount = sendable - tx.output_value() - fee
if amount < 0: if amount < 0:
raise NotEnoughFunds() raise NotEnoughFunds()
outputs[i_max] = outputs[i_max]._replace(value=amount) outputs[i_max].value = amount
tx = Transaction.from_io(coins, outputs[:]) tx = PartialTransaction.from_io(list(coins), list(outputs))
# Timelock tx to current height. # Timelock tx to current height.
tx.locktime = get_locktime_for_new_transaction(self.network) tx.locktime = get_locktime_for_new_transaction(self.network)
tx.add_info_from_wallet(self)
run_hook('make_unsigned_transaction', self, tx) run_hook('make_unsigned_transaction', self, tx)
return tx return tx
def mktx(self, outputs, password, fee=None, change_addr=None, def mktx(self, *, outputs: List[PartialTxOutput], password, fee=None, change_addr=None,
domain=None, rbf=False, nonlocal_only=False, *, tx_version=None): domain=None, rbf=False, nonlocal_only=False, tx_version=None) -> PartialTransaction:
coins = self.get_spendable_coins(domain, nonlocal_only=nonlocal_only) 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) tx.set_rbf(rbf)
if tx_version is not None: if tx_version is not None:
tx.version = tx_version tx.version = tx_version
@ -973,10 +1007,9 @@ class Abstract_Wallet(AddressSynchronizer):
def is_frozen_address(self, addr: str) -> bool: def is_frozen_address(self, addr: str) -> bool:
return addr in self.frozen_addresses return addr in self.frozen_addresses
def is_frozen_coin(self, utxo) -> bool: def is_frozen_coin(self, utxo: PartialTxInput) -> bool:
# utxo is either a txid:vout str, or a dict prevout_str = utxo.prevout.to_str()
utxo = self._utxo_str_from_utxo(utxo) return prevout_str in self.frozen_coins
return utxo in self.frozen_coins
def set_frozen_state_of_addresses(self, addrs, freeze: bool): def set_frozen_state_of_addresses(self, addrs, freeze: bool):
"""Set frozen state of the addresses to FREEZE, True or False""" """Set frozen state of the addresses to FREEZE, True or False"""
@ -990,9 +1023,9 @@ class Abstract_Wallet(AddressSynchronizer):
return True return True
return False 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""" """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? # FIXME take lock?
if freeze: if freeze:
self.frozen_coins |= set(utxos) self.frozen_coins |= set(utxos)
@ -1000,15 +1033,6 @@ class Abstract_Wallet(AddressSynchronizer):
self.frozen_coins -= set(utxos) self.frozen_coins -= set(utxos)
self.storage.put('frozen_coins', list(self.frozen_coins)) 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_until_synchronized(self, callback=None):
def wait_for_wallet(): def wait_for_wallet():
self.set_up_to_date(False) self.set_up_to_date(False)
@ -1055,7 +1079,7 @@ class Abstract_Wallet(AddressSynchronizer):
max_conf = max(max_conf, tx_age) max_conf = max(max_conf, tx_age)
return max_conf >= req_conf 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'. """Increase the miner fee of 'tx'.
'new_fee_rate' is the target min rate in sat/vbyte '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) tx_new.locktime = get_locktime_for_new_transaction(self.network)
return tx_new return tx_new
def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> Transaction: def _bump_fee_through_coinchooser(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
tx = Transaction(tx.serialize()) tx = PartialTransaction.from_tx(tx)
tx.deserialize(force_full_parse=True) # need to parse inputs tx.add_info_from_wallet(self)
tx.remove_signatures() old_inputs = list(tx.inputs())
tx.add_inputs_info(self) old_outputs = list(tx.outputs())
old_inputs = tx.inputs()[:]
old_outputs = tx.outputs()[:]
# change address # change address
old_change_addrs = [o.address for o in old_outputs if self.is_change(o.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) 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) return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
coin_chooser = coinchooser.get_coin_chooser(self.config) coin_chooser = coinchooser.get_coin_chooser(self.config)
try: try:
return coin_chooser.make_tx(coins, old_inputs, fixed_outputs, change_addrs, return coin_chooser.make_tx(coins=coins,
fee_estimator, self.dust_threshold()) inputs=old_inputs,
outputs=fixed_outputs,
change_addrs=change_addrs,
fee_estimator_vb=fee_estimator,
dust_threshold=self.dust_threshold())
except NotEnoughFunds as e: except NotEnoughFunds as e:
raise CannotBumpFee(e) raise CannotBumpFee(e)
def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> Transaction: def _bump_fee_through_decreasing_outputs(self, *, tx: Transaction, new_fee_rate) -> PartialTransaction:
tx = Transaction(tx.serialize()) tx = PartialTransaction.from_tx(tx)
tx.deserialize(force_full_parse=True) # need to parse inputs tx.add_info_from_wallet(self)
tx.remove_signatures()
tx.add_inputs_info(self)
inputs = tx.inputs() inputs = tx.inputs()
outputs = tx.outputs() outputs = list(tx.outputs())
# use own outputs # use own outputs
s = list(filter(lambda o: self.is_mine(o.address), 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(): if o.value - delta >= self.dust_threshold():
new_output_value = o.value - delta new_output_value = o.value - delta
assert isinstance(new_output_value, int) assert isinstance(new_output_value, int)
outputs[i] = o._replace(value=new_output_value) outputs[i].value = new_output_value
delta = 0 delta = 0
break break
else: else:
@ -1176,48 +1200,84 @@ class Abstract_Wallet(AddressSynchronizer):
if delta > 0: if delta > 0:
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('could not find suitable outputs')) 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() txid = tx.txid()
for i, o in enumerate(tx.outputs()): for i, o in enumerate(tx.outputs()):
address, value = o.address, o.value address, value = o.address, o.value
if o.type == TYPE_ADDRESS and self.is_mine(address): if self.is_mine(address):
break break
else: else:
return return
coins = self.get_addr_utxo(address) coins = self.get_addr_utxo(address)
item = coins.get(txid+':%d'%i) item = coins.get(TxOutpoint.from_str(txid+':%d'%i))
if not item: if not item:
return return
self.add_input_info(item) self.add_input_info(item)
inputs = [item] inputs = [item]
out_address = self.get_unused_address() or address 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) 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 raise NotImplementedError() # implemented by subclasses
def add_input_info(self, txin): def _add_input_utxo_info(self, txin: PartialTxInput, address: str) -> None:
address = self.get_txin_address(txin) if Transaction.is_segwit_input(txin):
if self.is_mine(address): if txin.witness_utxo is None:
txin['address'] = address
txin['type'] = self.get_txin_type(address)
# segwit needs value to sign
if txin.get('value') is None:
received, spent = self.get_addr_io(address) 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: if item:
txin['value'] = item[1] txin_value = item[1]
self.add_input_sig_info(txin, address) 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: def can_sign(self, tx: Transaction) -> bool:
if not isinstance(tx, PartialTransaction):
return False
if tx.is_complete(): if tx.is_complete():
return False return False
# add info to inputs if we can; otherwise we might return a false negative: # 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(): for k in self.get_keystores():
if k.can_sign(tx): if k.can_sign(tx):
return True return True
@ -1241,38 +1301,46 @@ class Abstract_Wallet(AddressSynchronizer):
tx = Transaction(raw_tx) tx = Transaction(raw_tx)
return tx return tx
def add_hw_info(self, tx: Transaction) -> None: def add_output_info(self, txout: PartialTxOutput) -> None:
# add previous tx for hw wallets address = txout.address
for txin in tx.inputs(): if not self.is_mine(address):
tx_hash = txin['prevout_hash'] is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
# segwit inputs might not be needed for some hw wallets if not is_mine:
ignore_network_issues = Transaction.is_segwit_input(txin) return
txin['prev_tx'] = self.get_input_tx(tx_hash, ignore_network_issues=ignore_network_issues) txout.script_type = self.get_txin_type(address)
# add output info for hw wallets txout.is_mine = True
info = {} txout.is_change = self.is_change(address)
xpubs = self.get_master_public_keys() if isinstance(self, Multisig_Wallet):
for o in tx.outputs(): txout.num_sig = self.m
if self.is_mine(o.address): if isinstance(self, Deterministic_Wallet):
index = self.get_address_index(o.address) if not txout.pubkeys or len(txout.pubkeys) != len(txout.bip32_paths):
pubkeys = self.get_public_keys(o.address) pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
# sort xpubs using the order of pubkeys txout.pubkeys = sorted([bfh(pk) for pk in list(pubkey_deriv_info)])
sorted_pubkeys, sorted_xpubs = zip(*sorted(zip(pubkeys, xpubs))) for pubkey_hex in pubkey_deriv_info:
num_sig = self.m if isinstance(self, Multisig_Wallet) else None ks, der_suffix = pubkey_deriv_info[pubkey_hex]
is_change = self.is_change(o.address) xfp_bytes = bfh(ks.get_root_fingerprint())
info[o.address] = TxOutputHwInfo(address_index=index, der_prefix = bip32.convert_bip32_path_to_list_of_uint32(ks.get_derivation_prefix())
sorted_xpubs=sorted_xpubs, der_full = der_prefix + list(der_suffix)
num_sig=num_sig, txout.bip32_paths[bfh(pubkey_hex)] = (xfp_bytes, der_full)
script_type=self.txin_type, if txout.redeem_script is None:
is_change=is_change) try:
tx.output_info = info redeem_script_hex = self.get_redeem_script(address)
txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
def sign_transaction(self, tx, password): 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(): if self.is_watching_only():
return return
tx.add_inputs_info(self) if not isinstance(tx, PartialTransaction):
# hardware wallets require extra info return
if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]): tx.add_info_from_wallet(self)
self.add_hw_info(tx)
# sign. start with ready keystores. # sign. start with ready keystores.
for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True): for k in sorted(self.get_keystores(), key=lambda ks: ks.ready_to_sign(), reverse=True):
try: try:
@ -1423,7 +1491,6 @@ class Abstract_Wallet(AddressSynchronizer):
self.network.trigger_callback('payment_received', self, addr, status) self.network.trigger_callback('payment_received', self, addr, status)
def make_payment_request(self, addr, amount, message, expiration): def make_payment_request(self, addr, amount, message, expiration):
from .bitcoin import TYPE_ADDRESS
timestamp = int(time.time()) timestamp = int(time.time())
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10] _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
return { return {
@ -1434,12 +1501,12 @@ class Abstract_Wallet(AddressSynchronizer):
'address':addr, 'address':addr,
'memo':message, 'memo':message,
'id':_id, '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): def sign_payment_request(self, key, alias, alias_addr, password):
req = self.receive_requests.get(key) 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) pr = paymentrequest.make_unsigned_request(req)
paymentrequest.sign_request_with_alias(pr, alias, alias_privkey) paymentrequest.sign_request_with_alias(pr, alias, alias_privkey)
req['name'] = pr.pki_data req['name'] = pr.pki_data
@ -1577,9 +1644,12 @@ class Abstract_Wallet(AddressSynchronizer):
index = self.get_address_index(addr) index = self.get_address_index(addr)
return self.keystore.decrypt_message(index, message, password) return self.keystore.decrypt_message(index, message, password)
def txin_value(self, txin): def txin_value(self, txin: TxInput) -> Optional[int]:
txid = txin['prevout_hash'] if isinstance(txin, PartialTxInput):
prev_n = txin['prevout_n'] 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): for addr in self.db.get_txo_addresses(txid):
d = self.db.get_txo_addr(txid, addr) d = self.db.get_txo_addr(txid, addr)
for n, v, cb in d: for n, v, cb in d:
@ -1597,8 +1667,8 @@ class Abstract_Wallet(AddressSynchronizer):
coins = self.get_utxos(domain) coins = self.get_utxos(domain)
now = time.time() now = time.time()
p = price_func(now) p = price_func(now)
ap = sum(self.coin_price(coin['prevout_hash'], price_func, ccy, self.txin_value(coin)) for coin in coins) ap = sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.txin_value(coin)) for coin in coins)
lp = sum([coin['value'] for coin in coins]) * p / Decimal(COIN) lp = sum([coin.value_sats() for coin in coins]) * p / Decimal(COIN)
return lp - ap return lp - ap
def average_price(self, txid, price_func, ccy): def average_price(self, txid, price_func, ccy):
@ -1684,9 +1754,6 @@ class Imported_Wallet(Simple_Wallet):
def load_keystore(self): def load_keystore(self):
self.keystore = load_keystore(self.storage, 'keystore') if self.storage.get('keystore') else None 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): def save_keystore(self):
self.storage.put('keystore', self.keystore.dump()) self.storage.put('keystore', self.keystore.dump())
@ -1795,11 +1862,11 @@ class Imported_Wallet(Simple_Wallet):
def is_mine(self, address) -> bool: def is_mine(self, address) -> bool:
return self.db.has_imported_address(address) 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 # returns None if address is not mine
return self.get_public_key(address) 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) x = self.db.get_imported_address(address)
return x.get('pubkey') if x else None return x.get('pubkey') if x else None
@ -1818,7 +1885,7 @@ class Imported_Wallet(Simple_Wallet):
continue continue
addr = bitcoin.pubkey_to_address(txin_type, pubkey) addr = bitcoin.pubkey_to_address(txin_type, pubkey)
good_addr.append(addr) 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.add_address(addr)
self.save_keystore() self.save_keystore()
if write_to_disk: if write_to_disk:
@ -1832,27 +1899,21 @@ class Imported_Wallet(Simple_Wallet):
else: else:
raise BitcoinException(str(bad_keys[0][1])) 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): def get_txin_type(self, address):
return self.db.get_imported_address(address).get('type', 'address') return self.db.get_imported_address(address).get('type', 'address')
def add_input_sig_info(self, txin, address): def _add_input_sig_info(self, txin, address):
if self.is_watching_only(): assert self.is_mine(address)
x_pubkey = 'fd' + address_to_script(address) if txin.script_type in ('unknown', 'address'):
txin['x_pubkeys'] = [x_pubkey]
txin['signatures'] = [None]
return return
if txin['type'] in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
pubkey = self.db.get_imported_address(address)['pubkey'] pubkey = self.get_public_key(address)
txin['num_sig'] = 1 if not pubkey:
txin['x_pubkeys'] = [pubkey] return
txin['signatures'] = [None] txin.pubkeys = [bfh(pubkey)]
else: 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): def pubkeys_to_address(self, pubkey):
for addr in self.db.get_imported_addresses(): for addr in self.db.get_imported_addresses():
@ -1862,6 +1923,7 @@ class Imported_Wallet(Simple_Wallet):
class Deterministic_Wallet(Abstract_Wallet): class Deterministic_Wallet(Abstract_Wallet):
def __init__(self, storage, *, config): def __init__(self, storage, *, config):
self._ephemeral_addr_to_addr_index = {} # type: Dict[str, Sequence[int]]
Abstract_Wallet.__init__(self, storage, config=config) Abstract_Wallet.__init__(self, storage, config=config)
self.gap_limit = storage.get('gap_limit', 20) self.gap_limit = storage.get('gap_limit', 20)
# generate addresses now. note that without libsecp this might block # 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) x = self.derive_pubkeys(for_change, n)
return self.pubkeys_to_address(x) 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): def create_new_address(self, for_change=False):
assert type(for_change) is bool assert type(for_change) is bool
with self.lock: with self.lock:
@ -1995,8 +2074,16 @@ class Deterministic_Wallet(Abstract_Wallet):
return False return False
return True return True
def get_address_index(self, address): def get_address_index(self, address) -> Optional[Sequence[int]]:
return self.db.get_address_index(address) 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): def get_master_public_keys(self):
return [self.get_master_public_key()] return [self.get_master_public_key()]
@ -2017,7 +2104,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
def get_public_key(self, address): def get_public_key(self, address):
sequence = self.get_address_index(address) sequence = self.get_address_index(address)
pubkey = self.get_pubkey(*sequence) pubkey = self.derive_pubkeys(*sequence)
return pubkey return pubkey
def load_keystore(self): def load_keystore(self):
@ -2028,16 +2115,6 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
xtype = 'standard' xtype = 'standard'
self.txin_type = 'p2pkh' if xtype == 'standard' else xtype 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): def get_master_public_key(self):
return self.keystore.get_master_public_key() 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) self.m, self.n = multisig_type(self.wallet_type)
Deterministic_Wallet.__init__(self, storage, config=config) 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): def get_public_keys(self, address):
sequence = self.get_address_index(address) return list(self.get_public_keys_with_deriv_info(address))
return self.get_pubkeys(*sequence)
def pubkeys_to_address(self, pubkeys): 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) 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) return transaction.multisig_script(sorted(pubkeys), self.m)
def get_redeem_script(self, address): 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) pubkeys = self.get_public_keys(address)
redeem_script = self.pubkeys_to_redeem_script(pubkeys) scriptcode = self.pubkeys_to_scriptcode(pubkeys)
return redeem_script 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): def derive_pubkeys(self, c, i):
return [k.derive_pubkey(c, i) for k in self.get_keystores()] 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): def get_fingerprint(self):
return ''.join(sorted(self.get_master_public_keys())) 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'] wallet_types = ['standard', 'multisig', 'imported']

Loading…
Cancel
Save