electrum

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

commit d56917f4b15541164047ecb61e411fc902b81515
parent 15dda65c5282c133558584cff3bc397ad3763905
Author: SomberNight <somber.night@protonmail.com>
Date:   Thu,  2 May 2019 03:07:34 +0200

coinchooser: improve performance significantly

existing code was n^2 in number of UTXOs
this is now mostly linear
(linear if shortcut is hit; otherwise in rare cases still quadratic)

tested using wallet with 800 UTXOs, most of which were needed to make payment
coinchooser.make_tx() went from 18 sec to 0.8 sec

Diffstat:
Melectrum/coinchooser.py | 41+++++++++++++++++++++++++++++------------
1 file changed, 29 insertions(+), 12 deletions(-)

diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py @@ -80,12 +80,17 @@ class Bucket(NamedTuple): def strip_unneeded(bkts, sufficient_funds): '''Remove buckets that are unnecessary in achieving the spend amount''' - bkts = sorted(bkts, key = lambda bkt: bkt.value) + if sufficient_funds([], bucket_value_sum=0): + # none of the buckets are needed + return [] + bkts = sorted(bkts, key=lambda bkt: bkt.value, reverse=True) + bucket_value_sum = 0 for i in range(len(bkts)): - if not sufficient_funds(bkts[i + 1:]): - return bkts[i:] - # none of the buckets are needed - return [] + bucket_value_sum += (bkts[i]).value + if sufficient_funds(bkts[:i+1], bucket_value_sum=bucket_value_sum): + return bkts[:i+1] + raise Exception("keeping all buckets is still not enough") + class CoinChooserBase(PrintError): @@ -230,10 +235,15 @@ class CoinChooserBase(PrintError): return total_weight - def sufficient_funds(buckets): + def sufficient_funds(buckets, *, bucket_value_sum): '''Given a list of buckets, return True if it has enough value to pay for the transaction''' - total_input = input_value + sum(bucket.value for bucket in buckets) + # assert bucket_value_sum == sum(bucket.value for bucket in buckets) # expensive! + total_input = input_value + bucket_value_sum + if total_input < spent_amount: # shortcut for performance + return False + # note re performance: so far this was constant time + # what follows is linear in len(buckets) total_weight = get_tx_weight(buckets) return total_input >= spent_amount + fee_estimator_w(total_weight) @@ -278,7 +288,7 @@ class CoinChooserRandom(CoinChooserBase): # Add all singletons for n, bucket in enumerate(buckets): - if sufficient_funds([bucket]): + if sufficient_funds([bucket], bucket_value_sum=bucket.value): candidates.add((n, )) # And now some random ones @@ -289,9 +299,12 @@ class CoinChooserRandom(CoinChooserBase): # incrementally combine buckets until sufficient self.p.shuffle(permutation) bkts = [] + bucket_value_sum = 0 for count, index in enumerate(permutation): - bkts.append(buckets[index]) - if sufficient_funds(bkts): + bucket = buckets[index] + bkts.append(bucket) + bucket_value_sum += bucket.value + if sufficient_funds(bkts, bucket_value_sum=bucket_value_sum): candidates.add(tuple(sorted(permutation[:count + 1]))) break else: @@ -320,16 +333,20 @@ class CoinChooserRandom(CoinChooserBase): bucket_sets = [conf_buckets, unconf_buckets, other_buckets] already_selected_buckets = [] + already_selected_buckets_value_sum = 0 for bkts_choose_from in bucket_sets: try: - def sfunds(bkts): - return sufficient_funds(already_selected_buckets + bkts) + def sfunds(bkts, *, bucket_value_sum): + bucket_value_sum += already_selected_buckets_value_sum + return sufficient_funds(already_selected_buckets + bkts, + bucket_value_sum=bucket_value_sum) candidates = self.bucket_candidates_any(bkts_choose_from, sfunds) break except NotEnoughFunds: already_selected_buckets += bkts_choose_from + already_selected_buckets_value_sum += sum(bucket.value for bucket in bkts_choose_from) else: raise NotEnoughFunds()