Browse Source

transaction.py: delegate size estimation to descriptors

master
SomberNight 3 years ago
parent
commit
d062505cfd
No known key found for this signature in database
GPG Key ID: B33B5F232C6271E9
  1. 48
      electrum/descriptor.py
  2. 105
      electrum/transaction.py

48
electrum/descriptor.py

@ -28,7 +28,10 @@
from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo
from . import bitcoin from . import bitcoin
from .bitcoin import construct_script, opcodes, construct_witness from .bitcoin import construct_script, opcodes, construct_witness
from . import constants
from .crypto import hash_160, sha256 from .crypto import hash_160, sha256
from . import ecc
from . import segwit_addr
from .util import bfh from .util import bfh
from binascii import unhexlify from binascii import unhexlify
@ -45,6 +48,14 @@ from typing import (
MAX_TAPROOT_NODES = 128 MAX_TAPROOT_NODES = 128
# 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.
DUMMY_DER_SIG = 72 * b"\x00"
class ExpandedScripts: class ExpandedScripts:
@ -403,7 +414,7 @@ class PKDescriptor(Descriptor):
pubkey = self.pubkeys[0].get_pubkey_bytes() pubkey = self.pubkeys[0].get_pubkey_bytes()
sig = sigdata.get(pubkey) sig = sigdata.get(pubkey)
if sig is None and allow_dummy: if sig is None and allow_dummy:
sig = 72 * b"\x00" sig = DUMMY_DER_SIG
if sig is None: if sig is None:
raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") raise MissingSolutionPiece(f"no sig for {pubkey.hex()}")
return ScriptSolutionInner( return ScriptSolutionInner(
@ -437,7 +448,7 @@ class PKHDescriptor(Descriptor):
pubkey = self.pubkeys[0].get_pubkey_bytes() pubkey = self.pubkeys[0].get_pubkey_bytes()
sig = sigdata.get(pubkey) sig = sigdata.get(pubkey)
if sig is None and allow_dummy: if sig is None and allow_dummy:
sig = 72 * b"\x00" sig = DUMMY_DER_SIG
if sig is None: if sig is None:
raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") raise MissingSolutionPiece(f"no sig for {pubkey.hex()}")
return ScriptSolutionInner( return ScriptSolutionInner(
@ -474,7 +485,7 @@ class WPKHDescriptor(Descriptor):
pubkey = self.pubkeys[0].get_pubkey_bytes() pubkey = self.pubkeys[0].get_pubkey_bytes()
sig = sigdata.get(pubkey) sig = sigdata.get(pubkey)
if sig is None and allow_dummy: if sig is None and allow_dummy:
sig = 72 * b"\x00" sig = DUMMY_DER_SIG
if sig is None: if sig is None:
raise MissingSolutionPiece(f"no sig for {pubkey.hex()}") raise MissingSolutionPiece(f"no sig for {pubkey.hex()}")
return ScriptSolutionInner( return ScriptSolutionInner(
@ -532,7 +543,7 @@ class MultisigDescriptor(Descriptor):
if len(signatures) >= self.thresh: if len(signatures) >= self.thresh:
break break
if allow_dummy: if allow_dummy:
dummy_sig = 72 * b"\x00" dummy_sig = DUMMY_DER_SIG
signatures += (self.thresh - len(signatures)) * [dummy_sig] signatures += (self.thresh - len(signatures)) * [dummy_sig]
if len(signatures) < self.thresh: if len(signatures) < self.thresh:
raise MissingSolutionPiece(f"not enough sigs") raise MissingSolutionPiece(f"not enough sigs")
@ -900,3 +911,32 @@ def get_singlesig_descriptor_from_legacy_leaf(*, pubkey: str, script_type: str)
return SHDescriptor(subdescriptor=wpkh) return SHDescriptor(subdescriptor=wpkh)
else: else:
raise NotImplementedError(f"unexpected {script_type=}") raise NotImplementedError(f"unexpected {script_type=}")
def create_dummy_descriptor_from_address(addr: Optional[str]) -> 'Descriptor':
# 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.
def guess_script_type(addr: Optional[str]) -> str:
if addr is None:
return 'p2wpkh' # the default guess
witver, witprog = segwit_addr.decode_segwit_address(constants.net.SEGWIT_HRP, addr)
if witprog is not None:
return 'p2wpkh'
addrtype, hash_160_ = bitcoin.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)}')
script_type = guess_script_type(addr)
# guess pubkey-len to be 33-bytes:
pubkey = ecc.GENERATOR.get_public_key_bytes(compressed=True).hex()
desc = get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type=script_type)
return desc

105
electrum/transaction.py

@ -47,12 +47,12 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr, hash160_to_p2sh, hash160_to_p2pkh, hash_to_segwit_addr,
var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN, var_int, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC, COIN,
int_to_hex, push_script, b58_address_to_hash160, int_to_hex, push_script, b58_address_to_hash160,
opcodes, add_number_to_script, base_decode, is_segwit_script_type, opcodes, add_number_to_script, base_decode,
base_encode, construct_witness, construct_script) base_encode, construct_witness, construct_script)
from .crypto import sha256d from .crypto import sha256d
from .logging import get_logger from .logging import get_logger
from .util import ShortID from .util import ShortID
from .descriptor import Descriptor, MissingSolutionPiece from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
if TYPE_CHECKING: if TYPE_CHECKING:
from .wallet import Abstract_Wallet from .wallet import Abstract_Wallet
@ -733,33 +733,6 @@ class Transaction:
if vds.can_read_more(): if vds.can_read_more():
raise SerializationError('extra junk at the end') raise SerializationError('extra junk at the end')
@classmethod
def get_siglist(self, txin: 'PartialTxInput', *, estimate_size=False):
if txin.is_coinbase_input():
return [], []
if estimate_size:
try:
pubkey_size = len(txin.pubkeys[0])
except IndexError:
pubkey_size = 33 # guess it is compressed
num_pubkeys = max(1, len(txin.pubkeys))
pk_list = ["00" * pubkey_size] * num_pubkeys
num_sig = max(1, txin.num_sig)
# we guess that signatures will be 72 bytes long
# note: DER-encoded ECDSA signatures are 71 or 72 bytes in practice
# See https://bitcoin.stackexchange.com/questions/77191/what-is-the-maximum-size-of-a-der-encoded-ecdsa-signature
# We assume low S (as that is a bitcoin standardness rule).
# We do not assume low R (even though the sigs we create conform), as external sigs,
# e.g. from a hw signer cannot be expected to have a low R.
sig_list = ["00" * 72] * num_sig
else:
pk_list = [pubkey.hex() for pubkey in txin.pubkeys]
sig_list = [txin.part_sigs.get(pubkey, b'').hex() for pubkey in txin.pubkeys]
if txin.is_complete():
sig_list = [sig for sig in sig_list if sig]
return pk_list, sig_list
@classmethod @classmethod
def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str: def serialize_witness(cls, txin: TxInput, *, estimate_size=False) -> str:
if txin.witness is not None: if txin.witness is not None:
@ -775,47 +748,15 @@ class Transaction:
if estimate_size and txin.witness_sizehint is not None: if estimate_size and txin.witness_sizehint is not None:
return '00' * txin.witness_sizehint return '00' * txin.witness_sizehint
if desc := txin.script_descriptor: 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) sol = desc.satisfy(allow_dummy=estimate_size, sigdata=txin.part_sigs)
if sol.witness is not None: if sol.witness is not None:
return sol.witness.hex() return sol.witness.hex()
return construct_witness([]) return construct_witness([])
raise UnknownTxinType("cannot construct witness")
assert estimate_size # TODO xxxxx
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)}')
@classmethod @classmethod
def input_script(self, txin: TxInput, *, estimate_size=False) -> str: def input_script(self, txin: TxInput, *, estimate_size=False) -> str:
@ -830,7 +771,10 @@ class Transaction:
if txin.is_native_segwit(): if txin.is_native_segwit():
return '' return ''
if desc := txin.script_descriptor: 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 desc.is_segwit():
if redeem_script := desc.expand().redeem_script: if redeem_script := desc.expand().redeem_script:
return construct_script([redeem_script]) return construct_script([redeem_script])
@ -839,24 +783,7 @@ class Transaction:
if sol.script_sig is not None: if sol.script_sig is not None:
return sol.script_sig.hex() return sol.script_sig.hex()
return "" return ""
raise UnknownTxinType("cannot construct scriptSig")
assert estimate_size # TODO xxxxx
_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 == 'p2wpkh-p2sh':
redeem_script = bitcoin.p2wpkh_nested_script(pubkeys[0])
return construct_script([redeem_script])
raise UnknownTxinType(f'cannot construct scriptSig for txin_type: {_type}')
@classmethod @classmethod
def get_preimage_script(cls, txin: 'PartialTxInput') -> str: def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
@ -1599,10 +1526,10 @@ class PartialTxInput(TxInput, PSBTSection):
return True return True
if desc := self.script_descriptor: if desc := self.script_descriptor:
return desc.is_segwit() return desc.is_segwit()
_type = self.script_type if guess_for_address:
if _type == 'address' and guess_for_address: dummy_desc = create_dummy_descriptor_from_address(self.address)
_type = Transaction.guess_txintype_from_address(self.address) return dummy_desc.is_segwit()
return is_segwit_script_type(_type) return False # can be false-negative
def already_has_some_signatures(self) -> bool: def already_has_some_signatures(self) -> bool:
"""Returns whether progress has been made towards completing this input.""" """Returns whether progress has been made towards completing this input."""

Loading…
Cancel
Save