URI: 
       tUse bucketing to choose coins - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 9a6dcf7b1e0219bd6d5a5a51d21ec1dc6df999d0
   DIR parent 93bb09230c917bd58a139fb39b2d82a64da06cc9
  HTML Author: Neil Booth <kyuupichan@gmail.com>
       Date:   Sun, 29 Nov 2015 17:59:36 +0900
       
       Use bucketing to choose coins
       
       Bucketing is generalization of coin chooser logic that makes it easy
       tto implement other algorithms.
       
       - Put core coin chooser functionality in base class.
       - Specialize derived class to implement classic electrum algorithm of
         oldest coins first.  One bucket per output.
       
       No intended change in behaviour.
       Coin chooser now sorts the coins as it wants; remove redundant sorting
       from get_spendable_coins().
       
       Diffstat:
         M lib/coinchooser.py                  |     101 +++++++++++++++++++++----------
         M lib/wallet.py                       |      10 ++--------
       
       2 files changed, 72 insertions(+), 39 deletions(-)
       ---
   DIR diff --git a/lib/coinchooser.py b/lib/coinchooser.py
       t@@ -16,13 +16,28 @@
        # 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 collections import defaultdict, namedtuple
        
        from util import NotEnoughFunds, PrintError, profiler
        from transaction import Transaction
        
       +Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins'])
        
       -class CoinChooser(PrintError):
       +class CoinChooserBase(PrintError):
       +
       +    def bucketize_coins(self, coins):
       +        keys = self.keys(coins)
       +        buckets = defaultdict(list)
       +        for key, coin in zip(keys, coins):
       +            buckets[key].append(coin)
       +
       +        def make_Bucket(desc, coins):
       +            size = sum(Transaction.estimated_input_size(coin)
       +                       for coin in coins)
       +            value = sum(coin['value'] for coin in coins)
       +            return Bucket(desc, size, value, coins)
       +
       +        return map(make_Bucket, buckets.keys(), buckets.values())
        
            def make_tx(self, coins, outputs, change_addrs, fee_estimator,
                        dust_threshold):
       t@@ -30,47 +45,71 @@ class CoinChooser(PrintError):
                greater than dust_threshold (after adding the change output to
                the transaction) it is kept, otherwise none is sent and it is
                added to the transaction fee.'''
       -        amount = sum(map(lambda x: x[2], outputs))
       -        total = 0
       -        tx = Transaction.from_io([], outputs)
       +        output_total = sum(map(lambda x: x[2], outputs))
        
                # Size of the transaction with no inputs and no change
       +        tx = Transaction.from_io([], outputs)
                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)
       -            if total >= amount + fee_estimator(size):
       -                break
       -        else:
       -            raise NotEnoughFunds()
       +        # Returns fee given input size
       +        fee = lambda input_size: fee_estimator(base_size + input_size)
       +
       +        # Collect the coins into buckets, choose a subset of the buckets
       +        buckets = self.bucketize_coins(coins)
       +        buckets = self.choose_buckets(buckets, output_total, fee)
        
       -        # remove unneeded inputs.
       -        for item in sorted(tx.inputs, key=itemgetter('value')):
       -            v = item.get('value')
       -            if total - v >= amount + fee_estimator(size - item['size']):
       -                tx.inputs.remove(item)
       -                total -= v
       -                size -= item['size']
                self.print_error("using %d inputs" % len(tx.inputs))
       +        self.print_error("using buckets:", [bucket.desc for bucket in buckets])
       +
       +        tx.inputs = [coin for b in buckets for coin in b.coins]
       +        input_total = sum(bucket.value for bucket in buckets)
       +        tx_size = base_size + sum(bucket.size for bucket in buckets)
        
                # 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))
       +        # Pay to bitcoin address serializes as 34 bytes
       +        change_size = 34
       +        fee = fee_estimator(tx_size + change_size)
       +        change_amount = input_total - (output_total + fee)
                if change_amount > dust_threshold:
                    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)
        
                return tx
       +
       +class CoinChooser(CoinChooserBase):
       +    '''The original electrum algorithm.  Chooses coins starting with the
       +    oldest that are sufficient to cover the spent amount, and then
       +    removes any not needed starting with the smallest in value.'''
       +
       +    def keys(self, coins):
       +        return [coin['prevout_hash'] + ':' + str(coin['prevout_n'])
       +                for coin in coins]
       +
       +    def choose_buckets(self, buckets, spent_amount, fee):
       +        '''Spend the oldest buckets first.'''
       +        # Unconfirmed coins are young, not old
       +        adj_height = lambda height: 99999999 if height == 0 else height
       +        buckets.sort(key = lambda b: max(adj_height(coin['height'])
       +                                         for coin in b.coins))
       +        selected, value, size = [], 0, 0
       +        for bucket in buckets:
       +            selected.append(bucket)
       +            value += bucket.value
       +            size += bucket.size
       +            if value >= spent_amount + fee(size):
       +                break
       +        else:
       +            raise NotEnoughFunds()
       +
       +        # Remove unneeded inputs starting with the smallest.
       +        selected.sort(key = lambda b: b.value)
       +        dropped = []
       +        for bucket in selected:
       +            if value - bucket.value >= spent_amount + fee(size - bucket.size):
       +                value -= bucket.value
       +                size -= bucket.size
       +                dropped.append(bucket)
       +
       +        return [bucket for bucket in selected if bucket not in dropped]
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -627,15 +627,9 @@ class Abstract_Wallet(PrintError):
                            'height':tx_height,
                            'coinbase':is_cb
                        }
       -                coins.append((tx_height, output))
       +                coins.append(output)
                        continue
       -        # sort by age
       -        if coins:
       -            coins = sorted(coins)
       -            if coins[-1][0] != 0:
       -                while coins[0][0] == 0:
       -                    coins = coins[1:] + [ coins[0] ]
       -        return [value for height, value in coins]
       +        return coins
        
            def get_max_amount(self, config, inputs, fee):
                sendable = sum(map(lambda x:x['value'], inputs))