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:
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()