Browse Source

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
regtest_lnd
SomberNight 6 years ago
parent
commit
d56917f4b1
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 41
      electrum/coinchooser.py

41
electrum/coinchooser.py

@ -80,12 +80,17 @@ class Bucket(NamedTuple):
def strip_unneeded(bkts, sufficient_funds): def strip_unneeded(bkts, sufficient_funds):
'''Remove buckets that are unnecessary in achieving the spend amount''' '''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)): for i in range(len(bkts)):
if not sufficient_funds(bkts[i + 1:]): bucket_value_sum += (bkts[i]).value
return bkts[i:] if sufficient_funds(bkts[:i+1], bucket_value_sum=bucket_value_sum):
# none of the buckets are needed return bkts[:i+1]
return [] raise Exception("keeping all buckets is still not enough")
class CoinChooserBase(PrintError): class CoinChooserBase(PrintError):
@ -230,10 +235,15 @@ class CoinChooserBase(PrintError):
return total_weight 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 '''Given a list of buckets, return True if it has enough
value to pay for the transaction''' 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) total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight) return total_input >= spent_amount + fee_estimator_w(total_weight)
@ -278,7 +288,7 @@ class CoinChooserRandom(CoinChooserBase):
# Add all singletons # Add all singletons
for n, bucket in enumerate(buckets): for n, bucket in enumerate(buckets):
if sufficient_funds([bucket]): if sufficient_funds([bucket], bucket_value_sum=bucket.value):
candidates.add((n, )) candidates.add((n, ))
# And now some random ones # And now some random ones
@ -289,9 +299,12 @@ class CoinChooserRandom(CoinChooserBase):
# incrementally combine buckets until sufficient # incrementally combine buckets until sufficient
self.p.shuffle(permutation) self.p.shuffle(permutation)
bkts = [] bkts = []
bucket_value_sum = 0
for count, index in enumerate(permutation): for count, index in enumerate(permutation):
bkts.append(buckets[index]) bucket = buckets[index]
if sufficient_funds(bkts): 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]))) candidates.add(tuple(sorted(permutation[:count + 1])))
break break
else: else:
@ -320,16 +333,20 @@ class CoinChooserRandom(CoinChooserBase):
bucket_sets = [conf_buckets, unconf_buckets, other_buckets] bucket_sets = [conf_buckets, unconf_buckets, other_buckets]
already_selected_buckets = [] already_selected_buckets = []
already_selected_buckets_value_sum = 0
for bkts_choose_from in bucket_sets: for bkts_choose_from in bucket_sets:
try: try:
def sfunds(bkts): def sfunds(bkts, *, bucket_value_sum):
return sufficient_funds(already_selected_buckets + bkts) 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) candidates = self.bucket_candidates_any(bkts_choose_from, sfunds)
break break
except NotEnoughFunds: except NotEnoughFunds:
already_selected_buckets += bkts_choose_from already_selected_buckets += bkts_choose_from
already_selected_buckets_value_sum += sum(bucket.value for bucket in bkts_choose_from)
else: else:
raise NotEnoughFunds() raise NotEnoughFunds()

Loading…
Cancel
Save