diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index 9fc243710..903280014 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -1146,19 +1146,15 @@ class Peer(Logger): htlc_id=htlc_id) chan.receive_htlc(htlc, onion_packet) - - @log_exceptions - async def _maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *, - onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket): + def maybe_forward_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *, + onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket): # Forward HTLC - # FIXME: this is not robust to us going offline before payment is fulfilled # FIXME: there are critical safety checks MISSING here forwarding_enabled = self.network.config.get('lightning_forward_payments', False) if not forwarding_enabled: self.logger.info(f"forwarding is disabled. failing htlc.") reason = OnionRoutingFailureMessage(code=OnionFailureCode.PERMANENT_CHANNEL_FAILURE, data=b'') - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, False, reason dph = processed_onion.hop_data.per_hop next_chan = self.lnworker.get_channel_by_short_id(dph.short_channel_id) next_chan_scid = dph.short_channel_id @@ -1167,8 +1163,7 @@ class Peer(Logger): if next_chan is None: self.logger.info(f"cannot forward htlc. cannot find next_chan {next_chan_scid}") reason = OnionRoutingFailureMessage(code=OnionFailureCode.UNKNOWN_NEXT_PEER, data=b'') - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, False, reason outgoing_chan_upd = next_chan.get_outgoing_gossip_channel_update()[2:] outgoing_chan_upd_len = len(outgoing_chan_upd).to_bytes(2, byteorder="big") if not next_chan.can_send_update_add_htlc(): @@ -1176,25 +1171,21 @@ class Peer(Logger): f"chan state {next_chan.get_state()}, peer state: {next_chan.peer_state}") reason = OnionRoutingFailureMessage(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=outgoing_chan_upd_len+outgoing_chan_upd) - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, False, reason next_cltv_expiry = int.from_bytes(dph.outgoing_cltv_value, 'big') if htlc.cltv_expiry - next_cltv_expiry < NBLOCK_OUR_CLTV_EXPIRY_DELTA: reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_CLTV_EXPIRY, data=(htlc.cltv_expiry.to_bytes(4, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd)) - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, False, reason if htlc.cltv_expiry - lnutil.NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS <= local_height \ or next_cltv_expiry <= local_height: reason = OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_SOON, data=outgoing_chan_upd_len+outgoing_chan_upd) - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, False,reason if max(htlc.cltv_expiry, next_cltv_expiry) > local_height + lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: reason = OnionRoutingFailureMessage(code=OnionFailureCode.EXPIRY_TOO_FAR, data=b'') - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, False, reason next_amount_msat_htlc = int.from_bytes(dph.amt_to_forward, 'big') forwarding_fees = fee_for_edge_msat(forwarded_amount_msat=next_amount_msat_htlc, fee_base_msat=lnutil.OUR_FEE_BASE_MSAT, @@ -1203,13 +1194,10 @@ class Peer(Logger): reason = OnionRoutingFailureMessage(code=OnionFailureCode.FEE_INSUFFICIENT, data=(next_amount_msat_htlc.to_bytes(8, byteorder="big") + outgoing_chan_upd_len + outgoing_chan_upd)) - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return - + return False, False, reason self.logger.info(f'forwarding htlc to {next_chan.node_id}') next_htlc = UpdateAddHtlc(amount_msat=next_amount_msat_htlc, payment_hash=htlc.payment_hash, cltv_expiry=next_cltv_expiry, timestamp=int(time.time())) next_htlc = next_chan.add_htlc(next_htlc) - next_remote_ctn = next_chan.get_latest_ctn(REMOTE) next_peer.send_message( "update_add_htlc", channel_id=next_chan.channel_id, @@ -1219,52 +1207,37 @@ class Peer(Logger): payment_hash=next_htlc.payment_hash, onion_routing_packet=processed_onion.next_packet.to_bytes() ) - await next_peer.await_remote(next_chan, next_remote_ctn) - success, preimage, reason = await self.lnworker.await_payment(next_htlc.payment_hash) - if success: - await self._fulfill_htlc(chan, htlc.htlc_id, preimage) - self.logger.info("htlc forwarded successfully") - else: - # TODO: test this - self.logger.info(f"forwarded htlc has failed, {reason}") - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + return next_chan, next_htlc, None - @log_exceptions - async def _maybe_fulfill_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *, - onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket): + def maybe_fulfill_htlc(self, chan: Channel, htlc: UpdateAddHtlc, *, + onion_packet: OnionPacket, processed_onion: ProcessedOnionPacket): try: info = self.lnworker.get_payment_info(htlc.payment_hash) preimage = self.lnworker.get_preimage(htlc.payment_hash) except UnknownPaymentHash: reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, reason expected_received_msat = int(info.amount * 1000) if info.amount is not None else None if expected_received_msat is not None and \ not (expected_received_msat <= htlc.amount_msat <= 2 * expected_received_msat): reason = OnionRoutingFailureMessage(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, reason local_height = self.network.get_local_height() if local_height + MIN_FINAL_CLTV_EXPIRY_ACCEPTED > htlc.cltv_expiry: reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_EXPIRY_TOO_SOON, data=b'') - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, reason cltv_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.outgoing_cltv_value, byteorder="big") if cltv_from_onion != htlc.cltv_expiry: reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY, data=htlc.cltv_expiry.to_bytes(4, byteorder="big")) - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return + return False, reason amount_from_onion = int.from_bytes(processed_onion.hop_data.per_hop.amt_to_forward, byteorder="big") if amount_from_onion > htlc.amount_msat: reason = OnionRoutingFailureMessage(code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, data=htlc.amount_msat.to_bytes(8, byteorder="big")) - await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) - return - #self.network.trigger_callback('htlc_added', htlc, invoice, RECEIVED) - await self.lnworker.enable_htlc_settle.wait() - await self._fulfill_htlc(chan, htlc.htlc_id, preimage) + return False, reason + # all good + return preimage, None async def _fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes): self.logger.info(f"_fulfill_htlc. chan {chan.short_channel_id}. htlc_id {htlc_id}") diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 9749068c7..8b10cee55 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1348,19 +1348,38 @@ class LNWallet(LNWorker): payment_hash = htlc.payment_hash processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=peer.privkey) if processed_onion.are_we_final: - await peer._maybe_fulfill_htlc( + preimage, reason = peer.maybe_fulfill_htlc( chan=chan, htlc=htlc, onion_packet=onion_packet, processed_onion=processed_onion) + if preimage: + await self.enable_htlc_settle.wait() + await peer._fulfill_htlc(chan, htlc.htlc_id, preimage) + else: + await peer.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) else: # todo: if we are forwarding we need to test next peer's state # we should dissociate forwarding and fulfillment - await peer._maybe_forward_htlc( + next_chan, next_htlc, reason = peer.maybe_forward_htlc( chan=chan, htlc=htlc, onion_packet=onion_packet, processed_onion=processed_onion) + if not next_chan: + await self.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) + else: + next_peer = self.peers[next_chan.node_id] + next_remote_ctn = next_chan.get_latest_ctn(REMOTE) + await next_peer.await_remote(next_chan, next_remote_ctn) + success, preimage, reason = await self.await_payment(next_htlc.payment_hash) + if success: + await peer._fulfill_htlc(chan, htlc.htlc_id, preimage) + self.logger.info("htlc forwarded successfully") + else: + # TODO: test this + self.logger.info(f"forwarded htlc has failed, {reason}") + await peer.fail_htlc(chan, htlc.htlc_id, onion_packet, reason) done.add(htlc_id) # cleanup for htlc_id in done: