Skip to content

Commit

Permalink
support p2tr outputs in size estimation
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamISZ authored and kristapsk committed Jan 5, 2024
1 parent f43d00c commit 6958215
Show file tree
Hide file tree
Showing 5 changed files with 35 additions and 10 deletions.
14 changes: 12 additions & 2 deletions jmbitcoin/jmbitcoin/secp256k1_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion jmbitcoin/test/test_tx_signing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions jmclient/jmclient/cryptoengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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)))

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
4 changes: 3 additions & 1 deletion jmclient/jmclient/taker_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions jmclient/jmclient/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 6958215

Please sign in to comment.