Browse Source

coinchooser fee calculation now uses tx weights. more precise fee calculation for segwit and mixed-segwit txns.

3.1
SomberNight 7 years ago
parent
commit
aaf5e32207
  1. 59
      lib/coinchooser.py
  2. 7
      lib/transaction.py

59
lib/coinchooser.py

@ -68,7 +68,13 @@ class PRNG:
x[i], x[j] = x[j], x[i] x[i], x[j] = x[j], x[i]
Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins', 'min_height']) Bucket = namedtuple('Bucket',
['desc',
'weight', # as in BIP-141
'value', # in satoshis
'coins', # UTXOs
'min_height', # min block height where a coin was confirmed
'witness']) # whether any coin uses segwit
def strip_unneeded(bkts, sufficient_funds): def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount''' '''Remove buckets that are unnecessary in achieving the spend amount'''
@ -91,12 +97,14 @@ class CoinChooserBase(PrintError):
buckets[key].append(coin) buckets[key].append(coin)
def make_Bucket(desc, coins): def make_Bucket(desc, coins):
weight = sum(Transaction.estimated_input_weight(coin) witness = any(Transaction.is_segwit_input(coin) for coin in coins)
for coin in coins) # note that we're guessing whether the tx uses segwit based
size = Transaction.virtual_size_from_weight(weight) # on this single bucket
weight = sum(Transaction.estimated_input_weight(coin, witness)
for coin in coins)
value = sum(coin['value'] for coin in coins) value = sum(coin['value'] for coin in coins)
min_height = min(coin['height'] for coin in coins) min_height = min(coin['height'] for coin in coins)
return Bucket(desc, size, value, coins, min_height) return Bucket(desc, weight, value, coins, min_height, witness)
return list(map(make_Bucket, buckets.keys(), buckets.values())) return list(map(make_Bucket, buckets.keys(), buckets.values()))
@ -169,10 +177,13 @@ class CoinChooserBase(PrintError):
def make_tx(self, coins, outputs, change_addrs, fee_estimator, def make_tx(self, coins, outputs, change_addrs, fee_estimator,
dust_threshold): dust_threshold):
'''Select unspent coins to spend to pay outputs. If the change is """Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to greater than dust_threshold (after adding the change output to
the transaction) it is kept, otherwise none is sent and it is the transaction) it is kept, otherwise none is sent and it is
added to the transaction fee.''' added to the transaction fee.
Note: fee_estimator expects virtual bytes
"""
# Deterministic randomness from coins # Deterministic randomness from coins
utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins] utxos = [c['prevout_hash'] + str(c['prevout_n']) for c in coins]
@ -180,16 +191,36 @@ class CoinChooserBase(PrintError):
# Copy the ouputs so when adding change we don't modify "outputs" # Copy the ouputs so when adding change we don't modify "outputs"
tx = Transaction.from_io([], outputs[:]) tx = Transaction.from_io([], outputs[:])
# Size of the transaction with no inputs and no change # Weight of the transaction with no inputs and no change
base_size = tx.estimated_size() # Note: this will use legacy tx serialization as the need for "segwit"
# would be detected from inputs. The only side effect should be that the
# marker and flag are excluded, which is compensated in get_tx_weight()
base_weight = tx.estimated_weight()
spent_amount = tx.output_value() spent_amount = tx.output_value()
def fee_estimator_w(weight):
return fee_estimator(Transaction.virtual_size_from_weight(weight))
def get_tx_weight(buckets):
total_weight = base_weight + sum(bucket.weight for bucket in buckets)
is_segwit_tx = any(bucket.witness for bucket in buckets)
if is_segwit_tx:
total_weight += 2 # marker and flag
# non-segwit inputs were previously assumed to have
# a witness of '' instead of '00' (hex)
# note that mixed legacy/segwit buckets are already ok
num_legacy_inputs = sum((not bucket.witness) * len(bucket.coins)
for bucket in buckets)
total_weight += num_legacy_inputs
return total_weight
def sufficient_funds(buckets): def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough '''Given a list of buckets, return True if it has enough
value to pay for the transaction''' value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets) total_input = sum(bucket.value for bucket in buckets)
total_size = sum(bucket.size for bucket in buckets) + base_size total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator(total_size) return total_input >= spent_amount + fee_estimator_w(total_weight)
# Collect the coins into buckets, choose a subset of the buckets # Collect the coins into buckets, choose a subset of the buckets
buckets = self.bucketize_coins(coins) buckets = self.bucketize_coins(coins)
@ -197,11 +228,11 @@ class CoinChooserBase(PrintError):
self.penalty_func(tx)) self.penalty_func(tx))
tx.add_inputs([coin for b in buckets for coin in b.coins]) tx.add_inputs([coin for b in buckets for coin in b.coins])
tx_size = base_size + sum(bucket.size for bucket in buckets) tx_weight = get_tx_weight(buckets)
# This takes a count of change outputs and returns a tx fee # This takes a count of change outputs and returns a tx fee
output_size = Transaction.estimated_output_size(change_addrs[0]) output_weight = 4 * Transaction.estimated_output_size(change_addrs[0])
fee = lambda count: fee_estimator(tx_size + count * output_size) fee = lambda count: fee_estimator_w(tx_weight + count * output_weight)
change = self.change_outputs(tx, change_addrs, fee, dust_threshold) change = self.change_outputs(tx, change_addrs, fee, dust_threshold)
tx.add_outputs(change) tx.add_outputs(change)

7
lib/transaction.py

@ -863,17 +863,16 @@ class Transaction:
return self.virtual_size_from_weight(weight) return self.virtual_size_from_weight(weight)
@classmethod @classmethod
def estimated_input_weight(cls, txin): def estimated_input_weight(cls, txin, is_segwit_tx):
'''Return an estimate of serialized input weight in weight units.''' '''Return an estimate of serialized input weight in weight units.'''
script = cls.input_script(txin, True) script = cls.input_script(txin, True)
input_size = len(cls.serialize_input(txin, script)) // 2 input_size = len(cls.serialize_input(txin, script)) // 2
# note: we should actually branch based on tx.is_segwit()
# only if none of the inputs have a witness, is the size actually 0
if cls.is_segwit_input(txin): if cls.is_segwit_input(txin):
assert is_segwit_tx
witness_size = len(cls.serialize_witness(txin, True)) // 2 witness_size = len(cls.serialize_witness(txin, True)) // 2
else: else:
witness_size = 0 witness_size = 1 if is_segwit_tx else 0
return 4 * input_size + witness_size return 4 * input_size + witness_size

Loading…
Cancel
Save