Browse Source

Merge pull request #8230 from SomberNight/202302_osd_tx

output script descriptors, part 1: change API of transaction.py
master
ThomasV 3 years ago committed by GitHub
parent
commit
798cd607b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 101
      electrum/bip32.py
  2. 14
      electrum/bitcoin.py
  3. 12
      electrum/commands.py
  4. 1047
      electrum/descriptor.py
  5. 43
      electrum/keystore.py
  6. 6
      electrum/lnsweep.py
  7. 8
      electrum/lnutil.py
  8. 5
      electrum/plugins/bitbox02/bitbox02.py
  9. 9
      electrum/plugins/digitalbitbox/digitalbitbox.py
  10. 19
      electrum/plugins/hw_wallet/plugin.py
  11. 5
      electrum/plugins/jade/jade.py
  12. 49
      electrum/plugins/keepkey/keepkey.py
  13. 35
      electrum/plugins/ledger/ledger.py
  14. 49
      electrum/plugins/safe_t/safe_t.py
  15. 49
      electrum/plugins/trezor/trezor.py
  16. 2
      electrum/segwit_addr.py
  17. 4
      electrum/submarine_swaps.py
  18. 22
      electrum/tests/test_bitcoin.py
  19. 390
      electrum/tests/test_descriptor.py
  20. 18
      electrum/tests/test_transaction.py
  21. 46
      electrum/tests/test_wallet_vertical.py
  22. 247
      electrum/transaction.py
  23. 173
      electrum/wallet.py

101
electrum/bip32.py

@ -2,7 +2,9 @@
# Distributed under the MIT software license, see the accompanying # Distributed under the MIT software license, see the accompanying
# file LICENCE or http://www.opensource.org/licenses/mit-license.php # file LICENCE or http://www.opensource.org/licenses/mit-license.php
import binascii
import hashlib import hashlib
import struct
from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional
from .util import bfh, BitcoinException from .util import bfh, BitcoinException
@ -122,7 +124,13 @@ class BIP32Node(NamedTuple):
child_number: bytes = b'\x00'*4 child_number: bytes = b'\x00'*4
@classmethod @classmethod
def from_xkey(cls, xkey: str, *, net=None) -> 'BIP32Node': def from_xkey(
cls,
xkey: str,
*,
net=None,
allow_custom_headers: bool = True, # to also accept ypub/zpub
) -> 'BIP32Node':
if net is None: if net is None:
net = constants.net net = constants.net
xkey = DecodeBase58Check(xkey) xkey = DecodeBase58Check(xkey)
@ -143,6 +151,8 @@ class BIP32Node(NamedTuple):
else: else:
raise InvalidMasterKeyVersionBytes(f'Invalid extended key format: {hex(header)}') raise InvalidMasterKeyVersionBytes(f'Invalid extended key format: {hex(header)}')
xtype = headers_inv[header] xtype = headers_inv[header]
if not allow_custom_headers and xtype != "standard":
raise ValueError(f"only standard xpub/xprv allowed. found custom xtype={xtype}")
if is_private: if is_private:
eckey = ecc.ECPrivkey(xkey[13 + 33:]) eckey = ecc.ECPrivkey(xkey[13 + 33:])
else: else:
@ -324,14 +334,18 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
# makes concatenating paths easier # makes concatenating paths easier
continue continue
prime = 0 prime = 0
if x.endswith("'") or x.endswith("h"): if x.endswith("'") or x.endswith("h"): # note: some implementations also accept "H", "p", "P"
x = x[:-1] x = x[:-1]
prime = BIP32_PRIME prime = BIP32_PRIME
if x.startswith('-'): if x.startswith('-'):
if prime: if prime:
raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways") raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways")
prime = BIP32_PRIME prime = BIP32_PRIME
child_index = abs(int(x)) | prime try:
x_int = int(x)
except ValueError as e:
raise ValueError(f"failed to parse bip32 path: {(str(e))}") from None
child_index = abs(x_int) | prime
if child_index > UINT32_MAX: if child_index > UINT32_MAX:
raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}") raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}")
path.append(child_index) path.append(child_index)
@ -426,3 +440,84 @@ def is_xkey_consistent_with_key_origin_info(xkey: str, *,
if bfh(root_fingerprint) != bip32node.fingerprint: if bfh(root_fingerprint) != bip32node.fingerprint:
return False return False
return True return True
class KeyOriginInfo:
"""
Object representing the origin of a key.
from https://github.com/bitcoin-core/HWI/blob/5f300d3dee7b317a6194680ad293eaa0962a3cc7/hwilib/key.py
# Copyright (c) 2020 The HWI developers
# Distributed under the MIT software license.
"""
def __init__(self, fingerprint: bytes, path: Sequence[int]) -> None:
"""
:param fingerprint: The 4 byte BIP 32 fingerprint of a parent key from which this key is derived from
:param path: The derivation path to reach this key from the key at ``fingerprint``
"""
self.fingerprint: bytes = fingerprint
self.path: Sequence[int] = path
@classmethod
def deserialize(cls, s: bytes) -> 'KeyOriginInfo':
"""
Deserialize a serialized KeyOriginInfo.
They will be serialized in the same way that PSBTs serialize derivation paths
"""
fingerprint = s[0:4]
s = s[4:]
path = list(struct.unpack("<" + "I" * (len(s) // 4), s))
return cls(fingerprint, path)
def serialize(self) -> bytes:
"""
Serializes the KeyOriginInfo in the same way that derivation paths are stored in PSBTs
"""
r = self.fingerprint
r += struct.pack("<" + "I" * len(self.path), *self.path)
return r
def _path_string(self) -> str:
strpath = self.get_derivation_path()
if len(strpath) >= 2:
assert strpath.startswith("m/")
return strpath[1:] # cut leading "m"
def to_string(self) -> str:
"""
Return the KeyOriginInfo as a string in the form <fingerprint>/<index>/<index>/...
This is the same way that KeyOriginInfo is shown in descriptors
"""
s = binascii.hexlify(self.fingerprint).decode()
s += self._path_string()
return s
@classmethod
def from_string(cls, s: str) -> 'KeyOriginInfo':
"""
Create a KeyOriginInfo from the string
:param s: The string to parse
"""
s = s.lower()
entries = s.split("/")
fingerprint = binascii.unhexlify(s[0:8])
path: Sequence[int] = []
if len(entries) > 1:
path = convert_bip32_path_to_list_of_uint32(s[9:])
return cls(fingerprint, path)
def get_derivation_path(self) -> str:
"""
Return the string for just the path
"""
return convert_bip32_intpath_to_strpath(self.path)
def get_full_int_list(self) -> List[int]:
"""
Return a list of ints representing this KeyOriginInfo.
The first int is the fingerprint, followed by the path
"""
xfp = [struct.unpack("<I", self.fingerprint)[0]]
xfp.extend(self.path)
return xfp

14
electrum/bitcoin.py

@ -421,15 +421,9 @@ def p2wsh_nested_script(witness_script: str) -> str:
return construct_script([0, wsh]) return construct_script([0, wsh])
def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str: def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
if txin_type == 'p2pkh': from . import descriptor
return public_key_to_p2pkh(bfh(pubkey), net=net) desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)
elif txin_type == 'p2wpkh': return desc.expand().address(net=net)
return public_key_to_p2wpkh(bfh(pubkey), net=net)
elif txin_type == 'p2wpkh-p2sh':
scriptSig = p2wpkh_nested_script(pubkey)
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
else:
raise NotImplementedError(txin_type)
# TODO this method is confusingly named # TODO this method is confusingly named
@ -448,7 +442,7 @@ def redeem_script_to_address(txin_type: str, scriptcode: str, *, net=None) -> st
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) -> Optional[str]:
from .transaction import get_address_from_output_script from .transaction import get_address_from_output_script
return get_address_from_output_script(bfh(script), net=net) return get_address_from_output_script(bfh(script), net=net)

12
electrum/commands.py

@ -66,6 +66,7 @@ from . import submarine_swaps
from . import GuiImportError from . import GuiImportError
from . import crypto from . import crypto
from . import constants from . import constants
from . import descriptor
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@ -394,9 +395,8 @@ class Commands:
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.script_type = txin_type desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)
txin.pubkeys = [bfh(pubkey)] txin.script_descriptor = desc
txin.num_sig = 1
inputs.append(txin) inputs.append(txin)
outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout.get('value', txout['value_sats']))) outputs = [PartialTxOutput.from_address_and_value(txout['address'], int(txout.get('value', txout['value_sats'])))
@ -420,11 +420,11 @@ class Commands:
for priv in privkey: for priv in privkey:
txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv) txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv)
pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed) pubkey = ecc.ECPrivkey(priv2).get_public_key_bytes(compressed=compressed)
address = bitcoin.pubkey_to_address(txin_type, pubkey.hex()) desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=txin_type)
address = desc.expand().address()
if address in txins_dict.keys(): if address in txins_dict.keys():
for txin in txins_dict[address]: for txin in txins_dict[address]:
txin.pubkeys = [pubkey] txin.script_descriptor = desc
txin.script_type = txin_type
tx.sign({pubkey.hex(): (priv2, compressed)}) tx.sign({pubkey.hex(): (priv2, compressed)})
return tx.serialize() return tx.serialize()

1047
electrum/descriptor.py

File diff suppressed because it is too large Load Diff

43
electrum/keystore.py

@ -36,7 +36,9 @@ from .bitcoin import deserialize_privkey, serialize_privkey, BaseDecodeError
from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput from .transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, TxInput
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, normalize_bip32_derivation, is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation,
convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info) convert_bip32_intpath_to_strpath, is_xkey_consistent_with_key_origin_info,
KeyOriginInfo)
from .descriptor import PubkeyProvider
from .ecc import string_to_number from .ecc import string_to_number
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, hash_160, SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160,
@ -179,6 +181,10 @@ class KeyStore(Logger, ABC):
""" """
pass pass
@abstractmethod
def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:
pass
def find_my_pubkey_in_txinout( def find_my_pubkey_in_txinout(
self, txinout: Union['PartialTxInput', 'PartialTxOutput'], self, txinout: Union['PartialTxInput', 'PartialTxOutput'],
*, only_der_suffix: bool = False *, only_der_suffix: bool = False
@ -302,6 +308,15 @@ class Imported_KeyStore(Software_KeyStore):
return pubkey.hex() return pubkey.hex()
return None return None
def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:
if sequence in self.keypairs:
return PubkeyProvider(
origin=None,
pubkey=sequence,
deriv_path=None,
)
return None
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)
if new_password == '': if new_password == '':
@ -403,6 +418,9 @@ class MasterPublicKeyMixin(ABC):
""" """
pass pass
def get_key_origin_info(self) -> Optional[KeyOriginInfo]:
return None
@abstractmethod @abstractmethod
def derive_pubkey(self, for_change: int, n: int) -> bytes: def derive_pubkey(self, for_change: int, n: int) -> bytes:
"""Returns pubkey at given path. """Returns pubkey at given path.
@ -532,6 +550,22 @@ class Xpub(MasterPublicKeyMixin):
) )
return bip32node.to_xpub() return bip32node.to_xpub()
def get_key_origin_info(self) -> Optional[KeyOriginInfo]:
fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(
der_suffix=[], only_der_suffix=False)
origin = KeyOriginInfo(fingerprint=fp_bytes, path=der_full)
return origin
def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:
strpath = convert_bip32_intpath_to_strpath(sequence)
strpath = strpath[1:] # cut leading "m"
bip32node = self.get_bip32_node_for_xpub()
return PubkeyProvider(
origin=self.get_key_origin_info(),
pubkey=bip32node._replace(xtype="standard").to_xkey(),
deriv_path=strpath,
)
def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node): def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node):
assert self.xpub assert self.xpub
# try to derive ourselves from what we were given # try to derive ourselves from what we were given
@ -802,6 +836,13 @@ class Old_KeyStore(MasterPublicKeyMixin, Deterministic_KeyStore):
der_full = der_prefix_ints + list(der_suffix) der_full = der_prefix_ints + list(der_suffix)
return fingerprint_bytes, der_full return fingerprint_bytes, der_full
def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:
return PubkeyProvider(
origin=None,
pubkey=self.derive_pubkey(*sequence).hex(),
deriv_path=None,
)
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)
if new_password == '': if new_password == '':

6
electrum/lnsweep.py

@ -8,6 +8,7 @@ from enum import Enum, auto
from .util import bfh from .util import bfh
from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness
from .invoices import PR_PAID from .invoices import PR_PAID
from . import descriptor
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,
@ -513,9 +514,8 @@ def create_sweeptx_their_ctx_to_remote(
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx) prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
txin = PartialTxInput(prevout=prevout) txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = val txin._trusted_value_sats = val
txin.script_type = 'p2wpkh' desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey, script_type='p2wpkh')
txin.pubkeys = [bfh(our_payment_pubkey)] txin.script_descriptor = desc
txin.num_sig = 1
sweep_inputs = [txin] sweep_inputs = [txin]
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)

8
electrum/lnutil.py

@ -21,6 +21,7 @@ from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOut
PartialTxOutput, opcodes, TxOutput) 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 . import descriptor
from .bitcoin import (push_script, redeem_script_to_address, address_to_script, from .bitcoin import (push_script, redeem_script_to_address, address_to_script,
construct_witness, construct_script) construct_witness, construct_script)
from . import segwit_addr from . import segwit_addr
@ -818,9 +819,10 @@ def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes
# commitment tx input # commitment tx input
prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos) prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)
c_input = PartialTxInput(prevout=prevout) c_input = PartialTxInput(prevout=prevout)
c_input.script_type = 'p2wsh'
c_input.pubkeys = [bfh(pk) for pk in pubkeys] ppubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys]
c_input.num_sig = 2 multi = descriptor.MultisigDescriptor(pubkeys=ppubkeys, thresh=2, is_sorted=True)
c_input.script_descriptor = descriptor.WSHDescriptor(subdescriptor=multi)
c_input._trusted_value_sats = funding_sat c_input._trusted_value_sats = funding_sat
return c_input return c_input

5
electrum/plugins/bitbox02/bitbox02.py

@ -444,9 +444,10 @@ class BitBox02Client(HardwareClientBase):
} }
) )
assert (desc := txin.script_descriptor)
if tx_script_type is None: if tx_script_type is None:
tx_script_type = txin.script_type tx_script_type = desc.to_legacy_electrum_script_type()
elif tx_script_type != txin.script_type: elif tx_script_type != desc.to_legacy_electrum_script_type():
raise Exception("Cannot mix different input script types") raise Exception("Cannot mix different input script types")
if tx_script_type == "p2wpkh": if tx_script_type == "p2wpkh":

9
electrum/plugins/digitalbitbox/digitalbitbox.py

@ -19,6 +19,7 @@ import copy
from electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot from electrum.crypto import sha256d, EncodeAES_bytes, DecodeAES_bytes, hmac_oneshot
from electrum.bitcoin import public_key_to_p2pkh from electrum.bitcoin import public_key_to_p2pkh
from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation
from electrum import descriptor
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
@ -527,7 +528,8 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
if txin.is_coinbase_input(): if txin.is_coinbase_input():
self.give_error("Coinbase not supported") # should never happen self.give_error("Coinbase not supported") # should never happen
if txin.script_type != 'p2pkh': assert (desc := txin.script_descriptor)
if desc.to_legacy_electrum_script_type() != 'p2pkh':
p2pkhTransaction = False p2pkhTransaction = False
my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin) my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)
@ -557,9 +559,10 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
tx_copy = copy.deepcopy(tx) tx_copy = copy.deepcopy(tx)
# monkey-patch method of tx_copy instance to change serialization # monkey-patch method of tx_copy instance to change serialization
def input_script(self, txin: PartialTxInput, *, estimate_size=False): def input_script(self, txin: PartialTxInput, *, estimate_size=False):
if txin.script_type == 'p2pkh': desc = txin.script_descriptor
if isinstance(desc, descriptor.PKHDescriptor):
return Transaction.get_preimage_script(txin) return Transaction.get_preimage_script(txin)
raise Exception("unsupported type %s" % txin.script_type) raise Exception(f"unsupported txin type. only p2pkh is supported. got: {desc.to_string()[:10]}")
tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction) tx_copy.input_script = input_script.__get__(tx_copy, PartialTransaction)
tx_dbb_serialized = tx_copy.serialize_to_network() tx_dbb_serialized = tx_copy.serialize_to_network()
else: else:

19
electrum/plugins/hw_wallet/plugin.py

@ -354,25 +354,6 @@ def validate_op_return_output(output: TxOutput, *, max_size: int = None) -> None
raise UserFacingException(_("Amount for OP_RETURN output must be zero.")) raise UserFacingException(_("Amount for OP_RETURN output must be zero."))
def get_xpubs_and_der_suffixes_from_txinout(tx: PartialTransaction,
txinout: Union[PartialTxInput, PartialTxOutput]) \
-> List[Tuple[str, List[int]]]:
xfp_to_xpub_map = {xfp: bip32node for bip32node, (xfp, path)
in tx.xpubs.items()} # type: Dict[bytes, BIP32Node]
xfps = [txinout.bip32_paths[pubkey][0] for pubkey in txinout.pubkeys]
try:
xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps]
except KeyError as e:
raise Exception(f"Partial transaction is missing global xpub for "
f"fingerprint ({str(e)}) in input/output") from e
xpubs_and_deriv_suffixes = []
for bip32node, pubkey in zip(xpubs, txinout.pubkeys):
xfp, path = txinout.bip32_paths[pubkey]
der_suffix = list(path)[bip32node.depth:]
xpubs_and_deriv_suffixes.append((bip32node.to_xpub(), der_suffix))
return xpubs_and_deriv_suffixes
def only_hook_if_libraries_available(func): 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

5
electrum/plugins/jade/jade.py

@ -264,7 +264,7 @@ class Jade_KeyStore(Hardware_KeyStore):
jade_inputs = [] jade_inputs = []
for txin in tx.inputs(): for txin in tx.inputs():
pubkey, path = self.find_my_pubkey_in_txinout(txin) pubkey, path = self.find_my_pubkey_in_txinout(txin)
witness_input = txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh', 'p2wpkh', 'p2wsh'] witness_input = txin.is_segwit()
redeem_script = Transaction.get_preimage_script(txin) redeem_script = Transaction.get_preimage_script(txin)
redeem_script = bytes.fromhex(redeem_script) if redeem_script is not None else None redeem_script = bytes.fromhex(redeem_script) if redeem_script is not None else None
input_tx = txin.utxo input_tx = txin.utxo
@ -280,6 +280,7 @@ class Jade_KeyStore(Hardware_KeyStore):
change = [None] * len(tx.outputs()) change = [None] * len(tx.outputs())
for index, txout in enumerate(tx.outputs()): for index, txout in enumerate(tx.outputs()):
if txout.is_mine and txout.is_change: if txout.is_mine and txout.is_change:
assert (desc := txout.script_descriptor)
if is_multisig: if is_multisig:
# Multisig - wallet details must be registered on Jade hw # Multisig - wallet details must be registered on Jade hw
multisig_name = _register_multisig_wallet(wallet, self, txout.address) multisig_name = _register_multisig_wallet(wallet, self, txout.address)
@ -294,7 +295,7 @@ class Jade_KeyStore(Hardware_KeyStore):
else: else:
# Pass entire path # Pass entire path
pubkey, path = self.find_my_pubkey_in_txinout(txout) pubkey, path = self.find_my_pubkey_in_txinout(txout)
change[index] = {'path':path, 'variant': txout.script_type} change[index] = {'path':path, 'variant': desc.to_legacy_electrum_script_type()}
# The txn itself # The txn itself
txn_bytes = bytes.fromhex(tx.serialize_to_network()) txn_bytes = bytes.fromhex(tx.serialize_to_network())

49
electrum/plugins/keepkey/keepkey.py

@ -1,10 +1,11 @@
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 typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence
from electrum.util import bfh, UserCancelled, UserFacingException from electrum.util import bfh, UserCancelled, UserFacingException
from electrum.bip32 import BIP32Node from electrum.bip32 import BIP32Node
from electrum import descriptor
from electrum import constants from electrum import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, Sighash from electrum.transaction import Transaction, PartialTransaction, PartialTxInput, PartialTxOutput, Sighash
@ -13,8 +14,7 @@ from electrum.plugin import Device, runs_in_hwd_thread
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: if TYPE_CHECKING:
import usb1 import usb1
@ -271,7 +271,7 @@ class KeepKeyPlugin(HW_PluginBase):
client.load_device_by_xprv(item, pin, passphrase_protection, client.load_device_by_xprv(item, pin, passphrase_protection,
label, language) label, language)
def _make_node_path(self, xpub, address_n): def _make_node_path(self, xpub: str, address_n: Sequence[int]):
bip32node = BIP32Node.from_xkey(xpub) bip32node = BIP32Node.from_xkey(xpub)
node = self.types.HDNodeType( node = self.types.HDNodeType(
depth=bip32node.depth, depth=bip32node.depth,
@ -351,14 +351,9 @@ class KeepKeyPlugin(HW_PluginBase):
script_type = self.get_keepkey_input_script_type(wallet.txin_type) script_type = self.get_keepkey_input_script_type(wallet.txin_type)
# prepare multisig, if available: # prepare multisig, if available:
xpubs = wallet.get_master_public_keys() desc = wallet.get_script_descriptor_for_address(address)
if len(xpubs) > 1: if multi := desc.get_simple_multisig():
pubkeys = wallet.get_public_keys(address) multisig = self._make_multisig(multi)
# sort xpubs using the order of pubkeys
sorted_pairs = sorted(zip(pubkeys, xpubs))
multisig = self._make_multisig(
wallet.m,
[(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
else: else:
multisig = None multisig = None
@ -376,12 +371,12 @@ class KeepKeyPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction) assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
assert keystore assert keystore
if len(txin.pubkeys) > 1: assert (desc := txin.script_descriptor)
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) if multi := desc.get_simple_multisig():
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) multisig = self._make_multisig(multi)
else: else:
multisig = None multisig = None
script_type = self.get_keepkey_input_script_type(txin.script_type) script_type = self.get_keepkey_input_script_type(desc.to_legacy_electrum_script_type())
txinputtype = self.types.TxInputType( txinputtype = self.types.TxInputType(
script_type=script_type, script_type=script_type,
multisig=multisig) multisig=multisig)
@ -406,22 +401,26 @@ class KeepKeyPlugin(HW_PluginBase):
return inputs return inputs
def _make_multisig(self, m, xpubs): def _make_multisig(self, desc: descriptor.MultisigDescriptor):
if len(xpubs) == 1: pubkeys = []
return None for pubkey_provider in desc.pubkeys:
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] assert not pubkey_provider.is_range()
assert pubkey_provider.extkey is not None
xpub = pubkey_provider.pubkey
der_suffix = pubkey_provider.get_der_suffix_int_list()
pubkeys.append(self._make_node_path(xpub, der_suffix))
return self.types.MultisigRedeemScriptType( return self.types.MultisigRedeemScriptType(
pubkeys=pubkeys, pubkeys=pubkeys,
signatures=[b''] * len(pubkeys), signatures=[b''] * len(pubkeys),
m=m) m=desc.thresh)
def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'):
def create_output_by_derivation(): def create_output_by_derivation():
script_type = self.get_keepkey_output_script_type(txout.script_type) assert (desc := txout.script_descriptor)
if len(txout.pubkeys) > 1: script_type = self.get_keepkey_output_script_type(desc.to_legacy_electrum_script_type())
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) if multi := desc.get_simple_multisig():
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) multisig = self._make_multisig(multi)
else: else:
multisig = None multisig = None
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)

35
electrum/plugins/ledger/ledger.py

@ -8,6 +8,7 @@ from typing import Dict, List, Optional, Sequence, Tuple
from electrum import bip32, constants, ecc from electrum import bip32, constants, ecc
from electrum import descriptor
from electrum.base_wizard import ScriptTypeNotSupported from electrum.base_wizard import ScriptTypeNotSupported
from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath
from electrum.bitcoin import EncodeBase58Check, int_to_hex, is_b58_address, is_segwit_script_type, var_int from electrum.bitcoin import EncodeBase58Check, int_to_hex, is_b58_address, is_segwit_script_type, var_int
@ -16,7 +17,7 @@ from electrum.i18n import _
from electrum.keystore import Hardware_KeyStore from electrum.keystore import Hardware_KeyStore
from electrum.logging import get_logger from electrum.logging import get_logger
from electrum.plugin import Device, runs_in_hwd_thread from electrum.plugin import Device, runs_in_hwd_thread
from electrum.transaction import PartialTransaction, Transaction from electrum.transaction import PartialTransaction, Transaction, PartialTxInput
from electrum.util import bfh, UserFacingException, versiontuple from electrum.util import bfh, UserFacingException, versiontuple
from electrum.wallet import Standard_Wallet from electrum.wallet import Standard_Wallet
@ -544,20 +545,25 @@ class Ledger_Client_Legacy(Ledger_Client):
pin = "" pin = ""
# prompt for the PIN before displaying the dialog if necessary # prompt for the PIN before displaying the dialog if necessary
def is_txin_legacy_multisig(txin: PartialTxInput) -> bool:
desc = txin.script_descriptor
return (isinstance(desc, descriptor.SHDescriptor)
and isinstance(desc.subdescriptors[0], descriptor.MultisigDescriptor))
# Fetch inputs of the transaction to sign # Fetch inputs of the transaction to sign
for txin in tx.inputs(): for txin in tx.inputs():
if txin.is_coinbase_input(): if txin.is_coinbase_input():
self.give_error("Coinbase not supported") # should never happen self.give_error("Coinbase not supported") # should never happen
if txin.script_type in ['p2sh']: if is_txin_legacy_multisig(txin):
p2shTransaction = True p2shTransaction = True
if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']: if txin.is_p2sh_segwit():
if not self.supports_segwit(): if not self.supports_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True segwitTransaction = True
if txin.script_type in ['p2wpkh', 'p2wsh']: if txin.is_native_segwit():
if not self.supports_native_segwit(): if not self.supports_native_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT) self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True segwitTransaction = True
@ -584,7 +590,7 @@ class Ledger_Client_Legacy(Ledger_Client):
# Sanity check # Sanity check
if p2shTransaction: if p2shTransaction:
for txin in tx.inputs(): for txin in tx.inputs():
if txin.script_type != 'p2sh': if not is_txin_legacy_multisig(txin):
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()))
@ -1083,7 +1089,6 @@ class Ledger_Client_New(Ledger_Client):
raise UserFacingException("Coinbase not supported") # should never happen raise UserFacingException("Coinbase not supported") # should never happen
utxo = None utxo = None
scriptcode = b""
if psbt_in.witness_utxo: if psbt_in.witness_utxo:
utxo = psbt_in.witness_utxo utxo = psbt_in.witness_utxo
if psbt_in.non_witness_utxo: if psbt_in.non_witness_utxo:
@ -1094,19 +1099,9 @@ class Ledger_Client_New(Ledger_Client):
if utxo is None: if utxo is None:
continue continue
scriptcode = utxo.scriptPubKey if (desc := electrum_txin.script_descriptor) is None:
if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh']: raise Exception("script_descriptor missing for txin ")
if len(psbt_in.redeem_script) == 0: scriptcode = desc.expand().scriptcode_for_sighash
continue
scriptcode = psbt_in.redeem_script
elif electrum_txin.script_type in ['p2wsh', 'p2wsh-p2sh']:
if len(psbt_in.witness_script) == 0:
continue
scriptcode = psbt_in.witness_script
p2sh = False
if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh', 'p2wsh-p2sh']:
p2sh = True
is_wit, wit_ver, __ = is_witness(psbt_in.redeem_script or utxo.scriptPubKey) is_wit, wit_ver, __ = is_witness(psbt_in.redeem_script or utxo.scriptPubKey)
@ -1115,7 +1110,7 @@ class Ledger_Client_New(Ledger_Client):
# if it's a segwit spend (any version), make sure the witness_utxo is also present # if it's a segwit spend (any version), make sure the witness_utxo is also present
psbt_in.witness_utxo = utxo psbt_in.witness_utxo = utxo
if p2sh: if electrum_txin.is_p2sh_segwit():
if wit_ver == 0: if wit_ver == 0:
script_addrtype = AddressType.SH_WIT script_addrtype = AddressType.SH_WIT
else: else:

49
electrum/plugins/safe_t/safe_t.py

@ -1,10 +1,11 @@
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 typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence
from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException
from electrum.bip32 import BIP32Node from electrum.bip32 import BIP32Node
from electrum import descriptor
from electrum import constants from electrum import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import Device, runs_in_hwd_thread from electrum.plugin import Device, runs_in_hwd_thread
@ -13,8 +14,7 @@ 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: if TYPE_CHECKING:
from .client import SafeTClient from .client import SafeTClient
@ -241,7 +241,7 @@ class SafeTPlugin(HW_PluginBase):
client.load_device_by_xprv(item, pin, passphrase_protection, client.load_device_by_xprv(item, pin, passphrase_protection,
label, language) label, language)
def _make_node_path(self, xpub, address_n): def _make_node_path(self, xpub: str, address_n: Sequence[int]):
bip32node = BIP32Node.from_xkey(xpub) bip32node = BIP32Node.from_xkey(xpub)
node = self.types.HDNodeType( node = self.types.HDNodeType(
depth=bip32node.depth, depth=bip32node.depth,
@ -321,14 +321,9 @@ class SafeTPlugin(HW_PluginBase):
script_type = self.get_safet_input_script_type(wallet.txin_type) script_type = self.get_safet_input_script_type(wallet.txin_type)
# prepare multisig, if available: # prepare multisig, if available:
xpubs = wallet.get_master_public_keys() desc = wallet.get_script_descriptor_for_address(address)
if len(xpubs) > 1: if multi := desc.get_simple_multisig():
pubkeys = wallet.get_public_keys(address) multisig = self._make_multisig(multi)
# sort xpubs using the order of pubkeys
sorted_pairs = sorted(zip(pubkeys, xpubs))
multisig = self._make_multisig(
wallet.m,
[(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
else: else:
multisig = None multisig = None
@ -346,12 +341,12 @@ class SafeTPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction) assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
assert keystore assert keystore
if len(txin.pubkeys) > 1: assert (desc := txin.script_descriptor)
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) if multi := desc.get_simple_multisig():
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) multisig = self._make_multisig(multi)
else: else:
multisig = None multisig = None
script_type = self.get_safet_input_script_type(txin.script_type) script_type = self.get_safet_input_script_type(desc.to_legacy_electrum_script_type())
txinputtype = self.types.TxInputType( txinputtype = self.types.TxInputType(
script_type=script_type, script_type=script_type,
multisig=multisig) multisig=multisig)
@ -376,22 +371,26 @@ class SafeTPlugin(HW_PluginBase):
return inputs return inputs
def _make_multisig(self, m, xpubs): def _make_multisig(self, desc: descriptor.MultisigDescriptor):
if len(xpubs) == 1: pubkeys = []
return None for pubkey_provider in desc.pubkeys:
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] assert not pubkey_provider.is_range()
assert pubkey_provider.extkey is not None
xpub = pubkey_provider.pubkey
der_suffix = pubkey_provider.get_der_suffix_int_list()
pubkeys.append(self._make_node_path(xpub, der_suffix))
return self.types.MultisigRedeemScriptType( return self.types.MultisigRedeemScriptType(
pubkeys=pubkeys, pubkeys=pubkeys,
signatures=[b''] * len(pubkeys), signatures=[b''] * len(pubkeys),
m=m) m=desc.thresh)
def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'):
def create_output_by_derivation(): def create_output_by_derivation():
script_type = self.get_safet_output_script_type(txout.script_type) assert (desc := txout.script_descriptor)
if len(txout.pubkeys) > 1: script_type = self.get_safet_output_script_type(desc.to_legacy_electrum_script_type())
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) if multi := desc.get_simple_multisig():
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) multisig = self._make_multisig(multi)
else: else:
multisig = None multisig = None
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)

49
electrum/plugins/trezor/trezor.py

@ -1,9 +1,10 @@
import traceback import traceback
import sys import sys
from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING from typing import NamedTuple, Any, Optional, Dict, Union, List, Tuple, TYPE_CHECKING, Sequence
from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException from electrum.util import bfh, versiontuple, UserCancelled, UserFacingException
from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
from electrum import descriptor
from electrum import constants from electrum import constants
from electrum.i18n import _ from electrum.i18n import _
from electrum.plugin import Device, runs_in_hwd_thread from electrum.plugin import Device, runs_in_hwd_thread
@ -14,8 +15,7 @@ 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__)
@ -284,7 +284,7 @@ class TrezorPlugin(HW_PluginBase):
else: else:
raise RuntimeError("Unsupported recovery method") raise RuntimeError("Unsupported recovery method")
def _make_node_path(self, xpub, address_n): def _make_node_path(self, xpub: str, address_n: Sequence[int]):
bip32node = BIP32Node.from_xkey(xpub) bip32node = BIP32Node.from_xkey(xpub)
node = HDNodeType( node = HDNodeType(
depth=bip32node.depth, depth=bip32node.depth,
@ -386,14 +386,9 @@ class TrezorPlugin(HW_PluginBase):
script_type = self.get_trezor_input_script_type(wallet.txin_type) script_type = self.get_trezor_input_script_type(wallet.txin_type)
# prepare multisig, if available: # prepare multisig, if available:
xpubs = wallet.get_master_public_keys() desc = wallet.get_script_descriptor_for_address(address)
if len(xpubs) > 1: if multi := desc.get_simple_multisig():
pubkeys = wallet.get_public_keys(address) multisig = self._make_multisig(multi)
# sort xpubs using the order of pubkeys
sorted_pairs = sorted(zip(pubkeys, xpubs))
multisig = self._make_multisig(
wallet.m,
[(xpub, deriv_suffix) for pubkey, xpub in sorted_pairs])
else: else:
multisig = None multisig = None
@ -417,10 +412,10 @@ class TrezorPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction) assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
assert keystore assert keystore
if len(txin.pubkeys) > 1: assert (desc := txin.script_descriptor)
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin) if multi := desc.get_simple_multisig():
txinputtype.multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes) txinputtype.multisig = self._make_multisig(multi)
txinputtype.script_type = self.get_trezor_input_script_type(txin.script_type) txinputtype.script_type = self.get_trezor_input_script_type(desc.to_legacy_electrum_script_type())
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txin)
if full_path: if full_path:
txinputtype.address_n = full_path txinputtype.address_n = full_path
@ -433,22 +428,26 @@ class TrezorPlugin(HW_PluginBase):
return inputs return inputs
def _make_multisig(self, m, xpubs): def _make_multisig(self, desc: descriptor.MultisigDescriptor):
if len(xpubs) == 1: pubkeys = []
return None for pubkey_provider in desc.pubkeys:
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs] assert not pubkey_provider.is_range()
assert pubkey_provider.extkey is not None
xpub = pubkey_provider.pubkey
der_suffix = pubkey_provider.get_der_suffix_int_list()
pubkeys.append(self._make_node_path(xpub, der_suffix))
return MultisigRedeemScriptType( return MultisigRedeemScriptType(
pubkeys=pubkeys, pubkeys=pubkeys,
signatures=[b''] * len(pubkeys), signatures=[b''] * len(pubkeys),
m=m) m=desc.thresh)
def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'): def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'):
def create_output_by_derivation(): def create_output_by_derivation():
script_type = self.get_trezor_output_script_type(txout.script_type) assert (desc := txout.script_descriptor)
if len(txout.pubkeys) > 1: script_type = self.get_trezor_output_script_type(desc.to_legacy_electrum_script_type())
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout) if multi := desc.get_simple_multisig():
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes) multisig = self._make_multisig(multi)
else: else:
multisig = None multisig = None
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout) my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)

2
electrum/segwit_addr.py

@ -25,7 +25,7 @@ from enum import Enum
from typing import Tuple, Optional, Sequence, NamedTuple, List from typing import Tuple, Optional, Sequence, NamedTuple, List
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
_CHARSET_INVERSE = {x: CHARSET.find(x) for x in CHARSET} _CHARSET_INVERSE = {c: i for (i, c) in enumerate(CHARSET)}
BECH32_CONST = 1 BECH32_CONST = 1
BECH32M_CONST = 0x2bc830a3 BECH32M_CONST = 0x2bc830a3

4
electrum/submarine_swaps.py

@ -124,7 +124,6 @@ def create_claim_tx(
"""Create tx to either claim successful reverse-swap, """Create tx to either claim successful reverse-swap,
or to get refunded for timed-out forward-swap. or to get refunded for timed-out forward-swap.
""" """
txin.script_type = 'p2wsh'
txin.script_sig = b'' txin.script_sig = b''
txin.witness_script = witness_script txin.witness_script = witness_script
txout = PartialTxOutput.from_address_and_value(address, amount_sat) txout = PartialTxOutput.from_address_and_value(address, amount_sat)
@ -622,8 +621,6 @@ class SwapManager(Logger):
return return
preimage = swap.preimage if swap.is_reverse else 0 preimage = swap.preimage if swap.is_reverse else 0
witness_script = swap.redeem_script witness_script = swap.redeem_script
txin.script_type = 'p2wsh'
txin.num_sig = 1 # hack so that txin not considered "is_complete"
txin.script_sig = b'' txin.script_sig = b''
txin.witness_script = witness_script txin.witness_script = witness_script
sig_dummy = b'\x00' * 71 # DER-encoded ECDSA sig, with low S and low R sig_dummy = b'\x00' * 71 # DER-encoded ECDSA sig, with low S and low R
@ -637,7 +634,6 @@ class SwapManager(Logger):
txin = tx.inputs()[0] txin = tx.inputs()[0]
assert len(tx.inputs()) == 1, f"expected 1 input for swap claim tx. found {len(tx.inputs())}" assert len(tx.inputs()) == 1, f"expected 1 input for swap claim tx. found {len(tx.inputs())}"
assert txin.prevout.txid.hex() == swap.funding_txid assert txin.prevout.txid.hex() == swap.funding_txid
txin.script_type = 'p2wsh'
txin.script_sig = b'' txin.script_sig = b''
txin.witness_script = witness_script txin.witness_script = witness_script
sig = bytes.fromhex(tx.sign_txin(0, swap.privkey)) sig = bytes.fromhex(tx.sign_txin(0, swap.privkey))

22
electrum/tests/test_bitcoin.py

@ -815,6 +815,28 @@ class Test_xprv_xpub(ElectrumTestCase):
self.assertFalse(is_xprv('xprv1nval1d')) self.assertFalse(is_xprv('xprv1nval1d'))
self.assertFalse(is_xprv('xprv661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG')) self.assertFalse(is_xprv('xprv661MyMwAqRbcFWohJWt7PHsFEJfZAvw9ZxwQoDa4SoMgsDDM1T7WK3u9E4edkC4ugRnZ8E4xDZRpk8Rnts3Nbt97dPwT52WRONGBADWRONG'))
def test_bip32_from_xkey(self):
bip32node1 = BIP32Node.from_xkey("xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy")
self.assertEqual(
BIP32Node(
xtype='standard',
eckey=ecc.ECPubkey(bytes.fromhex("022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011")),
chaincode=bytes.fromhex("c783e67b921d2beb8f6b389cc646d7263b4145701dadd2161548a8b078e65e9e"),
depth=5,
fingerprint=bytes.fromhex("d880d7d8"),
child_number=bytes.fromhex("3b9aca00"),
),
bip32node1)
with self.assertRaises(ValueError):
BIP32Node.from_xkey(
"zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8",
allow_custom_headers=False)
bip32node2 = BIP32Node.from_xkey(
"zpub6jftahH18ngZyLeqfLBFAm7YaWFVttE9pku5pNMX2qPzTjoq1FVgZMmhjecyB2nqFb31gHE9vNvbaggU6vvWpNZbXEWLLUjYjFqG95LNyT8",
allow_custom_headers=True)
self.assertEqual(bytes.fromhex("03f18e53f3386a5f9a9d2c369ad3b84b429eb397b4bc69ce600f2d833b54ba32f4"),
bip32node2.eckey.get_public_key_bytes(compressed=True))
def test_is_bip32_derivation(self): def test_is_bip32_derivation(self):
self.assertTrue(is_bip32_derivation("m/0'/1")) self.assertTrue(is_bip32_derivation("m/0'/1"))
self.assertTrue(is_bip32_derivation("m/0'/0'")) self.assertTrue(is_bip32_derivation("m/0'/0'"))

390
electrum/tests/test_descriptor.py

@ -0,0 +1,390 @@
# Copyright (c) 2018-2023 The HWI developers
# Copyright (c) 2023 The Electrum developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# originally from https://github.com/bitcoin-core/HWI/blob/f5a9b29c00e483cc99a1b8f4f5ef75413a092869/test/test_descriptor.py
from binascii import unhexlify
import unittest
from electrum.descriptor import (
parse_descriptor,
MultisigDescriptor,
SHDescriptor,
TRDescriptor,
PKHDescriptor,
WPKHDescriptor,
WSHDescriptor,
PubkeyProvider,
)
from electrum import ecc
from electrum.util import bfh
from . import ElectrumTestCase, as_testnet
class TestDescriptor(ElectrumTestCase):
@as_testnet
def test_parse_descriptor_with_origin(self):
d = "wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WPKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h")
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/84h/1h/0h/0/0")
self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0])
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, None)
self.assertEqual(e.address(), "tb1qm90ugl4d48jv8n6e5t9ln6t9zlpm5th690vysp")
@as_testnet
def test_parse_multisig_descriptor_with_origin(self):
d = "wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WSHDescriptor))
self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor))
self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_path(), "m/48h/0h/0h/2h/0/0")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483696, 2147483648, 2147483648, 2147483650, 0, 0])
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, "/0/0")
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae"))
d = "sh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0))"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, SHDescriptor))
self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor))
self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, "/0/0")
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("a91495ee6326805b1586bb821fc3c0eeab2c68441b4187"))
self.assertEqual(e.redeem_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae"))
self.assertEqual(e.witness_script, None)
d = "sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, SHDescriptor))
self.assertTrue(isinstance(desc.subdescriptors[0], WSHDescriptor))
self.assertTrue(isinstance(desc.subdescriptors[0].subdescriptors[0], MultisigDescriptor))
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty")
self.assertEqual(desc.subdescriptors[0].subdescriptors[0].pubkeys[1].deriv_path, "/0/0")
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("a914779ae0f6958e98b997cc177f9b554289905fbb5587"))
self.assertEqual(e.redeem_script, unhexlify("002084b64b2b8651df8fd3e9735f6269edbf9e03abf619ae0788be9f17bf18e83d59"))
self.assertEqual(e.witness_script, unhexlify("522102c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c721033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae"))
@as_testnet
def test_parse_descriptor_without_origin(self):
d = "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WPKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin, None)
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/0/0")
self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [0, 0])
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, None)
@as_testnet
def test_parse_descriptor_with_origin_fingerprint_only(self):
d = "wpkh([00000001]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WPKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(len(desc.pubkeys[0].origin.path), 0)
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, None)
def test_parse_descriptor_with_key_at_end_with_origin(self):
d = "wpkh([00000001/84h/1h/0h/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WPKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h/0/0")
self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7")
self.assertEqual(desc.pubkeys[0].deriv_path, None)
self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m/84h/1h/0h/0/0")
self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [16777216, 2147483732, 2147483649, 2147483648, 0, 0])
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("0014d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, None)
d = "pkh([00000001/84h/1h/0h/0/0]02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, PKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h/0/0")
self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7")
self.assertEqual(desc.pubkeys[0].deriv_path, None)
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand()
self.assertEqual(e.output_script, unhexlify("76a914d95fc47eada9e4c3cf59a2cbf9e96517c3ba2efa88ac"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, None)
def test_parse_descriptor_with_key_at_end_without_origin(self):
d = "wpkh(02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WPKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin, None)
self.assertEqual(desc.pubkeys[0].pubkey, "02c97dc3f4420402e01a113984311bf4a1b8de376cac0bdcfaf1b3ac81f13433c7")
self.assertEqual(desc.pubkeys[0].deriv_path, None)
self.assertEqual(desc.pubkeys[0].get_full_derivation_path(), "m")
self.assertEqual(desc.pubkeys[0].get_full_derivation_int_list(), [])
self.assertEqual(desc.to_string_no_checksum(), d)
def test_parse_empty_descriptor(self):
self.assertRaises(ValueError, parse_descriptor, "")
@as_testnet
def test_parse_descriptor_replace_h(self):
d = "wpkh([00000001/84'/1'/0']tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)"
desc = parse_descriptor(d)
self.assertIsNotNone(desc)
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h")
@as_testnet
def test_parse_descriptor_unknown_notation_for_hardened_derivation(self):
with self.assertRaises(ValueError):
desc = parse_descriptor("wpkh([00000001/84x/1x/0x]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)")
with self.assertRaises(ValueError):
desc = parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0x)")
def test_checksums(self):
with self.subTest(msg="Valid checksum"):
self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwj"))
self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmsckna"))
self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))"))
self.assertIsNotNone(parse_descriptor("sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))"))
with self.subTest(msg="Empty Checksum"):
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#")
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#")
with self.subTest(msg="Too long Checksum"):
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kwjq")
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmscknaq")
with self.subTest(msg="Too Short Checksum"):
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kw")
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#hgmsckn")
with self.subTest(msg="Error in Payload"):
self.assertRaises(ValueError, parse_descriptor, "sh(multi(3,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#ggrsrxf")
self.assertRaises(ValueError, parse_descriptor, "sh(multi(3,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09x5")
with self.subTest(msg="Error in Checksum"):
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0))#5js07kej")
self.assertRaises(ValueError, parse_descriptor, "sh(multi(2,[00000000/111h/222]xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0))#tjg09y5")
@as_testnet
def test_tr_descriptor(self):
d = "tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, TRDescriptor))
self.assertEqual(len(desc.pubkeys), 1)
self.assertEqual(len(desc.subdescriptors), 0)
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h")
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.to_string_no_checksum(), d)
d = "tr([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),{{pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B),pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)},pk(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B)}})"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, TRDescriptor))
self.assertEqual(len(desc.subdescriptors), 4)
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h")
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/0")
self.assertEqual(desc.depths, [1, 3, 3, 2])
self.assertEqual(desc.to_string_no_checksum(), d)
@as_testnet
def test_parse_descriptor_with_range(self):
d = "wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WPKHDescriptor))
self.assertEqual(desc.pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.pubkeys[0].origin.get_derivation_path(), "m/84h/1h/0h")
self.assertEqual(desc.pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.pubkeys[0].deriv_path, "/0/*")
self.assertEqual(desc.to_string_no_checksum(), d)
with self.assertRaises(ValueError): # "pos" arg needed due to "*"
e = desc.expand()
e = desc.expand(pos=7)
self.assertEqual(e.output_script, unhexlify("0014c5f80de08f6ae8dd720bf4e4948ba498c96256a1"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, None)
with self.assertRaises(ValueError): # wildcard only allowed in last position
parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*/0)")
with self.assertRaises(ValueError): # only one wildcard(*) is allowed
parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/*/*)")
@as_testnet
def test_parse_multisig_descriptor_with_range(self):
d = "wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/*))"
desc = parse_descriptor(d)
self.assertTrue(isinstance(desc, WSHDescriptor))
self.assertTrue(isinstance(desc.subdescriptors[0], MultisigDescriptor))
self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.fingerprint.hex(), "00000001")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].pubkey, "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B")
self.assertEqual(desc.subdescriptors[0].pubkeys[0].deriv_path, "/0/*")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.fingerprint.hex(), "00000002")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].origin.get_derivation_path(), "m/48h/0h/0h/2h")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].pubkey, "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty")
self.assertEqual(desc.subdescriptors[0].pubkeys[1].deriv_path, "/0/*")
self.assertEqual(desc.to_string_no_checksum(), d)
e = desc.expand(pos=7)
self.assertEqual(e.output_script, unhexlify("0020453cdf90aef0997947bc0605481f81dd2978ecd2d04ac36fb57397a82341682d"))
self.assertEqual(e.redeem_script, None)
self.assertEqual(e.witness_script, unhexlify("5221034e703dfcd64f23ad5d6156ee3b9dd7566137626c663bb521bf710957599723342102c35627535d26de98ae749b7a7849df99cbe53af795005437ca647c8af9a006af52ae"))
@as_testnet
def test_multisig_descriptor_with_mixed_range(self):
d = "sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))"
desc = parse_descriptor(d)
e = desc.expand(pos=7)
self.assertEqual(e.output_script, bfh("a914644ece12bab2f84ad6de96ec18de51e6168c028987"))
self.assertEqual(e.redeem_script, bfh("0020824ce4ffab74a8d09c2f77ed447fb040ea5dfbed06f8e3b3327127a18634f6a7"))
self.assertEqual(e.witness_script, bfh("5221034e703dfcd64f23ad5d6156ee3b9dd7566137626c663bb521bf7109575997233421033a4f18d2b498273ed7439c59f6d8a673d5b9c67a03163d530e12c941ca22be3352ae"))
self.assertEqual(e.address(), "2N2Pbxw3HNJ9jrUw8LCSfXyDWx9TKGRT2an")
@as_testnet
def test_uncompressed_pubkey_in_segwit(self):
pubkey = ecc.ECPubkey(bfh("02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc"))
pubkey_comp_hex = pubkey.get_public_key_hex(compressed=True)
pubkey_uncomp_hex = pubkey.get_public_key_hex(compressed=False)
self.assertEqual(pubkey_comp_hex, "02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc")
self.assertEqual(pubkey_uncomp_hex, "04a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc3ccfc29410b8f23c15d88413a6b88c8cd44b016a7f1dd91a8d64c3107c6bce1a")
# pkh
desc = parse_descriptor(f"pkh({pubkey_comp_hex})")
self.assertEqual(desc.expand().output_script, bfh("76a9140297bde2689a3c79ffe050583b62f86f2d9dae5488ac"))
desc = parse_descriptor(f"pkh({pubkey_uncomp_hex})")
self.assertEqual(desc.expand().output_script, bfh("76a914e1f4a76b122f0288b013404cd52a9d1de0ced3c488ac"))
# wpkh
desc = parse_descriptor(f"wpkh({pubkey_comp_hex})")
self.assertEqual(desc.expand().output_script, bfh("00140297bde2689a3c79ffe050583b62f86f2d9dae54"))
with self.assertRaises(ValueError): # only compressed public keys can be used in segwit scripts
desc = parse_descriptor(f"wpkh({pubkey_uncomp_hex})")
# sh(wsh(multi()))
desc = parse_descriptor(f"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,{pubkey_comp_hex})))")
self.assertEqual(desc.expand(pos=2).output_script, bfh("a9148f162cce29ad81e63ed45cd09aff83418316eab687"))
with self.assertRaises(ValueError): # only compressed public keys can be used in segwit scripts
desc = parse_descriptor(f"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*,{pubkey_uncomp_hex})))")
@as_testnet
def test_parse_descriptor_context(self):
desc = parse_descriptor("sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))")
self.assertTrue(isinstance(desc, SHDescriptor))
with self.assertRaises(ValueError): # Can only have sh() at top level
desc = parse_descriptor("wsh(sh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))")
with self.assertRaises(ValueError): # Can only have wsh() at top level or inside sh()
desc = parse_descriptor("wsh(wsh(multi(2,[00000001/48h/0h/0h/2h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0,[00000002/48h/0h/0h/2h]tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty/0/0)))")
desc = parse_descriptor("wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0)")
self.assertTrue(isinstance(desc, WPKHDescriptor))
with self.assertRaises(ValueError): # Can only have wpkh() at top level or inside sh()
desc = parse_descriptor("wsh(wpkh([00000001/84h/1h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0))")
def test_parse_descriptor_ypub_zpub_forbidden(self):
desc = parse_descriptor("wpkh([535e473f/0h]xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4/0/*)")
with self.assertRaises(ValueError): # only standard xpub/xprv allowed
desc = parse_descriptor("wpkh([535e473f/0h]ypub6TLJVy4mZfqBJhoQBTgDR1TzM7s91WbVnMhZj31swV6xxPiwCqeGYrBn2dNHbDrP86qqxbM6FNTX3VjhRjNoXYyBAR5G3o75D3r2djmhZwM/0/*)")
with self.assertRaises(ValueError): # only standard xpub/xprv allowed
desc = parse_descriptor("wpkh([535e473f/0h]zpub6nAZodjgiMNf9zzX1pTqd6ZVX61ax8azhUDnWRumKVUr1VYATVoqAuqv3qKsb8WJXjxei4wei2p4vnMG9RnpKnen2kmgdhvZUmug2NnHNsr/0/*)")
@as_testnet
def test_sortedmulti_ranged_pubkey_order(self):
xpub1 = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
xpub2 = "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty"
# if ranged, we sort lexicographically
desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/*,[00000002/48h/0h/0h/2h]{xpub2}/0/*)))")
self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000002/48h/0h/0h/2h]{xpub2}/0/*,[00000001/48h/0h/0h/2h]{xpub1}/0/*)))")
self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
# if unsorted "multi", don't touch order
desc = parse_descriptor(f"sh(wsh(multi(2,[00000002/48h/0h/0h/2h]{xpub2}/0/*,[00000001/48h/0h/0h/2h]{xpub1}/0/*)))")
self.assertEqual([xpub2, xpub1], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
@as_testnet
def test_sortedmulti_unranged_pubkey_order(self):
xpub1 = "tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B"
xpub2 = "tpubDFHiBJDeNvqPWNJbzzxqDVXmJZoNn2GEtoVcFhMjXipQiorGUmps3e5ieDGbRrBPTFTh9TXEKJCwbAGW9uZnfrVPbMxxbFohuFzfT6VThty"
# if not ranged, we sort according to final derived pubkey order
desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/0,[00000002/48h/0h/0h/2h]{xpub2}/0/0)))")
self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/1,[00000002/48h/0h/0h/2h]{xpub2}/0/1)))")
self.assertEqual([xpub2, xpub1], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
desc = parse_descriptor(f"sh(wsh(sortedmulti(2,[00000001/48h/0h/0h/2h]{xpub1}/0/4,[00000002/48h/0h/0h/2h]{xpub2}/0/4)))")
self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
# if unsorted "multi", don't touch order
desc = parse_descriptor(f"sh(wsh(multi(2,[00000001/48h/0h/0h/2h]{xpub1}/0/1,[00000002/48h/0h/0h/2h]{xpub2}/0/1)))")
self.assertEqual([xpub1, xpub2], [pk.pubkey for pk in desc.subdescriptors[0].subdescriptors[0].pubkeys])
def test_pubkey_provider_deriv_path(self):
xpub = "xpub68W3CJPrQzHhTQcHM6tbCvNVB9ih4tbzsFBLwe7zZUj5uHuhxBUhvnXe1RQhbKCTiTj3D7kXni6yAD88i2xnjKHaJ5NqTtHawKnPFCDnmo4"
# valid:
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="/1/7")
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="/1/*")
# invalid:
with self.assertRaises(ValueError):
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="1")
with self.assertRaises(ValueError):
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="1/7")
with self.assertRaises(ValueError):
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="m/1/7")
with self.assertRaises(ValueError):
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="*/7")
with self.assertRaises(ValueError):
pp = PubkeyProvider(origin=None, pubkey=xpub, deriv_path="*/*")
pubkey_hex = "02a0507c8bb3d96dfd7731bafb0ae30e6ed10bbadd6a9f9f88eaf0602b9cc99adc"
# valid:
pp = PubkeyProvider(origin=None, pubkey=pubkey_hex, deriv_path=None)
# invalid:
with self.assertRaises(ValueError):
pp = PubkeyProvider(origin=None, pubkey=pubkey_hex, deriv_path="/1/7")

18
electrum/tests/test_transaction.py

@ -9,8 +9,9 @@ from electrum.util import bfh
from electrum.bitcoin import (deserialize_privkey, opcodes, from electrum.bitcoin import (deserialize_privkey, opcodes,
construct_script, construct_witness) construct_script, construct_witness)
from electrum.ecc import ECPrivkey from electrum.ecc import ECPrivkey
from .test_bitcoin import disable_ecdsa_r_value_grinding from electrum import descriptor
from .test_bitcoin import disable_ecdsa_r_value_grinding
from . import ElectrumTestCase from . import ElectrumTestCase
signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000' signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
@ -89,9 +90,10 @@ class TestTransaction(ElectrumTestCase):
def test_tx_update_signatures(self): def test_tx_update_signatures(self):
tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA") tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA")
tx.inputs()[0].script_type = 'p2pkh' pubkey = bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')
tx.inputs()[0].pubkeys = [bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')] script_type = 'p2pkh'
tx.inputs()[0].num_sig = 1 desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey.hex(), script_type=script_type)
tx.inputs()[0].script_descriptor = desc
tx.update_signatures(signed_blob_signatures) tx.update_signatures(signed_blob_signatures)
self.assertEqual(tx.serialize(), signed_blob) self.assertEqual(tx.serialize(), signed_blob)
@ -873,7 +875,6 @@ class TestTransactionTestnet(ElectrumTestCase):
prevout = TxOutpoint(txid=bfh('6d500966f9e494b38a04545f0cea35fc7b3944e341a64b804fed71cdee11d434'), out_idx=1) prevout = TxOutpoint(txid=bfh('6d500966f9e494b38a04545f0cea35fc7b3944e341a64b804fed71cdee11d434'), out_idx=1)
txin = PartialTxInput(prevout=prevout) txin = PartialTxInput(prevout=prevout)
txin.nsequence = 2 ** 32 - 3 txin.nsequence = 2 ** 32 - 3
txin.script_type = 'p2sh'
redeem_script = bfh(construct_script([ redeem_script = bfh(construct_script([
locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG, locktime, opcodes.OP_CHECKLOCKTIMEVERIFY, opcodes.OP_DROP, pubkey, opcodes.OP_CHECKSIG,
])) ]))
@ -935,7 +936,6 @@ class TestSighashTypes(ElectrumTestCase):
prevout = TxOutpoint(txid=bfh('6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436'), out_idx=1) prevout = TxOutpoint(txid=bfh('6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436'), out_idx=1)
txin = PartialTxInput(prevout=prevout) txin = PartialTxInput(prevout=prevout)
txin.nsequence=0xffffffff txin.nsequence=0xffffffff
txin.script_type='p2sh-p2wsh'
txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae') txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae')
txin.redeem_script = bfh('0020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54') txin.redeem_script = bfh('0020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54')
txin._trusted_value_sats = 987654321 txin._trusted_value_sats = 987654321
@ -945,7 +945,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_all(self): def test_check_sighash_types_sighash_all(self):
self.txin.sighash=Sighash.ALL self.txin.sighash=Sighash.ALL
self.txin.pubkeys = [bfh('0307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba3')]
privkey = bfh('730fff80e1413068a05b57d6a58261f07551163369787f349438ea38ca80fac6') privkey = bfh('730fff80e1413068a05b57d6a58261f07551163369787f349438ea38ca80fac6')
tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)
sig = tx.sign_txin(0,privkey) sig = tx.sign_txin(0,privkey)
@ -954,7 +953,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_none(self): def test_check_sighash_types_sighash_none(self):
self.txin.sighash=Sighash.NONE self.txin.sighash=Sighash.NONE
self.txin.pubkeys = [bfh('03b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b')]
privkey = bfh('11fa3d25a17cbc22b29c44a484ba552b5a53149d106d3d853e22fdd05a2d8bb3') privkey = bfh('11fa3d25a17cbc22b29c44a484ba552b5a53149d106d3d853e22fdd05a2d8bb3')
tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)
sig = tx.sign_txin(0,privkey) sig = tx.sign_txin(0,privkey)
@ -963,7 +961,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_single(self): def test_check_sighash_types_sighash_single(self):
self.txin.sighash=Sighash.SINGLE self.txin.sighash=Sighash.SINGLE
self.txin.pubkeys = [bfh('034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a')]
privkey = bfh('77bf4141a87d55bdd7f3cd0bdccf6e9e642935fec45f2f30047be7b799120661') privkey = bfh('77bf4141a87d55bdd7f3cd0bdccf6e9e642935fec45f2f30047be7b799120661')
tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)
sig = tx.sign_txin(0,privkey) sig = tx.sign_txin(0,privkey)
@ -973,7 +970,6 @@ class TestSighashTypes(ElectrumTestCase):
@disable_ecdsa_r_value_grinding @disable_ecdsa_r_value_grinding
def test_check_sighash_types_sighash_all_anyonecanpay(self): def test_check_sighash_types_sighash_all_anyonecanpay(self):
self.txin.sighash=Sighash.ALL|Sighash.ANYONECANPAY self.txin.sighash=Sighash.ALL|Sighash.ANYONECANPAY
self.txin.pubkeys = [bfh('033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f4')]
privkey = bfh('14af36970f5025ea3e8b5542c0f8ebe7763e674838d08808896b63c3351ffe49') privkey = bfh('14af36970f5025ea3e8b5542c0f8ebe7763e674838d08808896b63c3351ffe49')
tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)
sig = tx.sign_txin(0,privkey) sig = tx.sign_txin(0,privkey)
@ -983,7 +979,6 @@ class TestSighashTypes(ElectrumTestCase):
@disable_ecdsa_r_value_grinding @disable_ecdsa_r_value_grinding
def test_check_sighash_types_sighash_none_anyonecanpay(self): def test_check_sighash_types_sighash_none_anyonecanpay(self):
self.txin.sighash=Sighash.NONE|Sighash.ANYONECANPAY self.txin.sighash=Sighash.NONE|Sighash.ANYONECANPAY
self.txin.pubkeys = [bfh('03a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac16')]
privkey = bfh('fe9a95c19eef81dde2b95c1284ef39be497d128e2aa46916fb02d552485e0323') privkey = bfh('fe9a95c19eef81dde2b95c1284ef39be497d128e2aa46916fb02d552485e0323')
tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)
sig = tx.sign_txin(0,privkey) sig = tx.sign_txin(0,privkey)
@ -992,7 +987,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_single_anyonecanpay(self): def test_check_sighash_types_sighash_single_anyonecanpay(self):
self.txin.sighash=Sighash.SINGLE|Sighash.ANYONECANPAY self.txin.sighash=Sighash.SINGLE|Sighash.ANYONECANPAY
self.txin.pubkeys = [bfh('02d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b')]
privkey = bfh('428a7aee9f0c2af0cd19af3cf1c78149951ea528726989b2e83e4778d2c3f890') privkey = bfh('428a7aee9f0c2af0cd19af3cf1c78149951ea528726989b2e83e4778d2c3f890')
tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False) tx = PartialTransaction.from_io(inputs=[self.txin], outputs=[self.txout1,self.txout2], locktime=self.locktime, version=1, BIP69_sort=False)
sig = tx.sign_txin(0,privkey) sig = tx.sign_txin(0,privkey)

46
electrum/tests/test_wallet_vertical.py

@ -729,7 +729,6 @@ class TestWalletSending(ElectrumTestCase):
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet1.is_mine(wallet1.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet1.is_mine(wallet1.adb.get_txin_address(tx_copy.inputs()[0])))
@ -749,7 +748,6 @@ class TestWalletSending(ElectrumTestCase):
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))
@ -809,7 +807,6 @@ class TestWalletSending(ElectrumTestCase):
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))
@ -824,12 +821,15 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1 # wallet2 -> wallet1
outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]
tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) tx = wallet2.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False)
self.assertEqual(
"pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
wallet2.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))
@ -895,7 +895,13 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 -> wallet2 # wallet1 -> wallet2
outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)] outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)]
tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) tx = wallet1a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False)
self.assertEqual((0, 2), tx.signature_count())
self.assertEqual(
"wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
wallet1a.sign_transaction(tx, password=None)
self.assertEqual((1, 2), tx.signature_count())
txid = tx.txid() txid = tx.txid()
partial_tx = tx.serialize_as_bytes().hex() partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100eb01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c1130022020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a1053b77ddb010000800100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd51043067d63010000800100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98010b2e35a7d0100008001000000000000000000", self.assertEqual("70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100eb01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c1130022020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a1053b77ddb010000800100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd51043067d63010000800100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98010b2e35a7d0100008001000000000000000000",
@ -906,9 +912,9 @@ class TestWalletSending(ElectrumTestCase):
wallet1b.sign_transaction(tx, password=None) wallet1b.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertEqual((2, 2), tx.signature_count())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))
@ -925,6 +931,10 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1 # wallet2 -> wallet1
outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)] outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]
tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False) tx = wallet2a.mktx(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
self.assertEqual((1, 2), tx.signature_count())
self.assertEqual(
"sh(wsh(sortedmulti(2,[d1dbcc21]tpubDDsv4RpsGViZeEVwivuj3aaKhFQSv1kYsz64mwRoHkqBfw8qBSYEmc8TtyVGotJb44V3pviGzefP9m9hidRg9dPPaDWL2yoRpMW3hdje3Rk/0/0,[17cea914]tpubDCZU2kACPGACYDvAXvZUXQ7cE7msFfCtpah5QCuaz8iarKMLTgR4c2u8RGKdFhbb3YJxzmktDd1rCtF58ksyVgFw28pchY55uwkDiXjY9hU/0/0)))",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
txid = tx.txid() txid = tx.txid()
partial_tx = tx.serialize_as_bytes().hex() partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000", self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000",
@ -935,9 +945,9 @@ class TestWalletSending(ElectrumTestCase):
wallet2b.sign_transaction(tx, password=None) wallet2b.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertEqual((2, 2), tx.signature_count())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet2a.is_mine(wallet2a.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet2a.is_mine(wallet2a.adb.get_txin_address(tx_copy.inputs()[0])))
@ -987,7 +997,6 @@ class TestWalletSending(ElectrumTestCase):
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))
@ -1007,7 +1016,6 @@ class TestWalletSending(ElectrumTestCase):
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize()) tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0]))) self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))
@ -2615,6 +2623,9 @@ class TestWalletSending(ElectrumTestCase):
tx.version = 2 tx.version = 2
tx.locktime = 2378363 tx.locktime = 2378363
self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid())
self.assertEqual(
"wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[015148ee]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual({}, tx.to_json()['xpubs'])
self.assertEqual( self.assertEqual(
{'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"),
@ -2642,6 +2653,9 @@ class TestWalletSending(ElectrumTestCase):
tx.version = 2 tx.version = 2
tx.locktime = 2378363 tx.locktime = 2378363
self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid()) self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid())
self.assertEqual(
"wsh(sortedmulti(2,[9559fbd1/9999h]tpubD9MoDeHnEQnU5EMgt9mc4yKU6SURbfq2ooMToY5GH95B8Li1CEsuo9dBKXM2sdjuDGq4KCXLuigss3y22fZULzVrfVuZDxEN55Sp6CcU9DK/0/0,[30cf1be5/48h/1h/0h/2h]tpubDFF7YPCSGHZy55HkQj6HJkXCR8DWbKKXpTYBH38fSHf6VuoEzNmZQZdAoKEVy36S8zXkbGeV4XQU6vaRXGsQfgptFYPR4HSpAenqkY7J7Lg/0/0))",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
self.assertEqual({}, tx.to_json()['xpubs']) self.assertEqual({}, tx.to_json()['xpubs'])
self.assertEqual( self.assertEqual(
{'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"), {'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"),
@ -2726,6 +2740,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
tx.version = 1 tx.version = 1
self.assertFalse(tx.is_complete()) self.assertFalse(tx.is_complete())
self.assertEqual((0, 1), tx.signature_count())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
partial_tx = tx.serialize_as_bytes().hex() partial_tx = tx.serialize_as_bytes().hex()
@ -2739,6 +2754,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx # sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None) tx = wallet_offline.sign_transaction(tx_copy, password=None)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertEqual((1, 1), tx.signature_count())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual('01000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b020000008a47304402206bed3e02af8a38f6ba2fa3bf5908cb8c643aa62e78e8de6d9af2e19dec55fafc0220039cc1d81d4e5e0292bbc54ea92b8ec4ec016d4828eedc8975a66952cedf13a1014104e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f5fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600', self.assertEqual('01000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b020000008a47304402206bed3e02af8a38f6ba2fa3bf5908cb8c643aa62e78e8de6d9af2e19dec55fafc0220039cc1d81d4e5e0292bbc54ea92b8ec4ec016d4828eedc8975a66952cedf13a1014104e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f5fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600',
str(tx)) str(tx))
@ -2871,6 +2887,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
tx.version = 1 tx.version = 1
self.assertFalse(tx.is_complete()) self.assertFalse(tx.is_complete())
self.assertEqual((0, 1), tx.signature_count())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs())) self.assertEqual(1, len(tx.inputs()))
@ -2895,6 +2912,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx # sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None) tx = wallet_offline.sign_transaction(tx_copy, password=None)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertEqual((1, 1), tx.signature_count())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid()) self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid())
self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid()) self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid())
@ -3034,6 +3052,9 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx # sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None) tx = wallet_offline.sign_transaction(tx_copy, password=None)
self.assertEqual(
"sh(wpkh(03845818239fe468a9e7c7ae1a3d3653a8333f89ff316a771a3acf6854b4d8c6db))",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit()) self.assertTrue(tx.is_segwit())
self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid()) self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid())
@ -3112,6 +3133,9 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx # sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None) tx = wallet_offline.sign_transaction(tx_copy, password=None)
self.assertEqual(
"pkh([233d2ae4]tpubDDMN69wQjDZxaJz9afZQGa48hZS7X5oSegF2hg67yddNvqfpuTN9DqvDEp7YyVf7AzXnqBqHdLhzTAStHvsoMDDb8WoJQzNrcHgDJHVYgQF/0/1)",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit()) self.assertFalse(tx.is_segwit())
self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid()) self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid())
@ -3249,6 +3273,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx - first # sign tx - first
tx = wallet_offline1.sign_transaction(tx_copy, password=None) tx = wallet_offline1.sign_transaction(tx_copy, password=None)
self.assertFalse(tx.is_complete()) self.assertFalse(tx.is_complete())
self.assertEqual((1, 2), tx.signature_count())
partial_tx = tx.serialize_as_bytes().hex() partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000", self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000",
partial_tx) partial_tx)
@ -3257,6 +3282,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx - second # sign tx - second
tx = wallet_offline2.sign_transaction(tx, password=None) tx = wallet_offline2.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete()) self.assertTrue(tx.is_complete())
self.assertEqual((2, 2), tx.signature_count())
tx = tx_from_any(tx.serialize()) tx = tx_from_any(tx.serialize())
self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fc004730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101473044022052980154bdf2e43d6bd8775316cc220ef5ae13b4b9574a7a904a691ee3c5efd3022069b3eddf904cc645bd8fc8b2aaa7aaf7eb5bbfb7bbbd3b6e6cd89b37dfb2856c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400', self.assertEqual('010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c500000000fc004730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101473044022052980154bdf2e43d6bd8775316cc220ef5ae13b4b9574a7a904a691ee3c5efd3022069b3eddf904cc645bd8fc8b2aaa7aaf7eb5bbfb7bbbd3b6e6cd89b37dfb2856c014c69522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53aefdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400',

247
electrum/transaction.py

@ -47,11 +47,12 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr,
var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
int_to_hex, push_script, b58_address_to_hash160, int_to_hex, push_script, b58_address_to_hash160,
opcodes, add_number_to_script, base_decode, is_segwit_script_type, opcodes, add_number_to_script, base_decode,
base_encode, construct_witness, construct_script) base_encode, construct_witness, construct_script)
from .crypto import sha256d from .crypto import sha256d
from .logging import get_logger from .logging import get_logger
from .util import ShortID from .util import ShortID
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
if TYPE_CHECKING: if TYPE_CHECKING:
from .wallet import Abstract_Wallet from .wallet import Abstract_Wallet
@ -732,33 +733,6 @@ class Transaction:
if vds.can_read_more(): if vds.can_read_more():
raise SerializationError('extra junk at the end') raise SerializationError('extra junk at the end')
@classmethod
def get_siglist(self, txin: 'PartialTxInput', *, estimate_size=False):
if txin.is_coinbase_input():
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
num_sig = max(1, txin.num_sig)
# we guess that signatures will be 72 bytes long
# note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice
# See https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature
# We assume low S (as that is a bitcoin standardness rule).
# We do not assume low R (even though the sigs we create conform), as external sigs,
# e.g. from a hw signer cannot be expected to have a low R.
sig_list = ["00" * 72] * num_sig
else:
pk_list = [pubkey.hex() for pubkey in txin.pubkeys]
sig_list = [txin.part_sigs.get(pubkey, b'').hex() for pubkey in txin.pubkeys]
if txin.is_complete():
sig_list = [sig for sig in sig_list if sig]
return pk_list, sig_list
@classmethod @classmethod
def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str:
if txin.witness is not None: if txin.witness is not None:
@ -767,46 +741,21 @@ class Transaction:
return '' return ''
assert isinstance(txin, PartialTxInput) assert isinstance(txin, PartialTxInput)
_type = txin.script_type
if not txin.is_segwit(): if not txin.is_segwit():
return construct_witness([]) return construct_witness([])
if estimate_size and txin.witness_sizehint is not None: if estimate_size and txin.witness_sizehint is not None:
return '00' * txin.witness_sizehint return '00' * txin.witness_sizehint
if _type in ('address', 'unknown') and estimate_size:
_type = cls.guess_txintype_from_address(txin.address)
pubkeys, sig_list = cls.get_siglist(txin, estimate_size=estimate_size)
if _type in ['p2wpkh', 'p2wpkh-p2sh']:
return construct_witness([sig_list[0], pubkeys[0]])
elif _type in ['p2wsh', 'p2wsh-p2sh']:
witness_script = multisig_script(pubkeys, txin.num_sig)
return construct_witness([0, *sig_list, witness_script])
elif _type in ['p2pk', 'p2pkh', 'p2sh']:
return construct_witness([])
raise UnknownTxinType(f'cannot construct witness for txin_type: {_type}')
@classmethod dummy_desc = None
def guess_txintype_from_address(cls, addr: Optional[str]) -> str: if estimate_size:
# It's not possible to tell the script type in general dummy_desc = create_dummy_descriptor_from_address(txin.address)
# just from an address. if desc := (txin.script_descriptor or dummy_desc):
# - "1" addresses are of course p2pkh sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs)
# - "3" addresses are p2sh but we don't know the redeem script.. if sol.witness is not None:
# - "bc1" addresses (if they are 42-long) are p2wpkh return sol.witness.hex()
# - "bc1" addresses that are 62-long are p2wsh but we don't know the script.. return construct_witness([])
# If we don't know the script, we _guess_ it is pubkeyhash. raise UnknownTxinType("cannot construct witness")
# As this method is used e.g. for tx size estimation,
# the estimation will not be precise.
if addr is None:
return 'p2wpkh'
witver, witprog = segwit_addr.decode_segwit_address(constants.net.SEGWIT_HRP, addr)
if witprog is not None:
return 'p2wpkh'
addrtype, hash_160_ = b58_address_to_hash160(addr)
if addrtype == constants.net.ADDRTYPE_P2PKH:
return 'p2pkh'
elif addrtype == constants.net.ADDRTYPE_P2SH:
return 'p2wpkh-p2sh'
raise Exception(f'unrecognized address: {repr(addr)}')
@classmethod @classmethod
def input_script(self, txin: TxInput, *, estimate_size=False) -> str: def input_script(self, txin: TxInput, *, estimate_size=False) -> str:
@ -821,31 +770,19 @@ class Transaction:
if txin.is_native_segwit(): if txin.is_native_segwit():
return '' return ''
_type = txin.script_type dummy_desc = None
pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size) if estimate_size:
if _type in ('address', 'unknown') and estimate_size: dummy_desc = create_dummy_descriptor_from_address(txin.address)
_type = self.guess_txintype_from_address(txin.address) if desc := (txin.script_descriptor or dummy_desc):
if _type == 'p2pk': if desc.is_segwit():
return construct_script([sig_list[0]]) if redeem_script := desc.expand().redeem_script:
elif _type == 'p2sh': return construct_script([redeem_script])
# put op_0 before script return ""
redeem_script = multisig_script(pubkeys, txin.num_sig) sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs)
return construct_script([0, *sig_list, redeem_script]) if sol.script_sig is not None:
elif _type == 'p2pkh': return sol.script_sig.hex()
return construct_script([sig_list[0], pubkeys[0]]) return ""
elif _type in ['p2wpkh', 'p2wsh']: raise UnknownTxinType("cannot construct scriptSig")
return ''
elif _type == 'p2wpkh-p2sh':
redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0])
return construct_script([redeem_script])
elif _type == 'p2wsh-p2sh':
if estimate_size:
witness_script = ''
else:
witness_script = self.get_preimage_script(txin)
redeem_script = bitcoin.p2wsh_nested_script(witness_script)
return construct_script([redeem_script])
raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}')
@classmethod @classmethod
def get_preimage_script(cls, txin: 'PartialTxInput') -> str: def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
@ -858,18 +795,12 @@ class Transaction:
raise Exception('OP_CODESEPARATOR black magic is not supported') raise Exception('OP_CODESEPARATOR black magic is not supported')
return txin.redeem_script.hex() return txin.redeem_script.hex()
pubkeys = [pk.hex() for pk in txin.pubkeys] if desc := txin.script_descriptor:
if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']: sc = desc.expand()
return multisig_script(pubkeys, txin.num_sig) if script := sc.scriptcode_for_sighash:
elif txin.script_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']: return script.hex()
pubkey = pubkeys[0] raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}")
pkh = hash_160(bfh(pubkey)).hex() raise UnknownTxinType(f'cannot construct preimage_script')
return bitcoin.pubkeyhash_to_p2pkh_script(pkh)
elif txin.script_type == 'p2pk':
pubkey = pubkeys[0]
return bitcoin.public_key_to_p2pk_script(pubkey)
else:
raise UnknownTxinType(f'cannot construct preimage_script for txin_type: {txin.script_type}')
def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields: def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields:
inputs = self.inputs() inputs = self.inputs()
@ -1255,9 +1186,7 @@ class PartialTxInput(TxInput, PSBTSection):
self.witness_script = None # type: Optional[bytes] self.witness_script = None # type: Optional[bytes]
self._unknown = {} # type: Dict[bytes, bytes] self._unknown = {} # type: Dict[bytes, bytes]
self.script_type = 'unknown' self._script_descriptor = None # type: Optional[Descriptor]
self.num_sig = 0 # type: int # num req sigs for multisig
self.pubkeys = [] # type: List[bytes] # note: order matters
self._trusted_value_sats = None # type: Optional[int] self._trusted_value_sats = None # type: Optional[int]
self._trusted_address = None # type: Optional[str] self._trusted_address = None # type: Optional[str]
self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown
@ -1290,12 +1219,32 @@ class PartialTxInput(TxInput, PSBTSection):
self._witness_utxo = value self._witness_utxo = value
self.validate_data() self.validate_data()
@property
def pubkeys(self) -> Set[bytes]:
if desc := self.script_descriptor:
return desc.get_all_pubkeys()
return set()
@property
def script_descriptor(self):
return self._script_descriptor
@script_descriptor.setter
def script_descriptor(self, desc: Optional[Descriptor]):
self._script_descriptor = desc
if desc:
if self.redeem_script is None:
self.redeem_script = desc.expand().redeem_script
if self.witness_script is None:
self.witness_script = desc.expand().witness_script
def to_json(self): def to_json(self):
d = super().to_json() d = super().to_json()
d.update({ d.update({
'height': self.block_height, 'height': self.block_height,
'value_sats': self.value_sats(), 'value_sats': self.value_sats(),
'address': self.address, 'address': self.address,
'desc': self.script_descriptor.to_string() if self.script_descriptor else None,
'utxo': str(self.utxo) if self.utxo else None, 'utxo': str(self.utxo) if self.utxo else None,
'witness_utxo': self.witness_utxo.serialize_to_network().hex() if self.witness_utxo else None, 'witness_utxo': self.witness_utxo.serialize_to_network().hex() if self.witness_utxo else None,
'sighash': self.sighash, 'sighash': self.sighash,
@ -1468,22 +1417,6 @@ class PartialTxInput(TxInput, PSBTSection):
return self.witness_utxo.scriptpubkey return self.witness_utxo.scriptpubkey
return None return None
def set_script_type(self) -> None:
if self.scriptpubkey is None:
return
type = get_script_type_from_output_script(self.scriptpubkey)
inner_type = None
if type is not None:
if type == 'p2sh':
inner_type = get_script_type_from_output_script(self.redeem_script)
elif type == 'p2wsh':
inner_type = get_script_type_from_output_script(self.witness_script)
if inner_type is not None:
type = inner_type + '-' + type
if type in ('p2pkh', 'p2wpkh-p2sh', 'p2wpkh'):
self.script_type = type
return
def is_complete(self) -> bool: def is_complete(self) -> bool:
if self.script_sig is not None and self.witness is not None: if self.script_sig is not None and self.witness is not None:
return True return True
@ -1491,19 +1424,20 @@ class PartialTxInput(TxInput, PSBTSection):
return True return True
if self.script_sig is not None and not self.is_segwit(): if self.script_sig is not None and not self.is_segwit():
return True return True
signatures = list(self.part_sigs.values()) if desc := self.script_descriptor:
s = len(signatures) try:
# note: The 'script_type' field is currently only set by the wallet, desc.satisfy(allow_dummy=False, sigdata=self.part_sigs)
# for its own addresses. This means we can only finalize inputs except MissingSolutionPiece:
# that are related to the wallet. pass
# The 'fix' would be adding extra logic that matches on templates, else:
# and figures out the script_type from available fields. return True
if self.script_type in ('p2pk', 'p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
return s >= 1
if self.script_type in ('p2sh', 'p2wsh', 'p2wsh-p2sh'):
return s >= self.num_sig
return False return False
def get_satisfaction_progress(self) -> Tuple[int, int]:
if desc := self.script_descriptor:
return desc.get_satisfaction_progress(sigdata=self.part_sigs)
return 0, 0
def finalize(self) -> None: def finalize(self) -> None:
def clear_fields_when_finalized(): def clear_fields_when_finalized():
# BIP-174: "All other data except the UTXO and unknown fields in the # BIP-174: "All other data except the UTXO and unknown fields in the
@ -1594,10 +1528,12 @@ class PartialTxInput(TxInput, PSBTSection):
return False return False
if self.witness_script: if self.witness_script:
return True return True
_type = self.script_type if desc := self.script_descriptor:
if _type == 'address' and guess_for_address: return desc.is_segwit()
_type = Transaction.guess_txintype_from_address(self.address) if guess_for_address:
return is_segwit_script_type(_type) dummy_desc = create_dummy_descriptor_from_address(self.address)
return dummy_desc.is_segwit()
return False # can be false-negative
def already_has_some_signatures(self) -> bool: def already_has_some_signatures(self) -> bool:
"""Returns whether progress has been made towards completing this input.""" """Returns whether progress has been made towards completing this input."""
@ -1614,15 +1550,33 @@ class PartialTxOutput(TxOutput, PSBTSection):
self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path)
self._unknown = {} # type: Dict[bytes, bytes] self._unknown = {} # type: Dict[bytes, bytes]
self.script_type = 'unknown' self._script_descriptor = None # type: Optional[Descriptor]
self.num_sig = 0 # num req sigs for multisig
self.pubkeys = [] # type: List[bytes] # note: order matters
self.is_mine = False # type: bool # whether the wallet considers the output to be ismine self.is_mine = False # type: bool # whether the wallet considers the output to be ismine
self.is_change = False # type: bool # whether the wallet considers the output to be change self.is_change = False # type: bool # whether the wallet considers the output to be change
@property
def pubkeys(self) -> Set[bytes]:
if desc := self.script_descriptor:
return desc.get_all_pubkeys()
return set()
@property
def script_descriptor(self):
return self._script_descriptor
@script_descriptor.setter
def script_descriptor(self, desc: Optional[Descriptor]):
self._script_descriptor = desc
if desc:
if self.redeem_script is None:
self.redeem_script = desc.expand().redeem_script
if self.witness_script is None:
self.witness_script = desc.expand().witness_script
def to_json(self): def to_json(self):
d = super().to_json() d = super().to_json()
d.update({ d.update({
'desc': self.script_descriptor.to_string() if self.script_descriptor else None,
'redeem_script': self.redeem_script.hex() if self.redeem_script else None, 'redeem_script': self.redeem_script.hex() if self.redeem_script else None,
'witness_script': self.witness_script.hex() if self.witness_script else None, 'witness_script': self.witness_script.hex() if self.witness_script else None,
'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path))
@ -2013,15 +1967,12 @@ class PartialTransaction(Transaction):
return all([txin.is_complete() for txin in self.inputs()]) return all([txin.is_complete() for txin in self.inputs()])
def signature_count(self) -> Tuple[int, int]: def signature_count(self) -> Tuple[int, int]:
s = 0 # "num Sigs we have" nhave, nreq = 0, 0
r = 0 # "Required"
for txin in self.inputs(): for txin in self.inputs():
if txin.is_coinbase_input(): a, b = txin.get_satisfaction_progress()
continue nhave += a
signatures = list(txin.part_sigs.values()) nreq += b
s += len(signatures) return nhave, nreq
r += txin.num_sig
return s, r
def serialize(self) -> str: def serialize(self) -> str:
"""Returns PSBT as base64 text, or raw hex of network tx (if complete).""" """Returns PSBT as base64 text, or raw hex of network tx (if complete)."""
@ -2170,14 +2121,6 @@ class PartialTransaction(Transaction):
assert not self.is_complete() assert not self.is_complete()
self.invalidate_ser_cache() self.invalidate_ser_cache()
def update_txin_script_type(self):
"""Determine the script_type of each input by analyzing the scripts.
It updates all tx-Inputs, NOT only the wallet owned, if the
scriptpubkey is present.
"""
for txin in self.inputs():
if txin.script_type in ('unknown', 'address'):
txin.set_script_type()
def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes:
if len(xfp) != 4: if len(xfp) != 4:

173
electrum/wallet.py

@ -84,6 +84,8 @@ from .lnworker import LNWallet
from .paymentrequest import PaymentRequest from .paymentrequest import PaymentRequest
from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed
from .util import EventListener, event_listener from .util import EventListener, event_listener
from . import descriptor
from .descriptor import Descriptor
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@ -100,16 +102,15 @@ TX_STATUS = [
] ]
async def _append_utxos_to_inputs(*, inputs: List[PartialTxInput], network: 'Network', async def _append_utxos_to_inputs(
pubkey: str, txin_type: str, imax: int) -> None: *,
if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'): inputs: List[PartialTxInput],
address = bitcoin.pubkey_to_address(txin_type, pubkey) network: 'Network',
scripthash = bitcoin.address_to_scripthash(address) script_descriptor: 'descriptor.Descriptor',
elif txin_type == 'p2pk': imax: int,
script = bitcoin.public_key_to_p2pk_script(pubkey) ) -> None:
scripthash = bitcoin.script_to_scripthash(script) script = script_descriptor.expand().output_script.hex()
else: scripthash = bitcoin.script_to_scripthash(script)
raise Exception(f'unexpected txin_type to sweep: {txin_type}')
async def append_single_utxo(item): async def append_single_utxo(item):
prev_tx_raw = await network.get_transaction(item['tx_hash']) prev_tx_raw = await network.get_transaction(item['tx_hash'])
@ -122,11 +123,7 @@ async def _append_utxos_to_inputs(*, inputs: List[PartialTxInput], network: 'Net
txin = PartialTxInput(prevout=prevout) txin = PartialTxInput(prevout=prevout)
txin.utxo = prev_tx txin.utxo = prev_tx
txin.block_height = int(item['height']) txin.block_height = int(item['height'])
txin.script_type = txin_type txin.script_descriptor = script_descriptor
txin.pubkeys = [bfh(pubkey)]
txin.num_sig = 1
if txin_type == 'p2wpkh-p2sh':
txin.redeem_script = bfh(bitcoin.p2wpkh_nested_script(pubkey))
inputs.append(txin) inputs.append(txin)
u = await network.listunspent_for_scripthash(scripthash) u = await network.listunspent_for_scripthash(scripthash)
@ -141,11 +138,11 @@ async def sweep_preparations(privkeys, network: 'Network', imax=100):
async def find_utxos_for_privkey(txin_type, privkey, compressed): async 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)
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)
await _append_utxos_to_inputs( await _append_utxos_to_inputs(
inputs=inputs, inputs=inputs,
network=network, network=network,
pubkey=pubkey, script_descriptor=desc,
txin_type=txin_type,
imax=imax) imax=imax)
keypairs[pubkey] = privkey, compressed keypairs[pubkey] = privkey, compressed
@ -684,13 +681,19 @@ class Abstract_Wallet(ABC, Logger, EventListener):
""" """
pass pass
@abstractmethod
def get_redeem_script(self, address: str) -> Optional[str]: def get_redeem_script(self, address: str) -> Optional[str]:
pass desc = self.get_script_descriptor_for_address(address)
if desc is None: return None
redeem_script = desc.expand().redeem_script
if redeem_script:
return redeem_script.hex()
@abstractmethod
def get_witness_script(self, address: str) -> Optional[str]: def get_witness_script(self, address: str) -> Optional[str]:
pass desc = self.get_script_descriptor_for_address(address)
if desc is None: return None
witness_script = desc.expand().witness_script
if witness_script:
return witness_script.hex()
@abstractmethod @abstractmethod
def get_txin_type(self, address: str) -> str: def get_txin_type(self, address: str) -> str:
@ -2138,10 +2141,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
tx_new.add_info_from_wallet(self) tx_new.add_info_from_wallet(self)
return tx_new return tx_new
@abstractmethod
def _add_input_sig_info(self, txin: PartialTxInput, address: str, *, only_der_suffix: bool) -> None:
pass
def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput], def _add_txinout_derivation_info(self, txinout: Union[PartialTxInput, PartialTxOutput],
address: str, *, only_der_suffix: bool) -> None: address: str, *, only_der_suffix: bool) -> None:
pass # implemented by subclasses pass # implemented by subclasses
@ -2194,24 +2193,44 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if self.lnworker: if self.lnworker:
self.lnworker.swap_manager.add_txin_info(txin) self.lnworker.swap_manager.add_txin_info(txin)
return return
# set script_type first, as later checks might rely on it: txin.script_descriptor = self.get_script_descriptor_for_address(address)
txin.script_type = self.get_txin_type(address) self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1
if txin.redeem_script is None:
try:
redeem_script_hex = self.get_redeem_script(address)
txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
except UnknownTxinType:
pass
if txin.witness_script is None:
try:
witness_script_hex = self.get_witness_script(address)
txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None
except UnknownTxinType:
pass
self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix)
txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height
def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]:
if not self.is_mine(address):
return None
script_type = self.get_txin_type(address)
if script_type in ('address', 'unknown'):
return None
addr_index = self.get_address_index(address)
if addr_index is None:
return None
pubkeys = [ks.get_pubkey_provider(addr_index) for ks in self.get_keystores()]
if not pubkeys:
return None
if script_type == 'p2pk':
return descriptor.PKDescriptor(pubkey=pubkeys[0])
elif script_type == 'p2pkh':
return descriptor.PKHDescriptor(pubkey=pubkeys[0])
elif script_type == 'p2wpkh':
return descriptor.WPKHDescriptor(pubkey=pubkeys[0])
elif script_type == 'p2wpkh-p2sh':
wpkh = descriptor.WPKHDescriptor(pubkey=pubkeys[0])
return descriptor.SHDescriptor(subdescriptor=wpkh)
elif script_type == 'p2sh':
multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
return descriptor.SHDescriptor(subdescriptor=multi)
elif script_type == 'p2wsh':
multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
return descriptor.WSHDescriptor(subdescriptor=multi)
elif script_type == 'p2wsh-p2sh':
multi = descriptor.MultisigDescriptor(pubkeys=pubkeys, thresh=self.m, is_sorted=True)
wsh = descriptor.WSHDescriptor(subdescriptor=multi)
return descriptor.SHDescriptor(subdescriptor=wsh)
else:
raise NotImplementedError(f"unexpected {script_type=}")
def can_sign(self, tx: Transaction) -> bool: def can_sign(self, tx: Transaction) -> bool:
if not isinstance(tx, PartialTransaction): if not isinstance(tx, PartialTransaction):
return False return False
@ -2263,24 +2282,10 @@ class Abstract_Wallet(ABC, Logger, EventListener):
is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address) is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
if not is_mine: if not is_mine:
return return
txout.script_type = self.get_txin_type(address) txout.script_descriptor = self.get_script_descriptor_for_address(address)
txout.is_mine = True txout.is_mine = True
txout.is_change = self.is_change(address) txout.is_change = self.is_change(address)
if isinstance(self, Multisig_Wallet):
txout.num_sig = self.m
self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix) self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
if txout.redeem_script is None:
try:
redeem_script_hex = self.get_redeem_script(address)
txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
except UnknownTxinType:
pass
if txout.witness_script is None:
try:
witness_script_hex = self.get_witness_script(address)
txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None
except UnknownTxinType:
pass
def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]: def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]:
if self.is_watching_only(): if self.is_watching_only():
@ -2942,20 +2947,6 @@ class Simple_Wallet(Abstract_Wallet):
def get_public_keys(self, address: str) -> Sequence[str]: def get_public_keys(self, address: str) -> Sequence[str]:
return [self.get_public_key(address)] return [self.get_public_key(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)
if txin_type == 'address':
return None
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
def get_witness_script(self, address: str) -> Optional[str]:
return None
class Imported_Wallet(Simple_Wallet): class Imported_Wallet(Simple_Wallet):
# wallet made of imported addresses # wallet made of imported addresses
@ -3163,20 +3154,6 @@ class Imported_Wallet(Simple_Wallet):
if addr != bitcoin.pubkey_to_address(txin_type, pubkey): if addr != bitcoin.pubkey_to_address(txin_type, pubkey):
raise InternalAddressCorruption() raise InternalAddressCorruption()
def _add_input_sig_info(self, txin, address, *, only_der_suffix):
if not self.is_mine(address):
return
if txin.script_type in ('unknown', 'address'):
return
elif txin.script_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
pubkey = self.get_public_key(address)
if not pubkey:
return
txin.pubkeys = [bfh(pubkey)]
else:
raise Exception(f'Unexpected script type: {txin.script_type}. '
f'Imported wallets are not implemented to handle this.')
def pubkeys_to_address(self, pubkeys): def pubkeys_to_address(self, pubkeys):
pubkey = pubkeys[0] pubkey = pubkeys[0]
# FIXME This is slow. # FIXME This is slow.
@ -3304,14 +3281,10 @@ class Deterministic_Wallet(Abstract_Wallet):
return {k.derive_pubkey(*der_suffix): (k, der_suffix) return {k.derive_pubkey(*der_suffix): (k, der_suffix)
for k in self.get_keystores()} for k in self.get_keystores()}
def _add_input_sig_info(self, txin, address, *, only_der_suffix):
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix): def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):
if not self.is_mine(address): if not self.is_mine(address):
return return
pubkey_deriv_info = self.get_public_keys_with_deriv_info(address) pubkey_deriv_info = self.get_public_keys_with_deriv_info(address)
txinout.pubkeys = sorted([pk for pk in list(pubkey_deriv_info)])
for pubkey in pubkey_deriv_info: for pubkey in pubkey_deriv_info:
ks, der_suffix = pubkey_deriv_info[pubkey] ks, der_suffix = pubkey_deriv_info[pubkey]
fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix, fp_bytes, der_full = ks.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix,
@ -3423,7 +3396,7 @@ class Simple_Deterministic_Wallet(Simple_Wallet, Deterministic_Wallet):
return pubkeys[0] return pubkeys[0]
def load_keystore(self): def load_keystore(self):
self.keystore = load_keystore(self.db, 'keystore') self.keystore = load_keystore(self.db, 'keystore') # type: KeyStoreWithMPK
try: try:
xtype = bip32.xpub_type(self.keystore.xpub) xtype = bip32.xpub_type(self.keystore.xpub)
except: except:
@ -3467,28 +3440,6 @@ class Multisig_Wallet(Deterministic_Wallet):
def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str: 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):
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)
scriptcode = self.pubkeys_to_scriptcode(pubkeys)
if txin_type == 'p2sh':
return None
elif txin_type in ('p2wsh-p2sh', 'p2wsh'):
return scriptcode
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
def derive_pubkeys(self, c, i): def derive_pubkeys(self, c, i):
return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()] return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]

Loading…
Cancel
Save