tlnchannel/lnhtlc: speed up balance calculation for recent ctns - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 5b23d5ee979c7c6b23cfc3f6fc1f92a99dcab5f8 DIR parent ec7473789e26e0d77d33ec31ec114930d5430dd5 HTML Author: SomberNight <somber.night@protonmail.com> Date: Sat, 7 Mar 2020 05:05:05 +0100 lnchannel/lnhtlc: speed up balance calculation for recent ctns Move the balance calculation from lnchannel to lnhtlc. Maintain a running balance in lnhtlc that is coupled with _maybe_active_htlc_ids for practicality reasons. Diffstat: M electrum/lnchannel.py | 22 +++++----------------- M electrum/lnhtlc.py | 45 ++++++++++++++++++++++++++++--- M electrum/tests/test_lnpeer.py | 4 ++-- 3 files changed, 48 insertions(+), 23 deletions(-) --- DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py t@@ -610,7 +610,7 @@ class Channel(Logger): reason = self._receive_fail_reasons.get(htlc.htlc_id) self.lnworker.payment_failed(self, htlc.payment_hash, reason) - def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None): + def balance(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None) -> int: """ This balance in mSAT is not including reserve and fees. So a node cannot actually use its whole balance. t@@ -623,22 +623,10 @@ class Channel(Logger): """ assert type(whose) is HTLCOwner initial = self.config[whose].initial_msat - - # TODO slow. -- and 'balance' is called from a decent number of places (e.g. 'make_commitment') - for direction, htlc in self.hm.all_settled_htlcs_ever(ctx_owner, ctn): - # note: could "simplify" to (whose * ctx_owner == direction * SENT) - if whose == ctx_owner: - if direction == SENT: - initial -= htlc.amount_msat - else: - initial += htlc.amount_msat - else: - if direction == SENT: - initial += htlc.amount_msat - else: - initial -= htlc.amount_msat - - return initial + return self.hm.get_balance_msat(whose=whose, + ctx_owner=ctx_owner, + ctn=ctn, + initial_balance_msat=initial) def balance_minus_outgoing_htlcs(self, whose: HTLCOwner, *, ctx_owner: HTLCOwner = HTLCOwner.LOCAL): """ DIR diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py t@@ -173,10 +173,12 @@ class HTLCManager: self.log['unacked_local_updates2'].pop(self.log[REMOTE]['ctn'], None) def _update_maybe_active_htlc_ids(self) -> None: - # Loosely, we want a set that contains the htlcs that are - # not "removed and revoked from all ctxs of both parties". - # It is guaranteed that those htlcs are in the set, but older htlcs might be there too: - # there is a sanity margin of 1 ctn -- this relaxes the care needed re order of method calls. + # - Loosely, we want a set that contains the htlcs that are + # not "removed and revoked from all ctxs of both parties". (self._maybe_active_htlc_ids) + # It is guaranteed that those htlcs are in the set, but older htlcs might be there too: + # there is a sanity margin of 1 ctn -- this relaxes the care needed re order of method calls. + # - balance_delta is in sync with maybe_active_htlc_ids. When htlcs are removed from the latter, + # balance_delta is updated to reflect that htlc. sanity_margin = 1 for htlc_proposer in (LOCAL, REMOTE): for log_action in ('settles', 'fails'): t@@ -188,10 +190,14 @@ class HTLCManager: and ctns[REMOTE] is not None and ctns[REMOTE] <= self.ctn_oldest_unrevoked(REMOTE) - sanity_margin): self._maybe_active_htlc_ids[htlc_proposer].remove(htlc_id) + if log_action == 'settles': + htlc = self.log[htlc_proposer]['adds'][htlc_id] # type: UpdateAddHtlc + self._balance_delta -= htlc.amount_msat * htlc_proposer def _init_maybe_active_htlc_ids(self): self._maybe_active_htlc_ids = {LOCAL: set(), REMOTE: set()} # first idx is "side who offered htlc" # add all htlcs + self._balance_delta = 0 # the balance delta of LOCAL since channel open for htlc_proposer in (LOCAL, REMOTE): for htlc_id in self.log[htlc_proposer]['adds']: self._maybe_active_htlc_ids[htlc_proposer].add(htlc_id) t@@ -333,6 +339,37 @@ class HTLCManager: received = [(RECEIVED, x) for x in self.all_settled_htlcs_ever_by_direction(subject, RECEIVED, ctn)] return sent + received + def get_balance_msat(self, whose: HTLCOwner, *, ctx_owner=HTLCOwner.LOCAL, ctn: int = None, + initial_balance_msat: int) -> int: + """Returns the balance of 'whose' in 'ctx' at 'ctn'. + Only HTLCs that have been settled by that ctn are counted. + """ + if ctn is None: + ctn = self.ctn_oldest_unrevoked(ctx_owner) + balance = initial_balance_msat + if ctn >= self.ctn_oldest_unrevoked(ctx_owner): + balance += self._balance_delta * whose + considered_sent_htlc_ids = self._maybe_active_htlc_ids[whose] + considered_recv_htlc_ids = self._maybe_active_htlc_ids[-whose] + else: # ctn is too old; need to consider full log (slow...) + considered_sent_htlc_ids = self.log[whose]['settles'] + considered_recv_htlc_ids = self.log[-whose]['settles'] + # sent htlcs + for htlc_id in considered_sent_htlc_ids: + ctns = self.log[whose]['settles'].get(htlc_id, None) + if ctns is None: continue + if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn: + htlc = self.log[whose]['adds'][htlc_id] + balance -= htlc.amount_msat + # recv htlcs + for htlc_id in considered_recv_htlc_ids: + ctns = self.log[-whose]['settles'].get(htlc_id, None) + if ctns is None: continue + if ctns[ctx_owner] is not None and ctns[ctx_owner] <= ctn: + htlc = self.log[-whose]['adds'][htlc_id] + balance += htlc.amount_msat + return balance + def _get_htlcs_that_got_removed_exactly_at_ctn( self, ctn: int, *, ctx_owner: HTLCOwner, htlc_proposer: HTLCOwner, log_action: str, ) -> Sequence[UpdateAddHtlc]: DIR diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py t@@ -306,14 +306,14 @@ class TestPeer(ElectrumTestCase): with self.assertRaises(concurrent.futures.CancelledError): run(f()) - @unittest.skip("too expensive") + #@unittest.skip("too expensive") #@needs_test_with_all_chacha20_implementations def test_payments_stresstest(self): alice_channel, bob_channel = create_test_channels() p1, p2, w1, w2, _q1, _q2 = self.prepare_peers(alice_channel, bob_channel) alice_init_balance_msat = alice_channel.balance(HTLCOwner.LOCAL) bob_init_balance_msat = bob_channel.balance(HTLCOwner.LOCAL) - num_payments = 1000 + num_payments = 50 #pay_reqs1 = [self.prepare_invoice(w1, amount_sat=1) for i in range(num_payments)] pay_reqs2 = [self.prepare_invoice(w2, amount_sat=1) for i in range(num_payments)] max_htlcs_in_flight = asyncio.Semaphore(5)