tcoinchooser: improve performance significantly - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit d56917f4b15541164047ecb61e411fc902b81515 DIR parent 15dda65c5282c133558584cff3bc397ad3763905 HTML 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 tthis is now mostly linear (linear if shortcut is hit; otherwise in rare cases still quadratic) ttested 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: M electrum/coinchooser.py | 41 ++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 12 deletions(-) --- DIR diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py t@@ -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): t@@ -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) t@@ -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 t@@ -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: t@@ -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()