Move coin choosing logic to own class

This contains no change in logic, but is preparation for cleanup
and possible alternative strategies.
This commit is contained in:
Neil Booth 2015-11-28 14:47:08 +09:00
parent 0c20e737a9
commit e9061ea371
2 changed files with 123 additions and 76 deletions

104
lib/coinchooser.py Normal file
View File

@ -0,0 +1,104 @@
#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from operator import itemgetter
from bitcoin import MIN_RELAY_TX_FEE
from util import NotEnoughFunds, PrintError, profiler
from transaction import Transaction
class CoinChooser(PrintError):
def __init__(self, wallet):
self.wallet = wallet
def fee(self, tx, fixed_fee, fee_per_kb):
if fixed_fee is not None:
return fixed_fee
return self.wallet.estimated_fee(tx, fee_per_kb)
def dust_threshold(self):
return 182 * 3 * MIN_RELAY_TX_FEE/1000
def make_tx(self, coins, outputs, change_addrs, fixed_fee, fee_per_kb):
'''Select unspent coins to spend to pay outputs.'''
amount = sum(map(lambda x: x[2], outputs))
total = 0
inputs = []
tx = Transaction.from_io(inputs, outputs)
fee = self.fee(tx, fixed_fee, fee_per_kb)
# add inputs, sorted by age
for item in coins:
v = item.get('value')
total += v
self.wallet.add_input_info(item)
tx.add_input(item)
# no need to estimate fee until we have reached desired amount
if total < amount + fee:
continue
fee = self.fee(tx, fixed_fee, fee_per_kb)
if total >= amount + fee:
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:
tx.inputs.remove(item)
total -= v
removed = True
continue
else:
break
if removed:
fee = self.fee(tx, fixed_fee, fee_per_kb)
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 = self.fee(tx, fixed_fee, fee_per_kb)
continue
break
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)
if fixed_fee is not None and change_amount > 0:
tx.outputs.append(('address', change_addr, change_amount))
elif change_amount > self.dust_threshold():
tx.outputs.append(('address', change_addr, change_amount))
# recompute fee including change output
fee = self.wallet.estimated_fee(tx, fee_per_kb)
# remove change output
tx.outputs.pop()
# if change is still above dust threshold, re-add change output.
change_amount = total - (amount + fee)
if change_amount > self.dust_threshold():
tx.outputs.append(('address', change_addr, change_amount))
self.print_error('change', change_amount)
else:
self.print_error('not keeping dust', change_amount)
else:
self.print_error('not keeping dust', change_amount)
return tx

View File

@ -24,9 +24,8 @@ import random
import time import time
import json import json
import copy import copy
from operator import itemgetter
from util import NotEnoughFunds, PrintError, profiler from util import PrintError, profiler
from bitcoin import * from bitcoin import *
from account import * from account import *
@ -35,6 +34,7 @@ from version import *
from transaction import Transaction from transaction import Transaction
from plugins import run_hook from plugins import run_hook
import bitcoin import bitcoin
from coinchooser import CoinChooser
from synchronizer import Synchronizer from synchronizer import Synchronizer
from verifier import SPV from verifier import SPV
from mnemonic import Mnemonic from mnemonic import Mnemonic
@ -153,6 +153,7 @@ class Abstract_Wallet(PrintError):
self.network = None self.network = None
self.electrum_version = ELECTRUM_VERSION self.electrum_version = ELECTRUM_VERSION
self.gap_limit_for_change = 6 # constant self.gap_limit_for_change = 6 # constant
self.coin_chooser = CoinChooser(self)
# saved fields # saved fields
self.seed_version = storage.get('seed_version', NEW_SEED_VERSION) self.seed_version = storage.get('seed_version', NEW_SEED_VERSION)
self.use_change = storage.get('use_change',True) self.use_change = storage.get('use_change',True)
@ -898,9 +899,6 @@ class Abstract_Wallet(PrintError):
# this method can be overloaded # this method can be overloaded
return tx.get_fee() return tx.get_fee()
def dust_threshold(self):
return 182 * 3 * MIN_RELAY_TX_FEE/1000
@profiler @profiler
def estimated_fee(self, tx, fee_per_kb): def estimated_fee(self, tx, fee_per_kb):
estimated_size = len(tx.serialize(-1))/2 estimated_size = len(tx.serialize(-1))/2
@ -916,84 +914,29 @@ class Abstract_Wallet(PrintError):
assert is_address(data), "Address " + data + " is invalid!" assert is_address(data), "Address " + data + " is invalid!"
fee_per_kb = self.fee_per_kb(config) fee_per_kb = self.fee_per_kb(config)
amount = sum(map(lambda x:x[2], outputs))
total = 0
inputs = []
tx = Transaction.from_io(inputs, outputs)
fee = fixed_fee if fixed_fee is not None else 0
# add inputs, sorted by age
for item in coins:
v = item.get('value')
total += v
self.add_input_info(item)
tx.add_input(item)
# no need to estimate fee until we have reached desired amount
if total < amount + fee:
continue
fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb)
if total >= amount + fee:
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:
tx.inputs.remove(item)
total -= v
removed = True
continue
else:
break
if removed:
fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb)
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 = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb)
continue
break
self.print_error("using %d inputs"%len(tx.inputs))
# change address # change address
if not change_addr: if change_addr:
change_addrs = [change_addr]
else:
# send change to one of the accounts involved in the tx # send change to one of the accounts involved in the tx
address = inputs[0].get('address') address = coins[0].get('address')
account, _ = self.get_address_index(address) account, _ = self.get_address_index(address)
if self.use_change and self.accounts[account].has_change(): if self.use_change and self.accounts[account].has_change():
# New change addresses are created only after a few confirmations. # New change addresses are created only after a few
# Choose an unused change address if any, otherwise take one at random # confirmations. Select the unused addresses within the
change_addrs = self.accounts[account].get_addresses(1)[-self.gap_limit_for_change:] # gap limit; if none take one at random
for change_addr in change_addrs: addrs = self.accounts[account].get_addresses(1)[-self.gap_limit_for_change:]
if self.get_num_tx(change_addr) == 0: change_addrs = [addr for addr in addrs if
break self.get_num_tx(change_addr) == 0]
else: if not change_addrs:
change_addr = random.choice(change_addrs) change_addrs = [random.choice(addrs)]
else: else:
change_addr = address change_addrs = [address]
# if change is above dust threshold, add a change output. # Let the coin chooser select the coins to spend
change_amount = total - ( amount + fee ) tx = self.coin_chooser.make_tx(coins, outputs, change_addrs,
if fixed_fee is not None and change_amount > 0: fixed_fee, fee_per_kb)
tx.outputs.append(('address', change_addr, change_amount))
elif change_amount > self.dust_threshold():
tx.outputs.append(('address', change_addr, change_amount))
# recompute fee including change output
fee = self.estimated_fee(tx, fee_per_kb)
# remove change output
tx.outputs.pop()
# if change is still above dust threshold, re-add change output.
change_amount = total - ( amount + fee )
if change_amount > self.dust_threshold():
tx.outputs.append(('address', change_addr, change_amount))
self.print_error('change', change_amount)
else:
self.print_error('not keeping dust', change_amount)
else:
self.print_error('not keeping dust', change_amount)
# Sort the inputs and outputs deterministically # Sort the inputs and outputs deterministically
tx.BIP_LI01_sort() tx.BIP_LI01_sort()