From 6958215f986c01301019eac4fcf2b19f41c2406d Mon Sep 17 00:00:00 2001 From: Adam Gibson Date: Sat, 14 Jan 2023 16:41:51 +0000 Subject: [PATCH] support p2tr outputs in size estimation --- jmbitcoin/jmbitcoin/secp256k1_transaction.py | 14 ++++++++++++-- jmbitcoin/test/test_tx_signing.py | 3 ++- jmclient/jmclient/cryptoengine.py | 14 ++++++++++++-- jmclient/jmclient/taker_utils.py | 4 +++- jmclient/jmclient/wallet.py | 10 ++++++---- 5 files changed, 35 insertions(+), 10 deletions(-) diff --git a/jmbitcoin/jmbitcoin/secp256k1_transaction.py b/jmbitcoin/jmbitcoin/secp256k1_transaction.py index 8dfc3c52a..d58deb773 100644 --- a/jmbitcoin/jmbitcoin/secp256k1_transaction.py +++ b/jmbitcoin/jmbitcoin/secp256k1_transaction.py @@ -88,6 +88,7 @@ def there_is_one_segwit_input(input_types: List[str]) -> bool: # since each may have a different size of witness; in # that case, the internal list in this list comprehension # will need updating. + # note that there is no support yet for paying *from* p2tr. return any(y in ["p2sh-p2wpkh", "p2wpkh", "p2wsh"] for y in input_types) def estimate_tx_size(ins: List[str], outs: List[str]) -> Union[int, Tuple[int]]: @@ -123,21 +124,30 @@ def estimate_tx_size(ins: List[str], outs: List[str]) -> Union[int, Tuple[int]]: # script's redeemscript field in the witness, but for arbitrary scripts, # the witness portion could be any other size. # Hence, we may need to modify this later. + # + # Note that there is no support yet for spending *from* p2tr: + # we should fix this soon, since it is desirable to be able to support + # coinjoins with counterparties sending taproot, but note, JM coinjoins + # do not allow non-standard (usually v0 segwit) inputs, anyway. inmults = {"p2wsh": {"w": 1 + 72 + 43, "nw": 41}, "p2wpkh": {"w": 108, "nw": 41}, "p2sh-p2wpkh": {"w": 108, "nw": 64}, "p2pkh": {"w": 0, "nw": 148}} # Notes: in outputs, there is only 1 'scripthash' - # type for either segwit/nonsegwit. + # type for either segwit/nonsegwit (hence "p2sh-p2wpkh" + # is a bit misleading, but is kept to the same as inputs, + # for simplicity. See notes on inputs above). # p2wsh has structure 8 bytes output, then: # x22,x00,x20,(32 byte hash), so 32 + 3 + 8 # note also there is no need to distinguish witness # here, outputs are always entirely nonwitness. + # p2tr is also 32 byte hash with x01 instead of x00 version. outmults = {"p2wsh": 43, "p2wpkh": 31, "p2sh-p2wpkh": 32, - "p2pkh": 34} + "p2pkh": 34, + "p2tr": 43} # nVersion, nLockTime, nins, nouts: nwsize = 4 + 4 + 2 diff --git a/jmbitcoin/test/test_tx_signing.py b/jmbitcoin/test/test_tx_signing.py index bfdb94697..ebe73eff5 100644 --- a/jmbitcoin/test/test_tx_signing.py +++ b/jmbitcoin/test/test_tx_signing.py @@ -36,13 +36,14 @@ (["p2pkh"], ["p2pkh", "p2sh-p2wpkh"], 224), (["p2sh-p2wpkh"], ["p2sh-p2wpkh"], 134), (["p2wpkh"], ["p2wpkh"], 110), + (["p2wpkh"], ["p2wpkh", "p2tr"], 153), ]) def test_tx_size_estimate(inaddrtypes, outaddrtypes, size_expected): # non-sw only inputs result in a single integer return, # segwit inputs return (witness size, non-witness size) x = btc.estimate_tx_size(inaddrtypes, outaddrtypes) if btc.there_is_one_segwit_input(inaddrtypes): - s = ceil((x[0] + x[1] * 4)/4.0) + s = ceil((x[0] + x[1] * 4) / 4.0) else: s = x assert s == size_expected diff --git a/jmclient/jmclient/cryptoengine.py b/jmclient/jmclient/cryptoengine.py index c3e872351..ea4a75a60 100644 --- a/jmclient/jmclient/cryptoengine.py +++ b/jmclient/jmclient/cryptoengine.py @@ -16,7 +16,8 @@ # make existing wallets unsable. TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \ TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \ - TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH = range(11) + TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, \ + TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, TYPE_P2TR = range(15) NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3) NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET, 'signet': NET_SIGNET} @@ -52,6 +53,8 @@ def detect_script_type(script_str): return TYPE_P2WPKH elif script.is_witness_v0_scripthash(): return TYPE_P2WSH + elif script.is_witness_v1_taproot(): + return TYPE_P2TR raise EngineError("Unknown script type for script '{}'" .format(bintohex(script_str))) @@ -224,6 +227,12 @@ def pubkey_has_script(cls, pubkey, script): stype = detect_script_type(script) assert stype in ENGINES engine = ENGINES[stype] + # TODO though taproot is currently a returnable + # type from detect_script_type, there is not yet + # a corresponding ENGINE, thus a None return is possible. + # Callers recognize this as EngineError. + if engine is None: + raise EngineError pscript = engine.pubkey_to_script(pubkey) return script == pscript @@ -486,5 +495,6 @@ def sign_transaction(cls, tx, index, privkey, amount, TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH, TYPE_WATCHONLY_P2SH_P2WPKH: BTC_Watchonly_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH: BTC_Watchonly_P2WPKH, - TYPE_SEGWIT_WALLET_FIDELITY_BONDS: BTC_P2WPKH + TYPE_SEGWIT_WALLET_FIDELITY_BONDS: BTC_P2WPKH, + TYPE_P2TR: None # TODO } diff --git a/jmclient/jmclient/taker_utils.py b/jmclient/jmclient/taker_utils.py index a500fd289..7ed235e6c 100644 --- a/jmclient/jmclient/taker_utils.py +++ b/jmclient/jmclient/taker_utils.py @@ -121,12 +121,14 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, # we don't recognize the destination script type, # so set it as the same as the change (which will usually # be the same as the spending wallet, but see above for custom) + # Notice that this is handled differently to the sweep case above, + # because we must use a list - there is more than one output outtype = change_type outtypes = [change_type, outtype] # not doing a sweep; we will have change. # 8 inputs to be conservative; note we cannot account for the possibility # of non-standard input types at this point. - initial_fee_est = estimate_tx_fee(8,2, txtype=txtype, outtype=outtypes) + initial_fee_est = estimate_tx_fee(8, 2, txtype=txtype, outtype=outtypes) utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est, includeaddr=True) script_types = get_utxo_scripts(wallet_service.wallet, utxos) diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 3ca737f01..e56280db4 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -27,9 +27,9 @@ select, NotEnoughFundsException from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WSH,\ TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\ - TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH,\ - TYPE_WATCHONLY_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH,\ - ENGINES, detect_script_type, EngineError + TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, \ + TYPE_WATCHONLY_P2SH_P2WPKH, TYPE_WATCHONLY_P2WPKH, TYPE_P2TR, ENGINES, \ + detect_script_type, EngineError from .support import get_random_bytes from . import mn_encode, mn_decode import jmbitcoin as btc @@ -93,7 +93,7 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh', outtype=None, extra_bytes=0): # See docstring for explanation: if isinstance(txtype, str): - ins = [txtype]* ins + ins = [txtype] * ins else: assert isinstance(txtype, list) ins = txtype @@ -506,6 +506,8 @@ def get_outtype(self, addr): return 'p2sh-p2wpkh' elif script_type == TYPE_P2WSH: return 'p2wsh' + elif script_type == TYPE_P2TR: + return 'p2tr' # should be unreachable; all possible returns # from detect_script_type are covered. assert False