electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 738411e32bacbb6d2859cbbc782634e3d5dff7d2
parent ba4d6bc8b34d17a998c011276ef57aab7f0c5106
Author: ThomasV <thomasv@electrum.org>
Date:   Wed,  3 Mar 2021 20:36:48 +0100

Trampoline forwarding:
 - fix regression in create_routes:
   fwd_trampoline_onion was not added to the tuple
 - fix onion structure for e2e
 - maybe_fulfill_htlc:
   check the mpp_status of the outer onion,
   return trampoline_onion to be forwarded

Diffstat:
Melectrum/lnpeer.py | 82++++++++++++++++++++++++++++++++++++++++----------------------------------------
Melectrum/lnworker.py | 12+++++++-----
Melectrum/trampoline.py | 35++++++++++++++++++++++-------------
3 files changed, 70 insertions(+), 59 deletions(-)

diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -1328,11 +1328,10 @@ class Peer(Logger): util.trigger_callback('htlc_added', chan, htlc, RECEIVED) def maybe_forward_htlc( - self, - *, + self, *, htlc: UpdateAddHtlc, - processed_onion: ProcessedOnionPacket, - ) -> Tuple[bytes, int]: + processed_onion: ProcessedOnionPacket) -> Tuple[bytes, int]: + # Forward HTLC # FIXME: there are critical safety checks MISSING here forwarding_enabled = self.network.config.get('lightning_forward_payments', False) @@ -1414,9 +1413,9 @@ class Peer(Logger): payload = trampoline_onion.hop_data.payload payment_hash = htlc.payment_hash + payment_secret = os.urandom(32) try: outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"] - payment_secret = payload["payment_data"]["payment_secret"] amt_to_forward = payload["amt_to_forward"]["amt_to_forward"] cltv_from_onion = payload["outgoing_cltv_value"]["outgoing_cltv_value"] if "invoice_features" in payload: @@ -1426,12 +1425,14 @@ class Peer(Logger): invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"] else: self.logger.info('forward_trampoline: end-to-end') - invoice_features = 0 + invoice_features = LnFeatures.BASIC_MPP_OPT next_trampoline_onion = trampoline_onion.next_packet except Exception as e: self.logger.exception('') raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00') + # these are the fee/cltv paid by the sender + # pay_to_node will raise if they are not sufficient trampoline_cltv_delta = htlc.cltv_expiry - cltv_from_onion trampoline_fee = htlc.amount_msat - amt_to_forward @@ -1447,7 +1448,7 @@ class Peer(Logger): r_tags=[], t_tags=[], invoice_features=invoice_features, - trampoline_onion=next_trampoline_onion, + fwd_trampoline_onion=next_trampoline_onion, trampoline_fee=trampoline_fee, trampoline_cltv_delta=trampoline_cltv_delta, attempts=1) @@ -1466,10 +1467,10 @@ class Peer(Logger): chan: Channel, htlc: UpdateAddHtlc, processed_onion: ProcessedOnionPacket, - is_trampoline: bool = False) -> Optional[bytes]: + is_trampoline: bool = False) -> Tuple[Optional[bytes], Optional[bytes]]: """As a final recipient of an HTLC, decide if we should fulfill it. - Returns the preimage if yes, or None. + Return (preimage, trampoline_onion_packet) with at most a single element not None """ def log_fail_reason(reason: str): self.logger.info(f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. " @@ -1518,15 +1519,6 @@ class Peer(Logger): code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT, data=htlc.amount_msat.to_bytes(8, byteorder="big")) - # if there is a trampoline_onion, perform the above checks on it - if processed_onion.trampoline_onion_packet: - return - - info = self.lnworker.get_payment_info(htlc.payment_hash) - if info is None: - log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}") - raise exc_incorrect_or_unknown_pd - preimage = self.lnworker.get_preimage(htlc.payment_hash) try: payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"] except: @@ -1535,7 +1527,26 @@ class Peer(Logger): log_fail_reason(f"'payment_secret' missing from onion") raise exc_incorrect_or_unknown_pd # TODO fail here if invoice has set PAYMENT_SECRET_REQ - else: + payment_secret_from_onion = None + + mpp_status = self.lnworker.add_received_htlc(chan.short_channel_id, htlc, total_msat) + if mpp_status is None: + return None, None + if mpp_status is False: + log_fail_reason(f"MPP_TIMEOUT") + raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') + assert mpp_status is True + + # if there is a trampoline_onion, maybe_fulfill_htlc will be called again + if processed_onion.trampoline_onion_packet: + return None, processed_onion.trampoline_onion_packet + + info = self.lnworker.get_payment_info(htlc.payment_hash) + if info is None: + log_fail_reason(f"no payment_info found for RHASH {htlc.payment_hash.hex()}") + raise exc_incorrect_or_unknown_pd + preimage = self.lnworker.get_preimage(htlc.payment_hash) + if payment_secret_from_onion: if payment_secret_from_onion != derive_payment_secret_from_payment_preimage(preimage): log_fail_reason(f'incorrect payment secret {payment_secret_from_onion.hex()} != {derive_payment_secret_from_payment_preimage(preimage).hex()}') raise exc_incorrect_or_unknown_pd @@ -1543,15 +1554,7 @@ class Peer(Logger): if not (invoice_msat is None or invoice_msat <= total_msat <= 2 * invoice_msat): log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}") raise exc_incorrect_or_unknown_pd - mpp_status = self.lnworker.add_received_htlc(chan.short_channel_id, htlc, total_msat) - if mpp_status is True: - self.logger.info(f"maybe_fulfill_htlc. will FULFILL HTLC: chan {chan.short_channel_id}. htlc={str(htlc)}") - return preimage - elif mpp_status is False: - log_fail_reason(f"MPP_TIMEOUT") - raise OnionRoutingFailure(code=OnionFailureCode.MPP_TIMEOUT, data=b'') - else: - return None + return preimage, None 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}") @@ -1835,16 +1838,14 @@ class Peer(Logger): unfulfilled.pop(htlc_id) def process_unfulfilled_htlc( - self, - *, + self, *, chan: Channel, htlc: UpdateAddHtlc, forwarding_info: Tuple[str, int], onion_packet_bytes: bytes, - onion_packet: OnionPacket, - ) -> Tuple[Optional[bytes], Union[bool, None, Tuple[str, int]], Optional[bytes]]: + onion_packet: OnionPacket) -> Tuple[Optional[bytes], Union[bool, None, Tuple[str, int]], Optional[bytes]]: """ - returns either preimage or fw_info or error_bytes or (None, None, None) + return (preimage, fw_info, error_bytes) with at most a single element that is not None raise an OnionRoutingFailure if we need to fail the htlc """ payment_hash = htlc.payment_hash @@ -1853,20 +1854,20 @@ class Peer(Logger): payment_hash=payment_hash, onion_packet_bytes=onion_packet_bytes) if processed_onion.are_we_final: - preimage = self.maybe_fulfill_htlc( + preimage, trampoline_onion_packet = self.maybe_fulfill_htlc( chan=chan, htlc=htlc, processed_onion=processed_onion) # trampoline forwarding - if not preimage and processed_onion.trampoline_onion_packet: + if trampoline_onion_packet: if not forwarding_info: trampoline_onion = self.process_onion_packet( - processed_onion.trampoline_onion_packet, + trampoline_onion_packet, payment_hash=htlc.payment_hash, onion_packet_bytes=onion_packet_bytes, is_trampoline=True) if trampoline_onion.are_we_final: - preimage = self.maybe_fulfill_htlc( + preimage, _ = self.maybe_fulfill_htlc( chan=chan, htlc=htlc, processed_onion=trampoline_onion, @@ -1907,12 +1908,11 @@ class Peer(Logger): def process_onion_packet( self, - onion_packet: OnionPacket, - *, + onion_packet: OnionPacket, *, payment_hash: bytes, onion_packet_bytes: bytes, - is_trampoline: bool = False, - ) -> ProcessedOnionPacket: + is_trampoline: bool = False) -> ProcessedOnionPacket: + failure_data = sha256(onion_packet_bytes) try: processed_onion = process_onion_packet( diff --git a/electrum/lnworker.py b/electrum/lnworker.py @@ -1059,11 +1059,11 @@ class LNWallet(LNWorker): invoice_features: int, attempts: int = 1, full_path: LNPaymentPath = None, - trampoline_onion=None, + fwd_trampoline_onion=None, trampoline_fee=None, trampoline_cltv_delta=None) -> None: - if trampoline_onion: + if fwd_trampoline_onion: # todo: compare to the fee of the actual route we found if trampoline_fee < 1000: raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'') @@ -1089,7 +1089,8 @@ class LNWallet(LNWorker): invoice_features=invoice_features, full_path=full_path, payment_hash=payment_hash, - payment_secret=payment_secret)) + payment_secret=payment_secret, + fwd_trampoline_onion=fwd_trampoline_onion)) # 2. send htlcs for route, amount_msat, total_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes: await self.pay_to_route( @@ -1301,6 +1302,7 @@ class LNWallet(LNWorker): invoice_features: int, payment_hash, payment_secret, + fwd_trampoline_onion=None, full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]: """Creates multiple routes for splitting a payment over the available @@ -1364,7 +1366,7 @@ class LNWallet(LNWorker): r_tags=r_tags, t_tags=t_tags, invoice_features=invoice_features, outgoing_channel=None, full_path=full_path) - routes = [(route, amount_msat, final_total_msat, min_cltv_expiry, payment_secret, None)] + routes = [(route, amount_msat, final_total_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion)] except NoPathFound: if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT): raise @@ -1438,7 +1440,7 @@ class LNWallet(LNWorker): r_tags=r_tags, t_tags=t_tags, invoice_features=invoice_features, outgoing_channel=channel, full_path=None) - routes.append((route, part_amount_msat, final_total_msat, min_cltv_expiry, payment_secret, None)) + routes.append((route, part_amount_msat, final_total_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion)) self.logger.info(f"found acceptable split configuration: {list(s[0].values())} rating: {s[1]}") break except NoPathFound: diff --git a/electrum/trampoline.py b/electrum/trampoline.py @@ -127,6 +127,7 @@ def create_trampoline_route( invoice_routing_info = encode_routing_info(r_tags) route[-1].invoice_routing_info = invoice_routing_info route[-1].invoice_features = invoice_features + route[-1].outgoing_node_id = invoice_pubkey else: if t_tag: pubkey, feebase, feerate, cltv = t_tag @@ -139,7 +140,7 @@ def create_trampoline_route( fee_proportional_millionths=feerate, cltv_expiry_delta=cltv, node_features=trampoline_features)) - # Fake edge (not part of actual route, needed by calc_hops_data) + # Final edge (not part of the route if payment is legacy, but eclair requires an encrypted blob) route.append( TrampolineEdge( start_node=route[-1].end_node, @@ -171,22 +172,30 @@ def create_trampoline_onion(*, route, amount_msat, final_cltv, total_msat, payme # detect trampoline hops. payment_path_pubkeys = [x.node_id for x in route] num_hops = len(payment_path_pubkeys) - for i in range(num_hops-1): + for i in range(num_hops): route_edge = route[i] - next_edge = route[i+1] assert route_edge.is_trampoline() - assert next_edge.is_trampoline() - hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id} - if route_edge.invoice_features: - hops_data[i].payload["invoice_features"] = {"invoice_features":route_edge.invoice_features} - if route_edge.invoice_routing_info: - hops_data[i].payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info} - # only for final, legacy - if i == num_hops - 2: - hops_data[i].payload["payment_data"] = { + payload = hops_data[i].payload + if i < num_hops - 1: + payload.pop('short_channel_id') + next_edge = route[i+1] + assert next_edge.is_trampoline() + hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id} + # only for final + if i == num_hops - 1: + payload["payment_data"] = { "payment_secret":payment_secret, - "total_msat": total_msat, + "total_msat": total_msat } + # legacy + if i == num_hops - 2 and route_edge.invoice_features: + payload["invoice_features"] = {"invoice_features":route_edge.invoice_features} + payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info} + payload["payment_data"] = { + "payment_secret":payment_secret, + "total_msat": total_msat + } + _logger.info(f'payload {i} {payload}') trampoline_session_key = os.urandom(32) trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True) return trampoline_onion, amount_msat, cltv