transaction.py: delegate size estimation to descriptors
This commit is contained in:
@@ -28,7 +28,10 @@
|
||||
from .bip32 import convert_bip32_path_to_list_of_uint32, BIP32Node, KeyOriginInfo
|
||||
from . import bitcoin
|
||||
from .bitcoin import construct_script, opcodes, construct_witness
|
||||
from . import constants
|
||||
from .crypto import hash_160, sha256
|
||||
from . import ecc
|
||||
from . import segwit_addr
|
||||
from .util import bfh
|
||||
|
||||
from binascii import unhexlify
|
||||
@@ -45,6 +48,14 @@ from typing import (
|
||||
|
||||
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:
|
||||
|
||||
@@ -403,7 +414,7 @@ class PKDescriptor(Descriptor):
|
||||
pubkey = self.pubkeys[0].get_pubkey_bytes()
|
||||
sig = sigdata.get(pubkey)
|
||||
if sig is None and allow_dummy:
|
||||
sig = 72 * b"\x00"
|
||||
sig = DUMMY_DER_SIG
|
||||
if sig is None:
|
||||
raise MissingSolutionPiece(f"no sig for {pubkey.hex()}")
|
||||
return ScriptSolutionInner(
|
||||
@@ -437,7 +448,7 @@ class PKHDescriptor(Descriptor):
|
||||
pubkey = self.pubkeys[0].get_pubkey_bytes()
|
||||
sig = sigdata.get(pubkey)
|
||||
if sig is None and allow_dummy:
|
||||
sig = 72 * b"\x00"
|
||||
sig = DUMMY_DER_SIG
|
||||
if sig is None:
|
||||
raise MissingSolutionPiece(f"no sig for {pubkey.hex()}")
|
||||
return ScriptSolutionInner(
|
||||
@@ -474,7 +485,7 @@ class WPKHDescriptor(Descriptor):
|
||||
pubkey = self.pubkeys[0].get_pubkey_bytes()
|
||||
sig = sigdata.get(pubkey)
|
||||
if sig is None and allow_dummy:
|
||||
sig = 72 * b"\x00"
|
||||
sig = DUMMY_DER_SIG
|
||||
if sig is None:
|
||||
raise MissingSolutionPiece(f"no sig for {pubkey.hex()}")
|
||||
return ScriptSolutionInner(
|
||||
@@ -532,7 +543,7 @@ class MultisigDescriptor(Descriptor):
|
||||
if len(signatures) >= self.thresh:
|
||||
break
|
||||
if allow_dummy:
|
||||
dummy_sig = 72 * b"\x00"
|
||||
dummy_sig = DUMMY_DER_SIG
|
||||
signatures += (self.thresh - len(signatures)) * [dummy_sig]
|
||||
if len(signatures) < self.thresh:
|
||||
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)
|
||||
else:
|
||||
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
|
||||
|
||||
@@ -47,12 +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
|
||||
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .wallet import Abstract_Wallet
|
||||
@@ -733,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:
|
||||
@@ -775,47 +748,15 @@ class Transaction:
|
||||
if estimate_size and txin.witness_sizehint is not None:
|
||||
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)
|
||||
if sol.witness is not None:
|
||||
return sol.witness.hex()
|
||||
return 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)}')
|
||||
raise UnknownTxinType("cannot construct witness")
|
||||
|
||||
@classmethod
|
||||
def input_script(self, txin: TxInput, *, estimate_size=False) -> str:
|
||||
@@ -830,7 +771,10 @@ class Transaction:
|
||||
if txin.is_native_segwit():
|
||||
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 redeem_script := desc.expand().redeem_script:
|
||||
return construct_script([redeem_script])
|
||||
@@ -839,24 +783,7 @@ class Transaction:
|
||||
if sol.script_sig is not None:
|
||||
return sol.script_sig.hex()
|
||||
return ""
|
||||
|
||||
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}')
|
||||
raise UnknownTxinType("cannot construct scriptSig")
|
||||
|
||||
@classmethod
|
||||
def get_preimage_script(cls, txin: 'PartialTxInput') -> str:
|
||||
@@ -1599,10 +1526,10 @@ class PartialTxInput(TxInput, PSBTSection):
|
||||
return True
|
||||
if desc := self.script_descriptor:
|
||||
return desc.is_segwit()
|
||||
_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 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."""
|
||||
|
||||
Reference in New Issue
Block a user