From 93bb09230c917bd58a139fb39b2d82a64da06cc9 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sun, 29 Nov 2015 13:29:33 +0900 Subject: [PATCH] Track tx size directly; calculate fees from that This has several advantages. Fee calculation is now very fast, as we don't need to keep reserializing the tx. Another is that we can reason about the fees after adding a change output without having to add it, recalculate the tx fee, and remove it again. --- lib/coinchooser.py | 56 ++++++++++++++++------------------------------ lib/transaction.py | 35 ++++++++++++++++++++--------- lib/wallet.py | 9 ++++---- 3 files changed, 48 insertions(+), 52 deletions(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index dcd54de0..9fa7c76e 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -33,60 +33,42 @@ class CoinChooser(PrintError): amount = sum(map(lambda x: x[2], outputs)) total = 0 tx = Transaction.from_io([], outputs) - fee = fee_estimator(tx) + + # Size of the transaction with no inputs and no change + base_size = tx.estimated_size() + # Pay to bitcoin address serializes as 34 bytes + change_size = 34 + # Size of each serialized coin + for coin in coins: + coin['size'] = Transaction.estimated_input_size(coin) + + size = base_size # add inputs, sorted by age for item in coins: v = item.get('value') total += v + size += item['size'] tx.add_input(item) - # no need to estimate fee until we have reached desired amount - if total < amount + fee: - continue - fee = fee_estimator(tx) - if total >= amount + fee: + if total >= amount + fee_estimator(size): break else: raise NotEnoughFunds() # remove unneeded inputs. - removed = False for item in sorted(tx.inputs, key=itemgetter('value')): v = item.get('value') - if total - v >= amount + fee: + if total - v >= amount + fee_estimator(size - item['size']): tx.inputs.remove(item) total -= v - removed = True - continue - else: - break - if removed: - fee = fee_estimator(tx) - for item in sorted(tx.inputs, key=itemgetter('value')): - v = item.get('value') - if total - v >= amount + fee: - tx.inputs.remove(item) - total -= v - fee = fee_estimator(tx) - continue - break + size -= item['size'] self.print_error("using %d inputs" % len(tx.inputs)) - # if change is above dust threshold, add a change output. - change_addr = change_addrs[0] - change_amount = total - (amount + fee) - - # See if change would still be greater than dust after adding - # the change to the transaction + # If change is above dust threshold after accounting for the + # size of the change output, add it to the transaction. + change_amount = total - (amount + fee_estimator(size + change_size)) if change_amount > dust_threshold: - tx.outputs.append(('address', change_addr, change_amount)) - fee = fee_estimator(tx) - # remove change output - tx.outputs.pop() - change_amount = total - (amount + fee) - - # If change is still above dust threshold, keep the change. - if change_amount > dust_threshold: - tx.outputs.append(('address', change_addr, change_amount)) + tx.outputs.append(('address', change_addrs[0], change_amount)) + size += change_size self.print_error('change', change_amount) elif change_amount: self.print_error('not keeping dust', change_amount) diff --git a/lib/transaction.py b/lib/transaction.py index 6c167a2f..5dde8f7e 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -595,6 +595,7 @@ class Transaction: raise return script + @classmethod def input_script(self, txin, i, for_sig): # for_sig: # -1 : do not sign, estimate length @@ -641,6 +642,18 @@ class Transaction: return script + @classmethod + def serialize_input(self, txin, i, for_sig): + # Prev hash and index + s = txin['prevout_hash'].decode('hex')[::-1].encode('hex') + s = int_to_hex(txin['prevout_n'], 4) + # Script length, script, sequence + script = self.input_script(txin, i, for_sig) + s += var_int(len(script) / 2) + s += script + s += "ffffffff" + return s + def BIP_LI01_sort(self): # See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki self.inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) @@ -652,12 +665,7 @@ class Transaction: s = int_to_hex(1,4) # version s += var_int( len(inputs) ) # number of inputs for i, txin in enumerate(inputs): - s += txin['prevout_hash'].decode('hex')[::-1].encode('hex') # prev hash - s += int_to_hex(txin['prevout_n'], 4) # prev index - script = self.input_script(txin, i, for_sig) - s += var_int( len(script)/2 ) # script length - s += script - s += "ffffffff" # sequence + s += self.serialize_input(txin, i, for_sig) s += var_int( len(outputs) ) # number of outputs for output in outputs: output_type, addr, amount = output @@ -690,7 +698,7 @@ class Transaction: return self.input_value() - self.output_value() @classmethod - def estimated_fee_for_size(self, fee_per_kb, size): + def fee_for_size(self, fee_per_kb, size): '''Given a fee per kB in satoshis, and a tx size in bytes, returns the transaction fee.''' fee = int(fee_per_kb * size / 1000.) @@ -699,11 +707,18 @@ class Transaction: return fee @profiler + def estimated_size(self): + '''Return an estimated tx size in bytes.''' + return len(self.serialize(-1)) / 2 # ASCII hex string + + @classmethod + def estimated_input_size(self, txin): + '''Return an estimated of serialized input size in bytes.''' + return len(self.serialize_input(txin, -1, -1)) / 2 + def estimated_fee(self, fee_per_kb): '''Return an estimated fee given a fee per kB in satoshis.''' - # Remember self.serialize returns an ASCII hex string - size = len(self.serialize(-1)) / 2 - return self.estimated_fee_for_size(fee_per_kb, size) + return self.fee_for_size(fee_per_kb, self.estimated_size()) def signature_count(self): r = 0 diff --git a/lib/wallet.py b/lib/wallet.py index 51ffd4e3..8faa42f9 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -24,6 +24,7 @@ import random import time import json import copy +from functools import partial from util import PrintError, profiler @@ -929,12 +930,10 @@ class Abstract_Wallet(PrintError): # Fee estimator if fixed_fee is None: - fee_per_kb = self.fee_per_kb(config) - def fee_estimator(tx): - return tx.estimated_fee(fee_per_kb) + fee_estimator = partial(Transaction.fee_for_size, + self.fee_per_kb(config)) else: - def fee_estimator(tx): - return fixed_fee + fee_estimator = lambda size: fixed_fee # Change <= dust threshold is added to the tx fee dust_threshold = 182 * 3 * MIN_RELAY_TX_FEE / 1000