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

Loading…
Cancel
Save