Browse Source

Merge #1421: Tx size estimation allows varied in, out types

357b611 support p2tr outputs in size estimation (Adam Gibson)
c1d7f02 remove now unused OUTPUT_EXTRA_BYTES (Adam Gibson)
e281c14 account for unrecognized destination type (Adam Gibson)
6250d24 add typing hints (Adam Gibson)
db71d30 address review of @PulpCattel (Adam Gibson)
4921d01 add test cases, move marker, flag to witness (Adam Gibson)
7bf6696 Add tests of size estimator in jmbitcoin (Adam Gibson)
bffad33 Tx size estimation allows varied in, out types (Adam Gibson)
master
Adam Gibson 3 years ago
parent
commit
da4eed9638
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 177
      jmbitcoin/jmbitcoin/secp256k1_transaction.py
  2. 44
      jmbitcoin/test/test_tx_signing.py
  3. 13
      jmclient/jmclient/cryptoengine.py
  4. 67
      jmclient/jmclient/taker_utils.py
  5. 54
      jmclient/jmclient/wallet.py
  6. 12
      jmclient/test/test_taker.py

177
jmbitcoin/jmbitcoin/secp256k1_transaction.py

@ -1,6 +1,7 @@
# note, only used for non-cryptographic randomness: # note, only used for non-cryptographic randomness:
import random import random
import json import json
from typing import List, Union, Tuple
# needed for single sha256 evaluation, which is used # needed for single sha256 evaluation, which is used
# in bitcoin (p2wsh) but not exposed in python-bitcointx: # in bitcoin (p2wsh) but not exposed in python-bitcointx:
import hashlib import hashlib
@ -18,31 +19,6 @@ from bitcointx.core.scripteval import (VerifyScript, SCRIPT_VERIFY_WITNESS,
SCRIPT_VERIFY_STRICTENC, SCRIPT_VERIFY_STRICTENC,
SIGVERSION_WITNESS_V0) SIGVERSION_WITNESS_V0)
# for each transaction type, different output script pubkeys may result in
# a difference in the number of bytes accounted for while estimating the
# transaction size, this variable stores the difference and is factored in
# when calculating the correct transaction size. For example, for a p2pkh
# transaction, if one of the outputs is a p2wsh pubkey, then the transaction
# would need 9 extra bytes to account for the difference in script pubkey
# sizes
OUTPUT_EXTRA_BYTES = {
'p2pkh': {
'p2wpkh': -3,
'p2sh-p2wpkh': -2,
'p2wsh': 9
},
'p2wpkh': {
'p2pkh': 3,
'p2sh-p2wpkh': 1,
'p2wsh': 12
},
'p2sh-p2wpkh': {
'p2pkh': 2,
'p2wpkh': -1,
'p2wsh': 11
}
}
def human_readable_transaction(tx, jsonified=True): def human_readable_transaction(tx, jsonified=True):
""" Given a CTransaction object, output a human """ Given a CTransaction object, output a human
readable json-formatted string (suitable for terminal readable json-formatted string (suitable for terminal
@ -106,70 +82,97 @@ def human_readable_output(txoutput):
pass # non standard script pass # non standard script
return outdict return outdict
def estimate_tx_size(ins, outs, txtype='p2pkh', outtype=None): def there_is_one_segwit_input(input_types: List[str]) -> bool:
# note that we need separate input types for
# any distinct types of scripthash inputs supported,
# 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]]:
'''Estimate transaction size. '''Estimate transaction size.
The txtype field as detailed below is used to distinguish Both arguments `ins` and `outs` must be lists of script types,
the type, but there is at least one source of meaningful roughness: and they must be present in the keys of the dicts `inmults`,
we assume that the scriptPubKey type of all the outputs are the same as `outmults` defined here.
the input, unless `outtype` is specified, in which case *one* of Note that variation in ECDSA signature sizes means
the outputs is assumed to be that other type, with all of the other we will sometimes see small inaccuracies in this estimate, but
outputs being of the same type as before. that this is ameliorated by the existence of the witness discount,
This, combined with a few bytes variation in signature sizes means in actually estimating fees.
we will sometimes see small inaccuracies in this estimate. The value '72' is used for the most-likely size of these ECDSA
signatures, due to 30[1 byte] + len(rest)[1 byte] + type:02 [1 byte] + len(r)[1] + r[32 or 33] + type:02[1] + len(s)[1] + s[32] + sighash_all [1]
Assuming p2pkh: ... though as can be seen, 71 is also likely:
out: 8+1+3+20+2=34, in: 32+4+1+1+~72+1+33+4=148, r length 33 occurs when the value is 'negative' (>N/2) and a byte x80 is prepended,
ver: 4, locktime:4, +2 (len in,out) but shorter values for r are possible if rare.
total = 34*len_out + 148*len_in + 10 (sig sizes vary slightly) Returns:
Either a single integer, if the transaction will be non-segwit,
Assuming p2sh M of N multisig: or a tuple (int, int) for witness and non-witness bytes respectively).
"ins" must contain M, N so ins= (numins, M, N) (crude assuming all same)
73*M + 34*N + 45 per input, so total ins ~ len_ins * (45+73M+34N)
so total ~ 32*len_out + (45+73M+34N)*len_in + 10
Assuming p2sh-p2wpkh:
witness are roughly 1+1+~72+1+33 for each input
(txid, vin, 4+20 for witness program encoded as scriptsig, 4 for sequence)
non-witness input fields are roughly 32+4+4+20+4=64, so total becomes
n_in * 64 + 4(ver) + 2(marker, flag) + 2(n_in, n_out) + 4(locktime) + n_out*32
Assuming p2wpkh native:
witness as previous case
non-witness loses the 24 witnessprogram, replaced with 1 zero,
in the scriptSig, so becomes:
4 + 1 + 1 + (n_in) + (vin) + (n_out) + (vout) + (witness) + (locktime)
non-witness: 4(ver) +2 (marker, flag) + n_in*41 + 4(locktime) +2 (len in, out) + n_out*31
witness: 1 + 1 + 72 + 1 + 33
''' '''
if txtype == 'p2pkh':
return 4 + 4 + 2 + ins*148 + 34*outs + ( # All non-witness input sizes include: txid, index, sequence,
OUTPUT_EXTRA_BYTES[txtype][outtype] # which is 32, 4 and 4; the remaining is scriptSig which is 1
if outtype and outtype in OUTPUT_EXTRA_BYTES[txtype] else 0) # at minimum, for native segwit (the byte x00). Hence 41 is the minimum.
elif txtype == 'p2sh-p2wpkh': # The witness field for p2wpkh consists of sig, pub so 72 + 33 + 1 byte
#return the estimate for the witness and non-witness # for the number of witness elements and 2 bytes for the size of each element,
#portions of the transaction, assuming that all the inputs # hence 108.
#are of segwit type p2sh-p2wpkh # For p2pkh, 148 comes from 32+4+1+1+~72+1+33+4
# Note as of Jan19: this misses 2 bytes (trivial) for len in, out # For p2sh-p2wpkh there is an additional 23 bytes of witness for the redeemscript.
# and also overestimates output size by 2 bytes. #
witness_estimate = ins*108 # Note that p2wsh here is specific to the script
non_witness_estimate = 4 + 4 + 4 + outs*32 + ins*64 + ( # we use for fidelity bonds; 43 is the bytes required for that
OUTPUT_EXTRA_BYTES[txtype][outtype] # script's redeemscript field in the witness, but for arbitrary scripts,
if outtype and outtype in OUTPUT_EXTRA_BYTES[txtype] else 0) # the witness portion could be any other size.
return (witness_estimate, non_witness_estimate) # Hence, we may need to modify this later.
elif txtype == 'p2wpkh': #
witness_estimate = ins*108 # Note that there is no support yet for spending *from* p2tr:
non_witness_estimate = 4 + 4 + 4 + outs*31 + ins*41 + ( # we should fix this soon, since it is desirable to be able to support
OUTPUT_EXTRA_BYTES[txtype][outtype] # coinjoins with counterparties sending taproot, but note, JM coinjoins
if outtype and outtype in OUTPUT_EXTRA_BYTES[txtype] else 0) # do not allow non-standard (usually v0 segwit) inputs, anyway.
return (witness_estimate, non_witness_estimate) inmults = {"p2wsh": {"w": 1 + 72 + 43, "nw": 41},
elif txtype == 'p2shMofN': "p2wpkh": {"w": 108, "nw": 41},
ins, M, N = ins "p2sh-p2wpkh": {"w": 108, "nw": 64},
return 4 + 4 + 2 + (45 + 73*M + 34*N)*ins + outs*32 + ( "p2pkh": {"w": 0, "nw": 148}}
OUTPUT_EXTRA_BYTES['p2sh-p2wpkh'][outtype]
if outtype and outtype in OUTPUT_EXTRA_BYTES['p2sh-p2wpkh'] else 0) # Notes: in outputs, there is only 1 'scripthash'
else: # type for either segwit/nonsegwit (hence "p2sh-p2wpkh"
raise NotImplementedError("Transaction size estimation not" + # is a bit misleading, but is kept to the same as inputs,
"yet implemented for type: " + txtype) # 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,
"p2tr": 43}
# nVersion, nLockTime, nins, nouts:
nwsize = 4 + 4 + 2
wsize = 0
tx_is_segwit = there_is_one_segwit_input(ins)
if tx_is_segwit:
# flag and marker bytes are included in witness
wsize += 2
for i in ins:
if i not in inmults:
raise NotImplementedError(
f"Script type not supported for transaction size estimation: {i}")
inmult = inmults[i]
nwsize += inmult["nw"]
wsize += inmult["w"]
for o in outs:
if o not in outmults:
raise NotImplementedError(
f"Script type not supported for transaction size estimation: {o}")
nwsize += outmults[o]
if not tx_is_segwit:
return nwsize
return (wsize, nwsize)
def pubkey_to_p2pkh_script(pub, require_compressed=False): def pubkey_to_p2pkh_script(pub, require_compressed=False):
""" """

44
jmbitcoin/test/test_tx_signing.py

@ -3,6 +3,50 @@ import pytest
import hashlib import hashlib
from jmbase import bintohex from jmbase import bintohex
import jmbitcoin as btc import jmbitcoin as btc
from math import ceil
# Case of spending into a FB, one p2wpkh input, one FB output, one p2wpkh output:
#
# "020000000001019b3a5c6ec9712bd6b1aa1e07ed12a677eb21215430fb84a689664fb0d4fa175a0000000000feffffff0290099c0a000000001600144d32ca4673822334531a2941bb85bff075c384d180b14f0100000000220020dae515a98f31542dd6b21bb0b0e31fccc4ebcdc9ba3e225798bc981ccbb8a21d024830450221009bc5fb8d077b32304c02a886926a68110bc7c5195a92b55977bd4affd23ab2d50220774e88828cf80cba4bb2e277a69a7c693c3566b5b72a0c0cf25cba89df0646d30121036d1baed4008f0d7f03ac41bbdbe46e02eeef6f040f7bdbc41adac30c2cf8831886020000"
#
# case of spending from a FB, single input, to 2 p2wpkh outputs
# "020000000001013499e10a8035a3cb928f5a894b2a3feed7d46ab579f0a2211a17ed1df0e2d9660100000000feffffff02405dc6000000000016001486551870825ef64d7fda89cc2a6fda497ab71a02954d8900000000001600147c24b9a3ddf5eb26142fb3f05a3746a0a582f81a0247304402206ca2049154939b083a5eb22713d3cb78f6673f43828fa4a7ef0f03275584da6c0220770fe7d50ba5e0a5f8039f28c5deaaf1e8b2030008bf51cc8721cf088eab5586012a0480c22e61b175210376e5964ee2c328e85b88a50f02953b1cddb1490825140331a3948023cc19946bac81c22e61"
#
# case of spending from an FB plus one more p2wpkh input, spending to a p2sh output and a p2wpkh output
# "02000000000102117efb788cee9644d5493fc9d1a120598f4f5200bb6909c0ebdac66cf88da80a0100000000feffffffb0f3ce583987d08080684e2c75d2d6871cb2d30f610327671440d7121c14b7ab0000000000feffffff0240a5ae020000000017a914c579342c2c4c9220205e2cdc285617040c924a0a8797027a00000000001600146fad63e6420ec3eb423a105b05c6df6cc8dab92902473044022040f1db3289d8c6e0dd94c0f55254f10512486094e36fd92f4423abc95320418902206718670b3f332d84cf8294ad389155594ebe125988f2f535c58ff3b077471ce9012102f9306fdc84a366f21fed44fbdd9149a046829f26fb52e7a316b19b536c18d2df0247304402207d0fde11ce32f48107ac246a01efad1c22b9466cd9ff4d5683030790bcb34ce5022020014fcf1b1b0606db5ef363568f10476524f7102644691a8a435faa17cbbe88012a04804f5661b17521037a9502304b2810706adef15b884ac7ca0c48c2e5d03cf93934487b44feb7c276ac814f5661"
#
# case of spending from one FB input to one FB output
#
# "0200000000010150c8e3786d357cbe61d8e27de7439e1b32d75d0a0ad596c5ff2863134cbd3ead0100000000feffffff01db84f701000000002200208b8ed0bc565e419dd956c3841b7bb25f7c197e2699002bac58a68f47206e1f340247304402202a639209aa9a2883ad75210edce2165260167435f56cede83e8c74095944f355022050fde591f1fefb615a072a797ace3c332c678e0f9161e58d79efa1705f9ab17c012a04002e7f61b1752103d4d747d0dca80c129c017ec1cdc658945013e04ff3d6946f15ccc9df52c323f0ac012e7f61"
#
# Virtual sizes can be calculated from bitcointx.core.CTransaction.deserialize(unhexlify(txhex)).get_virtual_size()
#
# More cases copied from:
# https://github.com/kristapsk/bitcoin-scripts/blob/0b847bec016638e60313ecec2b81f2e8accd311b/tests/tx-vsize.bats
@pytest.mark.parametrize(
"inaddrtypes, outaddrtypes, size_expected",
[(["p2wpkh"], ["p2wsh", "p2wpkh"], 153),
(["p2wsh"], ["p2wpkh", "p2wpkh"], 143),
(["p2wsh", "p2wpkh"], ["p2sh-p2wpkh", "p2wpkh"], 212),
(["p2wsh"], ["p2wsh"], 124),
(["p2pkh"], ["p2pkh"], 192),
(["p2pkh"], ["p2pkh", "p2pkh"], 226),
(["p2pkh"], ["p2sh-p2wpkh", "p2sh-p2wpkh"], 222),
(["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)
else:
s = x
assert s == size_expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
"addrtype", "addrtype",

13
jmclient/jmclient/cryptoengine.py

@ -16,7 +16,7 @@ from .configure import get_network, jm_single
# make existing wallets unsable. # make existing wallets unsable.
TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \ TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH, TYPE_P2SH_M_N, TYPE_TIMELOCK_P2WSH, \
TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \ TYPE_SEGWIT_WALLET_FIDELITY_BONDS, TYPE_WATCHONLY_FIDELITY_BONDS, \
TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH = range(10) TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH, TYPE_P2WSH, TYPE_P2TR = range(11)
NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3) NET_MAINNET, NET_TESTNET, NET_SIGNET = range(3)
NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET, NET_MAP = {'mainnet': NET_MAINNET, 'testnet': NET_TESTNET,
'signet': NET_SIGNET} 'signet': NET_SIGNET}
@ -52,6 +52,8 @@ def detect_script_type(script_str):
return TYPE_P2WPKH return TYPE_P2WPKH
elif script.is_witness_v0_scripthash(): elif script.is_witness_v0_scripthash():
return TYPE_P2WSH return TYPE_P2WSH
elif script.is_witness_v1_taproot():
return TYPE_P2TR
raise EngineError("Unknown script type for script '{}'" raise EngineError("Unknown script type for script '{}'"
.format(bintohex(script_str))) .format(bintohex(script_str)))
@ -224,6 +226,12 @@ class BTCEngine(object):
stype = detect_script_type(script) stype = detect_script_type(script)
assert stype in ENGINES assert stype in ENGINES
engine = ENGINES[stype] 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) pscript = engine.pubkey_to_script(pubkey)
return script == pscript return script == pscript
@ -457,5 +465,6 @@ ENGINES = {
TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH, TYPE_TIMELOCK_P2WSH: BTC_Timelocked_P2WSH,
TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH, TYPE_WATCHONLY_TIMELOCK_P2WSH: BTC_Watchonly_Timelocked_P2WSH,
TYPE_WATCHONLY_P2WPKH: BTC_Watchonly_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
} }

67
jmclient/jmclient/taker_utils.py

@ -10,9 +10,9 @@ from .schedule import human_readable_schedule_entry, tweak_tumble_schedule,\
schedule_to_text schedule_to_text
from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \ from .wallet import BaseWallet, estimate_tx_fee, compute_tx_locktime, \
FidelityBondMixin FidelityBondMixin
from jmbitcoin import make_shuffled_tx, amount_to_str, mk_burn_script,\ from jmbitcoin import make_shuffled_tx, amount_to_str, \
PartiallySignedTransaction, CMutableTxOut,\ PartiallySignedTransaction, CMutableTxOut,\
human_readable_transaction, Hash160 human_readable_transaction
from jmbase.support import EXIT_SUCCESS from jmbase.support import EXIT_SUCCESS
log = get_log() log = get_log()
@ -21,6 +21,15 @@ Utility functions for tumbler-style takers;
Currently re-used by CLI script tumbler.py and joinmarket-qt Currently re-used by CLI script tumbler.py and joinmarket-qt
""" """
def get_utxo_scripts(wallet: BaseWallet, utxos: dict) -> list:
# given a Joinmarket wallet and a set of utxos
# as passed from `get_utxos_by_mixdepth` at one mixdepth,
# return the list of script types for each utxo
script_types = []
for utxo in utxos.values():
script_types.append(wallet.get_outtype(utxo["address"]))
return script_types
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
accept_callback=None, info_callback=None, error_callback=None, accept_callback=None, info_callback=None, error_callback=None,
return_transaction=False, with_final_psbt=False, return_transaction=False, with_final_psbt=False,
@ -97,38 +106,34 @@ def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False,
"There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.") "There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.")
return return
total_inputs_val = sum([va['value'] for u, va in utxos.items()]) total_inputs_val = sum([va['value'] for u, va in utxos.items()])
script_types = get_utxo_scripts(wallet_service.wallet, utxos)
if is_burn_destination(destination): fee_est = estimate_tx_fee(len(utxos), 1, txtype=script_types, outtype=outtype)
if len(utxos) > 1:
log.error("Only one input allowed when burning coins, to keep "
+ "the tx small. Tip: use the coin control feature to freeze utxos")
return
address_type = FidelityBondMixin.BIP32_BURN_ID
index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type)
path = wallet_service.wallet.get_path(mixdepth, address_type, index)
privkey, engine = wallet_service.wallet._get_key_from_path(path)
pubkey = engine.privkey_to_pubkey(privkey)
pubkeyhash = Hash160(pubkey)
#size of burn output is slightly different from regular outputs
burn_script = mk_burn_script(pubkeyhash)
fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2)
outs = [{"script": burn_script, "value": total_inputs_val - fee_est}]
destination = "BURNER OUTPUT embedding pubkey at " \
+ wallet_service.wallet.get_path_repr(path) \
+ "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n"
else:
#regular sweep (non-burn)
fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype, outtype=outtype)
outs = [{"address": destination, "value": total_inputs_val - fee_est}] outs = [{"address": destination, "value": total_inputs_val - fee_est}]
else: else:
#not doing a sweep; we will have change change_type = txtype
#8 inputs to be conservative if custom_change_addr:
initial_fee_est = estimate_tx_fee(8,2, txtype=txtype, outtype=outtype) change_type = wallet_service.get_outtype(custom_change_addr)
utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est) if change_type is None:
# we don't recognize this type; best we can do is revert to default,
# even though it may be inaccurate:
change_type = txtype
if outtype is None:
# 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)
utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est,
includeaddr=True)
script_types = get_utxo_scripts(wallet_service.wallet, utxos)
if len(utxos) < 8: if len(utxos) < 8:
fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype, outtype=outtype) fee_est = estimate_tx_fee(len(utxos), 2, txtype=script_types, outtype=outtypes)
else: else:
fee_est = initial_fee_est fee_est = initial_fee_est
total_inputs_val = sum([va['value'] for u, va in utxos.items()]) total_inputs_val = sum([va['value'] for u, va in utxos.items()])

54
jmclient/jmclient/wallet.py

@ -8,6 +8,7 @@ import random
import copy import copy
import base64 import base64
import json import json
from math import ceil
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from datetime import datetime, timedelta from datetime import datetime, timedelta
from calendar import timegm from calendar import timegm
@ -26,8 +27,8 @@ from .support import select_gradual, select_greedy, select_greediest, \
select, NotEnoughFundsException select, NotEnoughFundsException
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WSH,\ from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WSH,\
TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\ TYPE_P2WPKH, TYPE_TIMELOCK_P2WSH, TYPE_SEGWIT_WALLET_FIDELITY_BONDS,\
TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, TYPE_WATCHONLY_P2WPKH,\ TYPE_WATCHONLY_FIDELITY_BONDS, TYPE_WATCHONLY_TIMELOCK_P2WSH, \
ENGINES, detect_script_type, EngineError TYPE_WATCHONLY_P2WPKH, TYPE_P2TR, ENGINES, detect_script_type, EngineError
from .support import get_random_bytes from .support import get_random_bytes
from . import mn_encode, mn_decode from . import mn_encode, mn_decode
import jmbitcoin as btc import jmbitcoin as btc
@ -57,6 +58,21 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh', outtype=None, extra_bytes=0):
'''Returns an estimate of the number of satoshis required '''Returns an estimate of the number of satoshis required
for a transaction with the given number of inputs and outputs, for a transaction with the given number of inputs and outputs,
based on information from the blockchain interface. based on information from the blockchain interface.
Arguments:
ins: int, number of inputs
outs: int, number of outputs
txtype: either a single string, or a list of strings
outtype: either None or a list of strings
extra_bytes: an int
These arguments are intended to allow a kind of 'default', where
all the inputs and outputs match a predefined type (that of the wallet),
but also allow customization for heterogeneous input and output types.
For supported input and output types, see the keys of the dicts
`inmults` and `outmults` in jmbitcoin.secp256k1_transaction.estimate_tx_size`.
Returns:
a single integer number of satoshis as estimate.
''' '''
if jm_single().bc_interface is None: if jm_single().bc_interface is None:
raise RuntimeError("Cannot estimate transaction fee " + raise RuntimeError("Cannot estimate transaction fee " +
@ -73,18 +89,32 @@ def estimate_tx_fee(ins, outs, txtype='p2pkh', outtype=None, extra_bytes=0):
btc.fee_per_kb_to_str(fee_per_kb) + btc.fee_per_kb_to_str(fee_per_kb) +
" greater than absurd value " + " greater than absurd value " +
btc.fee_per_kb_to_str(absurd_fee) + ", quitting.") btc.fee_per_kb_to_str(absurd_fee) + ", quitting.")
if txtype in ['p2pkh', 'p2shMofN']:
tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype, outtype) + extra_bytes # See docstring for explanation:
if isinstance(txtype, str):
ins = [txtype] * ins
else:
assert isinstance(txtype, list)
ins = txtype
if outtype is None:
outs = [txtype] * outs
elif isinstance(outtype, str):
outs = [outtype] * outs
else:
assert isinstance(outtype, list)
outs = outtype
# Note: the calls to `estimate_tx_size` in this code
# block can raise `NotImplementedError` if any of the
# strings in (ins, outs) are not known script types.
if not btc.there_is_one_segwit_input(ins):
tx_estimated_bytes = btc.estimate_tx_size(ins, outs) + extra_bytes
return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0)) return int((tx_estimated_bytes * fee_per_kb)/Decimal(1000.0))
elif txtype in ['p2wpkh', 'p2sh-p2wpkh']: else:
witness_estimate, non_witness_estimate = btc.estimate_tx_size( witness_estimate, non_witness_estimate = btc.estimate_tx_size(
ins, outs, txtype, outtype) ins, outs)
non_witness_estimate += extra_bytes non_witness_estimate += extra_bytes
return int(int(( return int(int(ceil(non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
non_witness_estimate + 0.25*witness_estimate)*fee_per_kb)/Decimal(1000.0))
else:
raise NotImplementedError("Txtype: " + txtype + " not implemented.")
def compute_tx_locktime(): def compute_tx_locktime():
# set locktime for best anonset (Core, Electrum) # set locktime for best anonset (Core, Electrum)
@ -475,6 +505,8 @@ class BaseWallet(object):
return 'p2sh-p2wpkh' return 'p2sh-p2wpkh'
elif script_type == TYPE_P2WSH: elif script_type == TYPE_P2WSH:
return 'p2wsh' return 'p2wsh'
elif script_type == TYPE_P2TR:
return 'p2tr'
# should be unreachable; all possible returns # should be unreachable; all possible returns
# from detect_script_type are covered. # from detect_script_type are covered.
assert False assert False

12
jmclient/test/test_taker.py

@ -516,15 +516,15 @@ def test_custom_change(setup_taker):
for out in taker.latest_tx.vout: for out in taker.latest_tx.vout:
# input utxo is 200M; amount is 20M; as per logs: # input utxo is 200M; amount is 20M; as per logs:
# totalin=200000000 # totalin=200000000
# my_txfee=13050 # my_txfee=13650 <- this estimate ignores address type
# makers_txfee=3000 # makers_txfee=3000
# cjfee_total=12000 => changevalue=179974950 # cjfee_total=12000 => changevalue=179974350
# note that there is a small variation in the size of # note that there is a small variation in the size of
# the transaction (a few bytes) for the different scriptPubKey # the transaction (a few bytes) for the different scriptPubKey
# type, but this is currently ignored by the Taker, who makes # type, but this is currently ignored in coinjoins by the
# fee estimate purely based on the number of ins and outs; # Taker (not true for direct send operations), hence we get
# this will never be too far off anyway. # the same value for each different output type.
if out.scriptPubKey == script and out.nValue == 179974950: if out.scriptPubKey == script and out.nValue == 179974350:
# must be only one # must be only one
assert not custom_change_found assert not custom_change_found
custom_change_found = True custom_change_found = True

Loading…
Cancel
Save