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
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
import binascii
import hashlib
import struct
from typing import List, Tuple, NamedTuple, Union, Iterable, Sequence, Optional
from .util import bfh, BitcoinException
@ -122,7 +124,13 @@ class BIP32Node(NamedTuple):
child_number: bytes = b'\x00'*4
@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:
net = constants.net
xkey = DecodeBase58Check(xkey)
@ -143,6 +151,8 @@ class BIP32Node(NamedTuple):
else:
raise InvalidMasterKeyVersionBytes(f'Invalid extended key format: {hex(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:
eckey = ecc.ECPrivkey(xkey[13 + 33:])
else:
@ -324,14 +334,18 @@ def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
# makes concatenating paths easier
continue
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]
prime = BIP32_PRIME
if x.startswith('-'):
if prime:
raise ValueError(f"bip32 path child index is signalling hardened level in multiple ways")
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:
raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}")
path.append(child_index)
@ -426,3 +440,84 @@ def is_xkey_consistent_with_key_origin_info(xkey: str, *,
if bfh(root_fingerprint) != bip32node.fingerprint:
return False
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])
def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
if txin_type == 'p2pkh':
return public_key_to_p2pkh(bfh(pubkey), net=net)
elif txin_type == 'p2wpkh':
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)
from . import descriptor
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)
return desc.expand().address(net=net)
# 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)
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
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 crypto
from . import constants
from . import descriptor
if TYPE_CHECKING:
from .network import Network
@ -394,9 +395,8 @@ class Commands:
txin_type, privkey, compressed = bitcoin.deserialize_privkey(sec)
pubkey = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
keypairs[pubkey] = privkey, compressed
txin.script_type = txin_type
txin.pubkeys = [bfh(pubkey)]
txin.num_sig = 1
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=txin_type)
txin.script_descriptor = desc
inputs.append(txin)
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:
txin_type, priv2, compressed = bitcoin.deserialize_privkey(priv)
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():
for txin in txins_dict[address]:
txin.pubkeys = [pubkey]
txin.script_type = txin_type
txin.script_descriptor = desc
tx.sign({pubkey.hex(): (priv2, compressed)})
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 .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME,
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 .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST,
SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160,
@ -179,6 +181,10 @@ class KeyStore(Logger, ABC):
"""
pass
@abstractmethod
def get_pubkey_provider(self, sequence: 'AddressIndexGeneric') -> Optional[PubkeyProvider]:
pass
def find_my_pubkey_in_txinout(
self, txinout: Union['PartialTxInput', 'PartialTxOutput'],
*, only_der_suffix: bool = False
@ -302,6 +308,15 @@ class Imported_KeyStore(Software_KeyStore):
return pubkey.hex()
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):
self.check_password(old_password)
if new_password == '':
@ -403,6 +418,9 @@ class MasterPublicKeyMixin(ABC):
"""
pass
def get_key_origin_info(self) -> Optional[KeyOriginInfo]:
return None
@abstractmethod
def derive_pubkey(self, for_change: int, n: int) -> bytes:
"""Returns pubkey at given path.
@ -532,6 +550,22 @@ class Xpub(MasterPublicKeyMixin):
)
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):
assert self.xpub
# 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)
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):
self.check_password(old_password)
if new_password == '':

6
electrum/lnsweep.py

@ -8,6 +8,7 @@ from enum import Enum, auto
from .util import bfh
from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness
from .invoices import PR_PAID
from . import descriptor
from . import ecc
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
@ -513,9 +514,8 @@ def create_sweeptx_their_ctx_to_remote(
prevout = TxOutpoint(txid=bfh(ctx.txid()), out_idx=output_idx)
txin = PartialTxInput(prevout=prevout)
txin._trusted_value_sats = val
txin.script_type = 'p2wpkh'
txin.pubkeys = [bfh(our_payment_pubkey)]
txin.num_sig = 1
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=our_payment_pubkey, script_type='p2wpkh')
txin.script_descriptor = desc
sweep_inputs = [txin]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)

8
electrum/lnutil.py

@ -21,6 +21,7 @@ from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOut
PartialTxOutput, opcodes, TxOutput)
from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
from . import ecc, bitcoin, crypto, transaction
from . import descriptor
from .bitcoin import (push_script, redeem_script_to_address, address_to_script,
construct_witness, construct_script)
from . import segwit_addr
@ -818,9 +819,10 @@ def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes
# commitment tx input
prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)
c_input = PartialTxInput(prevout=prevout)
c_input.script_type = 'p2wsh'
c_input.pubkeys = [bfh(pk) for pk in pubkeys]
c_input.num_sig = 2
ppubkeys = [descriptor.PubkeyProvider.parse(pk) for pk in pubkeys]
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
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:
tx_script_type = txin.script_type
elif tx_script_type != txin.script_type:
tx_script_type = desc.to_legacy_electrum_script_type()
elif tx_script_type != desc.to_legacy_electrum_script_type():
raise Exception("Cannot mix different input script types")
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.bitcoin import public_key_to_p2pkh
from electrum.bip32 import BIP32Node, convert_bip32_intpath_to_strpath, is_all_public_derivation
from electrum import descriptor
from electrum import ecc
from electrum.ecc import msg_magic
from electrum.wallet import Standard_Wallet
@ -527,7 +528,8 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
if txin.is_coinbase_input():
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
my_pubkey, inputPath = self.find_my_pubkey_in_txinout(txin)
@ -557,9 +559,10 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
tx_copy = copy.deepcopy(tx)
# monkey-patch method of tx_copy instance to change serialization
def input_script(self, txin: PartialTxInput, *, estimate_size=False):
if txin.script_type == 'p2pkh':
desc = txin.script_descriptor
if isinstance(desc, descriptor.PKHDescriptor):
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_dbb_serialized = tx_copy.serialize_to_network()
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."))
def get_xpubs_and_der_suffixes_from_txinout(tx: PartialTransaction,
txinout: Union[PartialTxInput, PartialTxOutput]) \
-> List[Tuple[str, List[int]]]:
xfp_to_xpub_map = {xfp: bip32node for bip32node, (xfp, path)
in tx.xpubs.items()} # type: Dict[bytes, BIP32Node]
xfps = [txinout.bip32_paths[pubkey][0] for pubkey in txinout.pubkeys]
try:
xpubs = [xfp_to_xpub_map[xfp] for xfp in xfps]
except KeyError as e:
raise Exception(f"Partial transaction is missing global xpub for "
f"fingerprint ({str(e)}) in input/output") from e
xpubs_and_deriv_suffixes = []
for bip32node, pubkey in zip(xpubs, txinout.pubkeys):
xfp, path = txinout.bip32_paths[pubkey]
der_suffix = list(path)[bip32node.depth:]
xpubs_and_deriv_suffixes.append((bip32node.to_xpub(), der_suffix))
return xpubs_and_deriv_suffixes
def only_hook_if_libraries_available(func):
# note: this decorator must wrap @hook, not the other way around,
# as 'hook' uses the name of the function it wraps

5
electrum/plugins/jade/jade.py

@ -264,7 +264,7 @@ class Jade_KeyStore(Hardware_KeyStore):
jade_inputs = []
for txin in tx.inputs():
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 = bytes.fromhex(redeem_script) if redeem_script is not None else None
input_tx = txin.utxo
@ -280,6 +280,7 @@ class Jade_KeyStore(Hardware_KeyStore):
change = [None] * len(tx.outputs())
for index, txout in enumerate(tx.outputs()):
if txout.is_mine and txout.is_change:
assert (desc := txout.script_descriptor)
if is_multisig:
# Multisig - wallet details must be registered on Jade hw
multisig_name = _register_multisig_wallet(wallet, self, txout.address)
@ -294,7 +295,7 @@ class Jade_KeyStore(Hardware_KeyStore):
else:
# Pass entire path
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
txn_bytes = bytes.fromhex(tx.serialize_to_network())

49
electrum/plugins/keepkey/keepkey.py

@ -1,10 +1,11 @@
from binascii import hexlify, unhexlify
import traceback
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.bip32 import BIP32Node
from electrum import descriptor
from electrum import constants
from electrum.i18n import _
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 ..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,
get_xpubs_and_der_suffixes_from_txinout)
from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
if TYPE_CHECKING:
import usb1
@ -271,7 +271,7 @@ class KeepKeyPlugin(HW_PluginBase):
client.load_device_by_xprv(item, pin, passphrase_protection,
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)
node = self.types.HDNodeType(
depth=bip32node.depth,
@ -351,14 +351,9 @@ class KeepKeyPlugin(HW_PluginBase):
script_type = self.get_keepkey_input_script_type(wallet.txin_type)
# prepare multisig, if available:
xpubs = wallet.get_master_public_keys()
if len(xpubs) > 1:
pubkeys = wallet.get_public_keys(address)
# 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])
desc = wallet.get_script_descriptor_for_address(address)
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
multisig = None
@ -376,12 +371,12 @@ class KeepKeyPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput)
assert keystore
if len(txin.pubkeys) > 1:
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
assert (desc := txin.script_descriptor)
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
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(
script_type=script_type,
multisig=multisig)
@ -406,22 +401,26 @@ class KeepKeyPlugin(HW_PluginBase):
return inputs
def _make_multisig(self, m, xpubs):
if len(xpubs) == 1:
return None
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
def _make_multisig(self, desc: descriptor.MultisigDescriptor):
pubkeys = []
for pubkey_provider in desc.pubkeys:
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(
pubkeys=pubkeys,
signatures=[b''] * len(pubkeys),
m=m)
m=desc.thresh)
def tx_outputs(self, tx: PartialTransaction, *, keystore: 'KeepKey_KeyStore'):
def create_output_by_derivation():
script_type = self.get_keepkey_output_script_type(txout.script_type)
if len(txout.pubkeys) > 1:
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
assert (desc := txout.script_descriptor)
script_type = self.get_keepkey_output_script_type(desc.to_legacy_electrum_script_type())
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
multisig = None
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 descriptor
from electrum.base_wizard import ScriptTypeNotSupported
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
@ -16,7 +17,7 @@ from electrum.i18n import _
from electrum.keystore import Hardware_KeyStore
from electrum.logging import get_logger
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.wallet import Standard_Wallet
@ -544,20 +545,25 @@ class Ledger_Client_Legacy(Ledger_Client):
pin = ""
# 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
for txin in tx.inputs():
if txin.is_coinbase_input():
self.give_error("Coinbase not supported") # should never happen
if txin.script_type in ['p2sh']:
if is_txin_legacy_multisig(txin):
p2shTransaction = True
if txin.script_type in ['p2wpkh-p2sh', 'p2wsh-p2sh']:
if txin.is_p2sh_segwit():
if not self.supports_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True
if txin.script_type in ['p2wpkh', 'p2wsh']:
if txin.is_native_segwit():
if not self.supports_native_segwit():
self.give_error(MSG_NEEDS_FW_UPDATE_SEGWIT)
segwitTransaction = True
@ -584,7 +590,7 @@ class Ledger_Client_Legacy(Ledger_Client):
# Sanity check
if p2shTransaction:
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
txOutput = var_int(len(tx.outputs()))
@ -1083,7 +1089,6 @@ class Ledger_Client_New(Ledger_Client):
raise UserFacingException("Coinbase not supported") # should never happen
utxo = None
scriptcode = b""
if psbt_in.witness_utxo:
utxo = psbt_in.witness_utxo
if psbt_in.non_witness_utxo:
@ -1094,19 +1099,9 @@ class Ledger_Client_New(Ledger_Client):
if utxo is None:
continue
scriptcode = utxo.scriptPubKey
if electrum_txin.script_type in ['p2sh', 'p2wpkh-p2sh']:
if len(psbt_in.redeem_script) == 0:
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
if (desc := electrum_txin.script_descriptor) is None:
raise Exception("script_descriptor missing for txin ")
scriptcode = desc.expand().scriptcode_for_sighash
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
psbt_in.witness_utxo = utxo
if p2sh:
if electrum_txin.is_p2sh_segwit():
if wit_ver == 0:
script_addrtype = AddressType.SH_WIT
else:

49
electrum/plugins/safe_t/safe_t.py

@ -1,10 +1,11 @@
from binascii import hexlify, unhexlify
import traceback
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.bip32 import BIP32Node
from electrum import descriptor
from electrum import constants
from electrum.i18n import _
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 ..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,
get_xpubs_and_der_suffixes_from_txinout)
from ..hw_wallet.plugin import is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data
if TYPE_CHECKING:
from .client import SafeTClient
@ -241,7 +241,7 @@ class SafeTPlugin(HW_PluginBase):
client.load_device_by_xprv(item, pin, passphrase_protection,
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)
node = self.types.HDNodeType(
depth=bip32node.depth,
@ -321,14 +321,9 @@ class SafeTPlugin(HW_PluginBase):
script_type = self.get_safet_input_script_type(wallet.txin_type)
# prepare multisig, if available:
xpubs = wallet.get_master_public_keys()
if len(xpubs) > 1:
pubkeys = wallet.get_public_keys(address)
# 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])
desc = wallet.get_script_descriptor_for_address(address)
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
multisig = None
@ -346,12 +341,12 @@ class SafeTPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput)
assert keystore
if len(txin.pubkeys) > 1:
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
assert (desc := txin.script_descriptor)
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
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(
script_type=script_type,
multisig=multisig)
@ -376,22 +371,26 @@ class SafeTPlugin(HW_PluginBase):
return inputs
def _make_multisig(self, m, xpubs):
if len(xpubs) == 1:
return None
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
def _make_multisig(self, desc: descriptor.MultisigDescriptor):
pubkeys = []
for pubkey_provider in desc.pubkeys:
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(
pubkeys=pubkeys,
signatures=[b''] * len(pubkeys),
m=m)
m=desc.thresh)
def tx_outputs(self, tx: PartialTransaction, *, keystore: 'SafeTKeyStore'):
def create_output_by_derivation():
script_type = self.get_safet_output_script_type(txout.script_type)
if len(txout.pubkeys) > 1:
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
assert (desc := txout.script_descriptor)
script_type = self.get_safet_output_script_type(desc.to_legacy_electrum_script_type())
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
multisig = None
my_pubkey, full_path = keystore.find_my_pubkey_in_txinout(txout)

49
electrum/plugins/trezor/trezor.py

@ -1,9 +1,10 @@
import traceback
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.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
from electrum import descriptor
from electrum import constants
from electrum.i18n import _
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.plugin import (is_any_tx_output_on_change_branch, trezor_validate_op_return_output_and_get_data,
LibraryFoundButUnusable, OutdatedHwFirmwareException,
get_xpubs_and_der_suffixes_from_txinout)
LibraryFoundButUnusable, OutdatedHwFirmwareException)
_logger = get_logger(__name__)
@ -284,7 +284,7 @@ class TrezorPlugin(HW_PluginBase):
else:
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)
node = HDNodeType(
depth=bip32node.depth,
@ -386,14 +386,9 @@ class TrezorPlugin(HW_PluginBase):
script_type = self.get_trezor_input_script_type(wallet.txin_type)
# prepare multisig, if available:
xpubs = wallet.get_master_public_keys()
if len(xpubs) > 1:
pubkeys = wallet.get_public_keys(address)
# 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])
desc = wallet.get_script_descriptor_for_address(address)
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
multisig = None
@ -417,10 +412,10 @@ class TrezorPlugin(HW_PluginBase):
assert isinstance(tx, PartialTransaction)
assert isinstance(txin, PartialTxInput)
assert keystore
if len(txin.pubkeys) > 1:
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txin)
txinputtype.multisig = self._make_multisig(txin.num_sig, xpubs_and_deriv_suffixes)
txinputtype.script_type = self.get_trezor_input_script_type(txin.script_type)
assert (desc := txin.script_descriptor)
if multi := desc.get_simple_multisig():
txinputtype.multisig = self._make_multisig(multi)
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)
if full_path:
txinputtype.address_n = full_path
@ -433,22 +428,26 @@ class TrezorPlugin(HW_PluginBase):
return inputs
def _make_multisig(self, m, xpubs):
if len(xpubs) == 1:
return None
pubkeys = [self._make_node_path(xpub, deriv) for xpub, deriv in xpubs]
def _make_multisig(self, desc: descriptor.MultisigDescriptor):
pubkeys = []
for pubkey_provider in desc.pubkeys:
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(
pubkeys=pubkeys,
signatures=[b''] * len(pubkeys),
m=m)
m=desc.thresh)
def tx_outputs(self, tx: PartialTransaction, *, keystore: 'TrezorKeyStore'):
def create_output_by_derivation():
script_type = self.get_trezor_output_script_type(txout.script_type)
if len(txout.pubkeys) > 1:
xpubs_and_deriv_suffixes = get_xpubs_and_der_suffixes_from_txinout(tx, txout)
multisig = self._make_multisig(txout.num_sig, xpubs_and_deriv_suffixes)
assert (desc := txout.script_descriptor)
script_type = self.get_trezor_output_script_type(desc.to_legacy_electrum_script_type())
if multi := desc.get_simple_multisig():
multisig = self._make_multisig(multi)
else:
multisig = None
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
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
BECH32M_CONST = 0x2bc830a3

4
electrum/submarine_swaps.py

@ -124,7 +124,6 @@ def create_claim_tx(
"""Create tx to either claim successful reverse-swap,
or to get refunded for timed-out forward-swap.
"""
txin.script_type = 'p2wsh'
txin.script_sig = b''
txin.witness_script = witness_script
txout = PartialTxOutput.from_address_and_value(address, amount_sat)
@ -622,8 +621,6 @@ class SwapManager(Logger):
return
preimage = swap.preimage if swap.is_reverse else 0
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.witness_script = witness_script
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]
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
txin.script_type = 'p2wsh'
txin.script_sig = b''
txin.witness_script = witness_script
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('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):
self.assertTrue(is_bip32_derivation("m/0'/1"))
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,
construct_script, construct_witness)
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
signed_blob = '01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65bf38633b424eb4031000000006c493046022100a82bbc57a0136751e5433f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7de89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d985012102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a42146f11ef8414ae929feaafc388ac00000000'
@ -89,9 +90,10 @@ class TestTransaction(ElectrumTestCase):
def test_tx_update_signatures(self):
tx = tx_from_any("cHNidP8BAFUBAAAAASpcmpT83pj1WBzQAWLGChOTbOt1OJ6mW/OGM7Qk60AxAAAAAAD/////AUBCDwAAAAAAGXapFCMKw3g0BzpCFG8R74QUrpKf6q/DiKwAAAAAAAAA")
tx.inputs()[0].script_type = 'p2pkh'
tx.inputs()[0].pubkeys = [bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')]
tx.inputs()[0].num_sig = 1
pubkey = bfh('02e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae35cdb84d2f6')
script_type = 'p2pkh'
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)
self.assertEqual(tx.serialize(), signed_blob)
@ -873,7 +875,6 @@ class TestTransactionTestnet(ElectrumTestCase):
prevout = TxOutpoint(txid=bfh('6d500966f9e494b38a04545f0cea35fc7b3944e341a64b804fed71cdee11d434'), out_idx=1)
txin = PartialTxInput(prevout=prevout)
txin.nsequence = 2 ** 32 - 3
txin.script_type = 'p2sh'
redeem_script = bfh(construct_script([
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)
txin = PartialTxInput(prevout=prevout)
txin.nsequence=0xffffffff
txin.script_type='p2sh-p2wsh'
txin.witness_script = bfh('56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae')
txin.redeem_script = bfh('0020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54')
txin._trusted_value_sats = 987654321
@ -945,7 +945,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_all(self):
self.txin.sighash=Sighash.ALL
self.txin.pubkeys = [bfh('0307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba3')]
privkey = bfh('730fff80e1413068a05b57d6a58261f07551163369787f349438ea38ca80fac6')
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)
@ -954,7 +953,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_none(self):
self.txin.sighash=Sighash.NONE
self.txin.pubkeys = [bfh('03b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b')]
privkey = bfh('11fa3d25a17cbc22b29c44a484ba552b5a53149d106d3d853e22fdd05a2d8bb3')
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)
@ -963,7 +961,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_single(self):
self.txin.sighash=Sighash.SINGLE
self.txin.pubkeys = [bfh('034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a')]
privkey = bfh('77bf4141a87d55bdd7f3cd0bdccf6e9e642935fec45f2f30047be7b799120661')
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)
@ -973,7 +970,6 @@ class TestSighashTypes(ElectrumTestCase):
@disable_ecdsa_r_value_grinding
def test_check_sighash_types_sighash_all_anyonecanpay(self):
self.txin.sighash=Sighash.ALL|Sighash.ANYONECANPAY
self.txin.pubkeys = [bfh('033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f4')]
privkey = bfh('14af36970f5025ea3e8b5542c0f8ebe7763e674838d08808896b63c3351ffe49')
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)
@ -983,7 +979,6 @@ class TestSighashTypes(ElectrumTestCase):
@disable_ecdsa_r_value_grinding
def test_check_sighash_types_sighash_none_anyonecanpay(self):
self.txin.sighash=Sighash.NONE|Sighash.ANYONECANPAY
self.txin.pubkeys = [bfh('03a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac16')]
privkey = bfh('fe9a95c19eef81dde2b95c1284ef39be497d128e2aa46916fb02d552485e0323')
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)
@ -992,7 +987,6 @@ class TestSighashTypes(ElectrumTestCase):
def test_check_sighash_types_sighash_single_anyonecanpay(self):
self.txin.sighash=Sighash.SINGLE|Sighash.ANYONECANPAY
self.txin.pubkeys = [bfh('02d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b')]
privkey = bfh('428a7aee9f0c2af0cd19af3cf1c78149951ea528726989b2e83e4778d2c3f890')
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)

46
electrum/tests/test_wallet_vertical.py

@ -729,7 +729,6 @@ class TestWalletSending(ElectrumTestCase):
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
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.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
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.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))
@ -824,12 +821,15 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1
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.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet2.is_mine(wallet2.adb.get_txin_address(tx_copy.inputs()[0])))
@ -895,7 +895,13 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 -> wallet2
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()
partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff01007e0100000001213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf874387000000000001012b400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c0100eb01000000000101a41aae475d026c9255200082c7fad26dc47771275b0afba238dccda98a597bd20000000000fdffffff02400d0300000000002200203c43ac80d6e3015cf378bf6bac0c22456723d6050bef324ec641e7762440c63c9dcd410000000000160014824626055515f3ed1d2cfc9152d2e70685c71e8f02483045022100b9f39fad57d07ce1e18251424034f21f10f20e59931041b5167ae343ce973cf602200fefb727fa0ffd25b353f1bcdae2395898fe407b692c62f5885afbf52fa06f5701210301a28f68511ace43114b674371257bb599fd2c686c4b19544870b1799c954b40e9c1130022020223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf0101056952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae22060223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa10b2e35a7d01000080000000000000000022060273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e1053b77ddb010000800000000000000000220602aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae9411043067d6301000080000000000000000000010169522102174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a2102c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd52102eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98053ae220202174696a58a8dcd6c6455bd25e0749e9a6fc7d84ee09e192ab37b0d0b18c2de1a1053b77ddb010000800100000000000000220202c807a19ca6783261f8c198ffcc437622e7ecba8d6c5692f3a5e7f1e45af53fd51043067d63010000800100000000000000220202eee40c7e24d89639182db32f5e9188613e4bc212da2ee9b4ccc85d9b82e1a98010b2e35a7d0100008001000000000000000000",
@ -906,9 +912,9 @@ class TestWalletSending(ElectrumTestCase):
wallet1b.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertEqual((2, 2), tx.signature_count())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
self.assertTrue(wallet1a.is_mine(wallet1a.adb.get_txin_address(tx_copy.inputs()[0])))
@ -925,6 +931,10 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1
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)
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()
partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff01007e010000000149d077be0ee9d52776211e9b4fec1cc02bd53661a04e120a97db8b78d83c9c6e0100000000feffffff0260ea00000000000017a9143025051b6b5ccd4baf30dfe2de8aa84f0dd567ed87a086010000000000220020f7b6b30c3073ae2680a7e90c589bbfec5303331be68bbab843eed5d51ba012390000000000010120888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870100fd7c0101000000000101213e1012a461e056752fab5a6414a2fb63f950cd21a50ac5e2b82d339d6cbdd20000000000feffffff023075000000000000220020cc5e4cc05a76d0648cd0742768556317e9f8cc729aed077134287909035dba88888402000000000017a914187842cea9c15989a51ce7ca889a08b824bf8743870400473044022055cb04fa71c4b5955724d7ac5da90436d75212e7847fc121cb588f54bcdffdc4022064eca1ad639b7c748101059dc69f2893abb3b396bcf9c13f670415076f93ddbf01473044022009230e456724f2a4c10d886c836eeec599b21db0bf078aa8fc8c95868b8920ec02200dfda835a66acb5af50f0d95fcc4b76c6e8f4789a7184c182275b087d1efe556016952210223f815ab09f6bfc8519165c5232947ae89d9d43d678fb3486f3b28382a2371fa210273c529c2c9a99592f2066cebc2172a48991af2b471cb726b9df78c6497ce984e2102aa8fc578b445a1e4257be6b978fcece92980def98dce0e1eb89e7364635ae94153ae00000000220202119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb14730440220091ea67af7c1131f51f62fe9596dff0a60c8b45bfc5be675389e193912e8a71802201bf813bbf83933a35ecc46e2d5b0442bd8758fa82e0f8ed16392c10d51f7f7660101042200204311edae835c7a5aa712c8ca644180f13a3b2f3b420fa879b181474724d6163c010547522102119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb12102fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab812652ae220602119f899075a131d4d519d4cdcf5de5907dc2df3b93d54b53ded852211d2b6cb10cd1dbcc210000000000000000220602fdb0f6775d4b6619257c43343ba5e7807b0164f1eb3f00f2b594ab9e53ab81260c17cea9140000000000000000000100220020717ab7037b81797cb3e192a8a1b4d88083444bbfcd26934cadf3bcf890f14e05010147522102987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde21034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f9952ae220202987c184fcd8ace2e2a314250e04a15a4b8c885fb4eb778ab82c45838bcbcbdde0c17cea91401000000000000002202034084c4a0493c248783e60d8415cd30b3ba2c3b7a79201e38b953adea2bc44f990cd1dbcc2101000000000000000000",
@ -935,9 +945,9 @@ class TestWalletSending(ElectrumTestCase):
wallet2b.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertEqual((2, 2), tx.signature_count())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
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.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet1a.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
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_segwit())
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(wallet2.txin_type, tx.inputs()[0].script_type)
tx_copy = tx_from_any(tx.serialize())
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.locktime = 2378363
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(
{'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"),
@ -2642,6 +2653,9 @@ class TestWalletSending(ElectrumTestCase):
tx.version = 2
tx.locktime = 2378363
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(
{'022c4338968f87a09b0fefd0aaac36f1b983bab237565d521944c60fdc48275049': ('9559fbd1', "m/9999h/0/0"),
@ -2726,6 +2740,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
tx.version = 1
self.assertFalse(tx.is_complete())
self.assertEqual((0, 1), tx.signature_count())
self.assertFalse(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
partial_tx = tx.serialize_as_bytes().hex()
@ -2739,6 +2754,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None)
self.assertTrue(tx.is_complete())
self.assertEqual((1, 1), tx.signature_count())
self.assertFalse(tx.is_segwit())
self.assertEqual('01000000015608436ec7dc01c95ca1ca91519f2dc62b6613ac3d6304cb56462f6081059e3b020000008a47304402206bed3e02af8a38f6ba2fa3bf5908cb8c643aa62e78e8de6d9af2e19dec55fafc0220039cc1d81d4e5e0292bbc54ea92b8ec4ec016d4828eedc8975a66952cedf13a1014104e79eb77f2f3f989f5e9d090bc0af50afeb0d5bd6ec916f2022c5629ed022e84a87584ef647d69f073ea314a0f0c110ebe24ad64bc1922a10819ea264fc3f35f5fdffffff02a02526000000000016001423a3878d93d5acac68e7245a4433169d3d455087585d7200000000001976a914b6a6bbbc4cf9da58786a8acc58291e218d52130688acff121600',
str(tx))
@ -2871,6 +2887,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
tx.version = 1
self.assertFalse(tx.is_complete())
self.assertEqual((0, 1), tx.signature_count())
self.assertTrue(tx.is_segwit())
self.assertEqual(1, len(tx.inputs()))
@ -2895,6 +2912,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx
tx = wallet_offline.sign_transaction(tx_copy, password=None)
self.assertTrue(tx.is_complete())
self.assertEqual((1, 1), tx.signature_count())
self.assertTrue(tx.is_segwit())
self.assertEqual('ee76c0c6da87f0eb5ab4d1ae05d3942512dcd3c4c42518f9d3619e74400cfc1f', tx.txid())
self.assertEqual('484e350beaa722a744bb3e2aa38de005baa8526d86536d6143e5814355acf775', tx.wtxid())
@ -3034,6 +3052,9 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx
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_segwit())
self.assertEqual('7642816d051aa3b333b6564bb6e44fe3a5885bfe7db9860dfbc9973a5c9a6562', tx.txid())
@ -3112,6 +3133,9 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx
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.assertFalse(tx.is_segwit())
self.assertEqual('e56da664631b8c666c6df38ec80c954c4ac3c4f56f040faf0070e4681e937fc4', tx.txid())
@ -3249,6 +3273,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx - first
tx = wallet_offline1.sign_transaction(tx_copy, password=None)
self.assertFalse(tx.is_complete())
self.assertEqual((1, 2), tx.signature_count())
partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff010073010000000132352f6459e847e65e56aa05cbd7b9ee67be90b40d8f92f6f11e9bfaa11399c50000000000fdffffff02a02526000000000017a9141567b2578f300fa618ef0033611fd67087aff6d187585d72000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887bf391400000100f7010000000001016207d958dc46508d706e4cd7d3bc46c5c2b02160e2578e5fad2efafc3927050301000000171600147a4fc8cdc1c2cf7abbcd88ef6d880e59269797acfdffffff02809698000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e48870d0916020000000017a914703f83ef20f3a52d908475dcad00c5144164d5a2870247304402203b1a5cb48cadeee14fa6c7bbf2bc581ca63104762ec5c37c703df778884cc5b702203233fa53a2a0bfbd85617c636e415da72214e359282cce409019319d031766c50121021112c01a48cc7ea13cba70493c6bffebb3e805df10ff4611d2bf559d26e25c04bf391400220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220451f77cb18224adcb4981492d9be2c3fa7537f94f4b29eb405992dbdd5df04aa022071e6759d40dde810caa01ca7f16bad3cb742d64428c419c8fb4bad6f1c3f718101010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb69242700000000000000000000010069522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002202030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220203e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000",
partial_tx)
@ -3257,6 +3282,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# sign tx - second
tx = wallet_offline2.sign_transaction(tx, password=None)
self.assertTrue(tx.is_complete())
self.assertEqual((2, 2), tx.signature_count())
tx = tx_from_any(tx.serialize())
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,
var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
int_to_hex, push_script, b58_address_to_hash160,
opcodes, add_number_to_script, base_decode, is_segwit_script_type,
opcodes, add_number_to_script, base_decode,
base_encode, construct_witness, construct_script)
from .crypto import sha256d
from .logging import get_logger
from .util import ShortID
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
if TYPE_CHECKING:
from .wallet import Abstract_Wallet
@ -732,33 +733,6 @@ class Transaction:
if vds.can_read_more():
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
def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str:
if txin.witness is not None:
@ -767,46 +741,21 @@ class Transaction:
return ''
assert isinstance(txin, PartialTxInput)
_type = txin.script_type
if not txin.is_segwit():
return construct_witness([])
if estimate_size and txin.witness_sizehint is not None:
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
def guess_txintype_from_address(cls, addr: Optional[str]) -> str:
# It's not possible to tell the script type in general
# just from an address.
# - "1" addresses are of course p2pkh
# - "3" addresses are p2sh but we don't know the redeem script..
# - "bc1" addresses (if they are 42-long) are p2wpkh
# - "bc1" addresses that are 62-long are p2wsh but we don't know the script..
# If we don't know the script, we _guess_ it is pubkeyhash.
# As this method is used e.g. for tx size estimation,
# the estimation will not be precise.
if addr is None:
return 'p2wpkh'
witver, witprog = segwit_addr.decode_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)}')
dummy_desc = None
if estimate_size:
dummy_desc = create_dummy_descriptor_from_address(txin.address)
if desc := (txin.script_descriptor or dummy_desc):
sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs)
if sol.witness is not None:
return sol.witness.hex()
return construct_witness([])
raise UnknownTxinType("cannot construct witness")
@classmethod
def input_script(self, txin: TxInput, *, estimate_size=False) -> str:
@ -821,31 +770,19 @@ class Transaction:
if txin.is_native_segwit():
return ''
_type = txin.script_type
pubkeys, sig_list = self.get_siglist(txin, estimate_size=estimate_size)
if _type in ('address', 'unknown') and estimate_size:
_type = self.guess_txintype_from_address(txin.address)
if _type == 'p2pk':
return construct_script([sig_list[0]])
elif _type == 'p2sh':
# put op_0 before script
redeem_script = multisig_script(pubkeys, txin.num_sig)
return construct_script([0, *sig_list, redeem_script])
elif _type == 'p2pkh':
return construct_script([sig_list[0], pubkeys[0]])
elif _type in ['p2wpkh', 'p2wsh']:
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}')
dummy_desc = None
if estimate_size:
dummy_desc = create_dummy_descriptor_from_address(txin.address)
if desc := (txin.script_descriptor or dummy_desc):
if desc.is_segwit():
if redeem_script := desc.expand().redeem_script:
return construct_script([redeem_script])
return ""
sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs)
if sol.script_sig is not None:
return sol.script_sig.hex()
return ""
raise UnknownTxinType("cannot construct scriptSig")
@classmethod
def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
@ -858,18 +795,12 @@ class Transaction:
raise Exception('OP_CODESEPARATOR black magic is not supported')
return txin.redeem_script.hex()
pubkeys = [pk.hex() for pk in txin.pubkeys]
if txin.script_type in ['p2sh', 'p2wsh', 'p2wsh-p2sh']:
return multisig_script(pubkeys, txin.num_sig)
elif txin.script_type in ['p2pkh', 'p2wpkh', 'p2wpkh-p2sh']:
pubkey = pubkeys[0]
pkh = hash_160(bfh(pubkey)).hex()
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}')
if desc := txin.script_descriptor:
sc = desc.expand()
if script := sc.scriptcode_for_sighash:
return script.hex()
raise Exception(f"don't know scriptcode for descriptor: {desc.to_string()}")
raise UnknownTxinType(f'cannot construct preimage_script')
def _calc_bip143_shared_txdigest_fields(self) -> BIP143SharedTxDigestFields:
inputs = self.inputs()
@ -1255,9 +1186,7 @@ class PartialTxInput(TxInput, PSBTSection):
self.witness_script = None # type: Optional[bytes]
self._unknown = {} # type: Dict[bytes, bytes]
self.script_type = 'unknown'
self.num_sig = 0 # type: int # num req sigs for multisig
self.pubkeys = [] # type: List[bytes] # note: order matters
self._script_descriptor = None # type: Optional[Descriptor]
self._trusted_value_sats = None # type: Optional[int]
self._trusted_address = None # type: Optional[str]
self._is_p2sh_segwit = None # type: Optional[bool] # None means unknown
@ -1290,12 +1219,32 @@ class PartialTxInput(TxInput, PSBTSection):
self._witness_utxo = value
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):
d = super().to_json()
d.update({
'height': self.block_height,
'value_sats': self.value_sats(),
'address': self.address,
'desc': self.script_descriptor.to_string() if self.script_descriptor 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,
'sighash': self.sighash,
@ -1468,22 +1417,6 @@ class PartialTxInput(TxInput, PSBTSection):
return self.witness_utxo.scriptpubkey
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:
if self.script_sig is not None and self.witness is not None:
return True
@ -1491,19 +1424,20 @@ class PartialTxInput(TxInput, PSBTSection):
return True
if self.script_sig is not None and not self.is_segwit():
return True
signatures = list(self.part_sigs.values())
s = len(signatures)
# note: The 'script_type' field is currently only set by the wallet,
# for its own addresses. This means we can only finalize inputs
# that are related to the wallet.
# The 'fix' would be adding extra logic that matches on templates,
# and figures out the script_type from available fields.
if self.script_type in ('p2pk', 'p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
return s >= 1
if self.script_type in ('p2sh', 'p2wsh', 'p2wsh-p2sh'):
return s >= self.num_sig
if desc := self.script_descriptor:
try:
desc.satisfy(allow_dummy=False, sigdata=self.part_sigs)
except MissingSolutionPiece:
pass
else:
return True
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 clear_fields_when_finalized():
# BIP-174: "All other data except the UTXO and unknown fields in the
@ -1594,10 +1528,12 @@ class PartialTxInput(TxInput, PSBTSection):
return False
if self.witness_script:
return True
_type = self.script_type
if _type == 'address' and guess_for_address:
_type = Transaction.guess_txintype_from_address(self.address)
return is_segwit_script_type(_type)
if desc := self.script_descriptor:
return desc.is_segwit()
if guess_for_address:
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:
"""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._unknown = {} # type: Dict[bytes, bytes]
self.script_type = 'unknown'
self.num_sig = 0 # num req sigs for multisig
self.pubkeys = [] # type: List[bytes] # note: order matters
self._script_descriptor = None # type: Optional[Descriptor]
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
@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):
d = super().to_json()
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,
'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))
@ -2013,15 +1967,12 @@ class PartialTransaction(Transaction):
return all([txin.is_complete() for txin in self.inputs()])
def signature_count(self) -> Tuple[int, int]:
s = 0 # "num Sigs we have"
r = 0 # "Required"
nhave, nreq = 0, 0
for txin in self.inputs():
if txin.is_coinbase_input():
continue
signatures = list(txin.part_sigs.values())
s += len(signatures)
r += txin.num_sig
return s, r
a, b = txin.get_satisfaction_progress()
nhave += a
nreq += b
return nhave, nreq
def serialize(self) -> str:
"""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()
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:
if len(xfp) != 4:

173
electrum/wallet.py

@ -84,6 +84,8 @@ from .lnworker import LNWallet
from .paymentrequest import PaymentRequest
from .util import read_json_file, write_json_file, UserFacingException, FileImportFailed
from .util import EventListener, event_listener
from . import descriptor
from .descriptor import Descriptor
if TYPE_CHECKING:
from .network import Network
@ -100,16 +102,15 @@ TX_STATUS = [
]
async def _append_utxos_to_inputs(*, inputs: List[PartialTxInput], network: 'Network',
pubkey: str, txin_type: str, imax: int) -> None:
if txin_type in ('p2pkh', 'p2wpkh', 'p2wpkh-p2sh'):
address = bitcoin.pubkey_to_address(txin_type, pubkey)
scripthash = bitcoin.address_to_scripthash(address)
elif txin_type == 'p2pk':
script = bitcoin.public_key_to_p2pk_script(pubkey)
scripthash = bitcoin.script_to_scripthash(script)
else:
raise Exception(f'unexpected txin_type to sweep: {txin_type}')
async def _append_utxos_to_inputs(
*,
inputs: List[PartialTxInput],
network: 'Network',
script_descriptor: 'descriptor.Descriptor',
imax: int,
) -> None:
script = script_descriptor.expand().output_script.hex()
scripthash = bitcoin.script_to_scripthash(script)
async def append_single_utxo(item):
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.utxo = prev_tx
txin.block_height = int(item['height'])
txin.script_type = txin_type
txin.pubkeys = [bfh(pubkey)]
txin.num_sig = 1
if txin_type == 'p2wpkh-p2sh':
txin.redeem_script = bfh(bitcoin.p2wpkh_nested_script(pubkey))
txin.script_descriptor = script_descriptor
inputs.append(txin)
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):
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(
inputs=inputs,
network=network,
pubkey=pubkey,
txin_type=txin_type,
script_descriptor=desc,
imax=imax)
keypairs[pubkey] = privkey, compressed
@ -684,13 +681,19 @@ class Abstract_Wallet(ABC, Logger, EventListener):
"""
pass
@abstractmethod
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]:
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
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)
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],
address: str, *, only_der_suffix: bool) -> None:
pass # implemented by subclasses
@ -2194,24 +2193,44 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if self.lnworker:
self.lnworker.swap_manager.add_txin_info(txin)
return
# set script_type first, as later checks might rely on it:
txin.script_type = self.get_txin_type(address)
txin.num_sig = self.m if isinstance(self, Multisig_Wallet) else 1
if txin.redeem_script is None:
try:
redeem_script_hex = self.get_redeem_script(address)
txin.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
except UnknownTxinType:
pass
if txin.witness_script is None:
try:
witness_script_hex = self.get_witness_script(address)
txin.witness_script = bfh(witness_script_hex) if witness_script_hex else None
except UnknownTxinType:
pass
self._add_input_sig_info(txin, address, only_der_suffix=only_der_suffix)
txin.script_descriptor = self.get_script_descriptor_for_address(address)
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
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:
if not isinstance(tx, PartialTransaction):
return False
@ -2263,24 +2282,10 @@ class Abstract_Wallet(ABC, Logger, EventListener):
is_mine = self._learn_derivation_path_for_address_from_txinout(txout, address)
if not is_mine:
return
txout.script_type = self.get_txin_type(address)
txout.script_descriptor = self.get_script_descriptor_for_address(address)
txout.is_mine = True
txout.is_change = self.is_change(address)
if isinstance(self, Multisig_Wallet):
txout.num_sig = self.m
self._add_txinout_derivation_info(txout, address, only_der_suffix=only_der_suffix)
if txout.redeem_script is None:
try:
redeem_script_hex = self.get_redeem_script(address)
txout.redeem_script = bfh(redeem_script_hex) if redeem_script_hex else None
except UnknownTxinType:
pass
if txout.witness_script is None:
try:
witness_script_hex = self.get_witness_script(address)
txout.witness_script = bfh(witness_script_hex) if witness_script_hex else None
except UnknownTxinType:
pass
def sign_transaction(self, tx: Transaction, password) -> Optional[PartialTransaction]:
if self.is_watching_only():
@ -2942,20 +2947,6 @@ class Simple_Wallet(Abstract_Wallet):
def get_public_keys(self, address: str) -> Sequence[str]:
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):
# wallet made of imported addresses
@ -3163,20 +3154,6 @@ class Imported_Wallet(Simple_Wallet):
if addr != bitcoin.pubkey_to_address(txin_type, pubkey):
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):
pubkey = pubkeys[0]
# FIXME This is slow.
@ -3304,14 +3281,10 @@ class Deterministic_Wallet(Abstract_Wallet):
return {k.derive_pubkey(*der_suffix): (k, der_suffix)
for k in self.get_keystores()}
def _add_input_sig_info(self, txin, address, *, only_der_suffix):
self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix)
def _add_txinout_derivation_info(self, txinout, address, *, only_der_suffix):
if not self.is_mine(address):
return
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:
ks, der_suffix = pubkey_deriv_info[pubkey]
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]
def load_keystore(self):
self.keystore = load_keystore(self.db, 'keystore')
self.keystore = load_keystore(self.db, 'keystore') # type: KeyStoreWithMPK
try:
xtype = bip32.xpub_type(self.keystore.xpub)
except:
@ -3467,28 +3440,6 @@ class Multisig_Wallet(Deterministic_Wallet):
def pubkeys_to_scriptcode(self, pubkeys: Sequence[str]) -> str:
return transaction.multisig_script(sorted(pubkeys), self.m)
def get_redeem_script(self, address):
txin_type = self.get_txin_type(address)
pubkeys = self.get_public_keys(address)
scriptcode = self.pubkeys_to_scriptcode(pubkeys)
if txin_type == 'p2sh':
return scriptcode
elif txin_type == 'p2wsh-p2sh':
return bitcoin.p2wsh_nested_script(scriptcode)
elif txin_type == 'p2wsh':
return None
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
def get_witness_script(self, address):
txin_type = self.get_txin_type(address)
pubkeys = self.get_public_keys(address)
scriptcode = self.pubkeys_to_scriptcode(pubkeys)
if txin_type == 'p2sh':
return None
elif txin_type in ('p2wsh-p2sh', 'p2wsh'):
return scriptcode
raise UnknownTxinType(f'unexpected txin_type {txin_type}')
def derive_pubkeys(self, c, i):
return [k.derive_pubkey(c, i).hex() for k in self.get_keystores()]

Loading…
Cancel
Save