electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 93bb09230c917bd58a139fb39b2d82a64da06cc9
parent a4dd5acc48e75f1c6cb2873abacc5b6717ba3727
Author: Neil Booth <kyuupichan@gmail.com>
Date:   Sun, 29 Nov 2015 13:29:33 +0900

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.

Diffstat:
Mlib/coinchooser.py | 56+++++++++++++++++++-------------------------------------
Mlib/transaction.py | 35+++++++++++++++++++++++++----------
Mlib/wallet.py | 9++++-----
3 files changed, 48 insertions(+), 52 deletions(-)

diff --git 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_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 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)) + 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 @@ -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 @@ -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