diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 607968d2e..dde0ba0c0 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -710,6 +710,10 @@ class LNGossip(LNWorker): class PaySession(Logger): + + # how long we wait for another htlc to resolve after receiving a failure for one sent htlc. + TIMEOUT_WAIT_FOR_NEXT_RESOLVED_HTLC = 0.5 + def __init__( self, *, @@ -754,6 +758,10 @@ class PaySession(Logger): pkey = sha256(self.payment_key) return f"{self.payment_hash[:4].hex()}-{pkey[:2].hex()}" + @property + def number_htlcs_inflight(self) -> int: + return self._nhtlcs_inflight + def maybe_raise_trampoline_fee(self, htlc_log: HtlcLog): if htlc_log.trampoline_fee_level == self.trampoline_fee_level: self.trampoline_fee_level += 1 @@ -1709,46 +1717,57 @@ class LNWallet(LNWorker): # It is also triggered here to update progress for a lightning payment in the GUI # (e.g. attempt counter) util.trigger_callback('invoice_status', self.wallet, payment_hash.hex(), PR_INFLIGHT) - # 3. await a queue - htlc_log = await paysession.wait_for_one_htlc_to_resolve() # TODO maybe wait a bit, more failures might come - log.append(htlc_log) - if htlc_log.success: - if self.network.path_finder: - # TODO: report every route to liquidity hints for mpp - # in the case of success, we report channels of the - # route as being able to send the same amount in the future, - # as we assume to not know the capacity - self.network.path_finder.update_liquidity_hints(htlc_log.route, htlc_log.amount_msat) - # remove inflight htlcs from liquidity hints - self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False) - return - # htlc failed - # if we get a tmp channel failure, it might work to split the amount and try more routes - # if we get a channel update, we might retry the same route and amount - route = htlc_log.route - sender_idx = htlc_log.sender_idx - failure_msg = htlc_log.failure_msg - if sender_idx is None: - raise PaymentFailure(failure_msg.code_name()) - erring_node_id = route[sender_idx].node_id - code, data = failure_msg.code, failure_msg.data - self.logger.info(f"UPDATE_FAIL_HTLC. code={repr(code)}. " - f"decoded_data={failure_msg.decode_data()}. data={data.hex()!r}") - self.logger.info(f"error reported by {erring_node_id.hex()}") - if code == OnionFailureCode.MPP_TIMEOUT: - raise PaymentFailure(failure_msg.code_name()) - # errors returned by the next trampoline. - if fwd_trampoline_onion and code in [ - OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, - OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON]: - raise failure_msg - # trampoline - if self.uses_trampoline(): - paysession.handle_failed_trampoline_htlc( - htlc_log=htlc_log, failure_msg=failure_msg) - else: - self.handle_error_code_from_failed_htlc( - route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat) + # 3. await a queue, collect resolved htlcs + htlc_log = await paysession.wait_for_one_htlc_to_resolve() + while True: + log.append(htlc_log) + if htlc_log.success: + if self.network.path_finder: + # TODO: report every route to liquidity hints for mpp + # in the case of success, we report channels of the + # route as being able to send the same amount in the future, + # as we assume to not know the capacity + self.network.path_finder.update_liquidity_hints(htlc_log.route, htlc_log.amount_msat) + # remove inflight htlcs from liquidity hints + self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False) + return + # htlc failed + # if we get a tmp channel failure, it might work to split the amount and try more routes + # if we get a channel update, we might retry the same route and amount + route = htlc_log.route + sender_idx = htlc_log.sender_idx + failure_msg = htlc_log.failure_msg + if sender_idx is None: + raise PaymentFailure(failure_msg.code_name()) + erring_node_id = route[sender_idx].node_id + code, data = failure_msg.code, failure_msg.data + self.logger.info(f"UPDATE_FAIL_HTLC. code={repr(code)}. " + f"decoded_data={failure_msg.decode_data()}. data={data.hex()!r}") + self.logger.info(f"error reported by {erring_node_id.hex()}") + if code == OnionFailureCode.MPP_TIMEOUT: + raise PaymentFailure(failure_msg.code_name()) + # errors returned by the next trampoline. + if fwd_trampoline_onion and code in [ + OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, + OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON]: + raise failure_msg + # trampoline + if self.uses_trampoline(): + paysession.handle_failed_trampoline_htlc( + htlc_log=htlc_log, failure_msg=failure_msg) + else: + self.handle_error_code_from_failed_htlc( + route=route, sender_idx=sender_idx, failure_msg=failure_msg, amount=htlc_log.amount_msat) + if paysession.number_htlcs_inflight < 1: + break + # wait a bit, more failures might come + try: + htlc_log = await util.wait_for2( + paysession.wait_for_one_htlc_to_resolve(), + timeout=paysession.TIMEOUT_WAIT_FOR_NEXT_RESOLVED_HTLC) + except asyncio.TimeoutError: + break + # max attempts or timeout if (attempts is not None and len(log) >= attempts) or (attempts is None and time.time() - paysession.start_time > self.PAYMENT_TIMEOUT): raise PaymentFailure('Giving up after %d attempts'%len(log))