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
This commit is contained in:
@@ -1328,11 +1328,10 @@ class Peer(Logger):
|
|||||||
util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
|
util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
|
||||||
|
|
||||||
def maybe_forward_htlc(
|
def maybe_forward_htlc(
|
||||||
self,
|
self, *,
|
||||||
*,
|
|
||||||
htlc: UpdateAddHtlc,
|
htlc: UpdateAddHtlc,
|
||||||
processed_onion: ProcessedOnionPacket,
|
processed_onion: ProcessedOnionPacket) -> Tuple[bytes, int]:
|
||||||
) -> Tuple[bytes, int]:
|
|
||||||
# Forward HTLC
|
# Forward HTLC
|
||||||
# FIXME: there are critical safety checks MISSING here
|
# FIXME: there are critical safety checks MISSING here
|
||||||
forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
|
forwarding_enabled = self.network.config.get('lightning_forward_payments', False)
|
||||||
@@ -1414,9 +1413,9 @@ class Peer(Logger):
|
|||||||
|
|
||||||
payload = trampoline_onion.hop_data.payload
|
payload = trampoline_onion.hop_data.payload
|
||||||
payment_hash = htlc.payment_hash
|
payment_hash = htlc.payment_hash
|
||||||
|
payment_secret = os.urandom(32)
|
||||||
try:
|
try:
|
||||||
outgoing_node_id = payload["outgoing_node_id"]["outgoing_node_id"]
|
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"]
|
amt_to_forward = payload["amt_to_forward"]["amt_to_forward"]
|
||||||
cltv_from_onion = payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
cltv_from_onion = payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
||||||
if "invoice_features" in payload:
|
if "invoice_features" in payload:
|
||||||
@@ -1426,12 +1425,14 @@ class Peer(Logger):
|
|||||||
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
||||||
else:
|
else:
|
||||||
self.logger.info('forward_trampoline: end-to-end')
|
self.logger.info('forward_trampoline: end-to-end')
|
||||||
invoice_features = 0
|
invoice_features = LnFeatures.BASIC_MPP_OPT
|
||||||
next_trampoline_onion = trampoline_onion.next_packet
|
next_trampoline_onion = trampoline_onion.next_packet
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.exception('')
|
self.logger.exception('')
|
||||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
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_cltv_delta = htlc.cltv_expiry - cltv_from_onion
|
||||||
trampoline_fee = htlc.amount_msat - amt_to_forward
|
trampoline_fee = htlc.amount_msat - amt_to_forward
|
||||||
|
|
||||||
@@ -1447,7 +1448,7 @@ class Peer(Logger):
|
|||||||
r_tags=[],
|
r_tags=[],
|
||||||
t_tags=[],
|
t_tags=[],
|
||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
trampoline_onion=next_trampoline_onion,
|
fwd_trampoline_onion=next_trampoline_onion,
|
||||||
trampoline_fee=trampoline_fee,
|
trampoline_fee=trampoline_fee,
|
||||||
trampoline_cltv_delta=trampoline_cltv_delta,
|
trampoline_cltv_delta=trampoline_cltv_delta,
|
||||||
attempts=1)
|
attempts=1)
|
||||||
@@ -1466,10 +1467,10 @@ class Peer(Logger):
|
|||||||
chan: Channel,
|
chan: Channel,
|
||||||
htlc: UpdateAddHtlc,
|
htlc: UpdateAddHtlc,
|
||||||
processed_onion: ProcessedOnionPacket,
|
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.
|
"""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):
|
def log_fail_reason(reason: str):
|
||||||
self.logger.info(f"maybe_fulfill_htlc. will FAIL HTLC: chan {chan.short_channel_id}. "
|
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,
|
code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
||||||
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
|
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:
|
try:
|
||||||
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
|
payment_secret_from_onion = processed_onion.hop_data.payload["payment_data"]["payment_secret"]
|
||||||
except:
|
except:
|
||||||
@@ -1535,7 +1527,26 @@ class Peer(Logger):
|
|||||||
log_fail_reason(f"'payment_secret' missing from onion")
|
log_fail_reason(f"'payment_secret' missing from onion")
|
||||||
raise exc_incorrect_or_unknown_pd
|
raise exc_incorrect_or_unknown_pd
|
||||||
# TODO fail here if invoice has set PAYMENT_SECRET_REQ
|
# 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):
|
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()}')
|
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
|
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):
|
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}")
|
log_fail_reason(f"total_msat={total_msat} too different from invoice_msat={invoice_msat}")
|
||||||
raise exc_incorrect_or_unknown_pd
|
raise exc_incorrect_or_unknown_pd
|
||||||
mpp_status = self.lnworker.add_received_htlc(chan.short_channel_id, htlc, total_msat)
|
return preimage, None
|
||||||
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
|
|
||||||
|
|
||||||
def fulfill_htlc(self, chan: Channel, htlc_id: int, preimage: bytes):
|
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}")
|
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)
|
unfulfilled.pop(htlc_id)
|
||||||
|
|
||||||
def process_unfulfilled_htlc(
|
def process_unfulfilled_htlc(
|
||||||
self,
|
self, *,
|
||||||
*,
|
|
||||||
chan: Channel,
|
chan: Channel,
|
||||||
htlc: UpdateAddHtlc,
|
htlc: UpdateAddHtlc,
|
||||||
forwarding_info: Tuple[str, int],
|
forwarding_info: Tuple[str, int],
|
||||||
onion_packet_bytes: bytes,
|
onion_packet_bytes: bytes,
|
||||||
onion_packet: OnionPacket,
|
onion_packet: OnionPacket) -> Tuple[Optional[bytes], Union[bool, None, Tuple[str, int]], Optional[bytes]]:
|
||||||
) -> 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
|
raise an OnionRoutingFailure if we need to fail the htlc
|
||||||
"""
|
"""
|
||||||
payment_hash = htlc.payment_hash
|
payment_hash = htlc.payment_hash
|
||||||
@@ -1853,20 +1854,20 @@ class Peer(Logger):
|
|||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
onion_packet_bytes=onion_packet_bytes)
|
onion_packet_bytes=onion_packet_bytes)
|
||||||
if processed_onion.are_we_final:
|
if processed_onion.are_we_final:
|
||||||
preimage = self.maybe_fulfill_htlc(
|
preimage, trampoline_onion_packet = self.maybe_fulfill_htlc(
|
||||||
chan=chan,
|
chan=chan,
|
||||||
htlc=htlc,
|
htlc=htlc,
|
||||||
processed_onion=processed_onion)
|
processed_onion=processed_onion)
|
||||||
# trampoline forwarding
|
# trampoline forwarding
|
||||||
if not preimage and processed_onion.trampoline_onion_packet:
|
if trampoline_onion_packet:
|
||||||
if not forwarding_info:
|
if not forwarding_info:
|
||||||
trampoline_onion = self.process_onion_packet(
|
trampoline_onion = self.process_onion_packet(
|
||||||
processed_onion.trampoline_onion_packet,
|
trampoline_onion_packet,
|
||||||
payment_hash=htlc.payment_hash,
|
payment_hash=htlc.payment_hash,
|
||||||
onion_packet_bytes=onion_packet_bytes,
|
onion_packet_bytes=onion_packet_bytes,
|
||||||
is_trampoline=True)
|
is_trampoline=True)
|
||||||
if trampoline_onion.are_we_final:
|
if trampoline_onion.are_we_final:
|
||||||
preimage = self.maybe_fulfill_htlc(
|
preimage, _ = self.maybe_fulfill_htlc(
|
||||||
chan=chan,
|
chan=chan,
|
||||||
htlc=htlc,
|
htlc=htlc,
|
||||||
processed_onion=trampoline_onion,
|
processed_onion=trampoline_onion,
|
||||||
@@ -1907,12 +1908,11 @@ class Peer(Logger):
|
|||||||
|
|
||||||
def process_onion_packet(
|
def process_onion_packet(
|
||||||
self,
|
self,
|
||||||
onion_packet: OnionPacket,
|
onion_packet: OnionPacket, *,
|
||||||
*,
|
|
||||||
payment_hash: bytes,
|
payment_hash: bytes,
|
||||||
onion_packet_bytes: bytes,
|
onion_packet_bytes: bytes,
|
||||||
is_trampoline: bool = False,
|
is_trampoline: bool = False) -> ProcessedOnionPacket:
|
||||||
) -> ProcessedOnionPacket:
|
|
||||||
failure_data = sha256(onion_packet_bytes)
|
failure_data = sha256(onion_packet_bytes)
|
||||||
try:
|
try:
|
||||||
processed_onion = process_onion_packet(
|
processed_onion = process_onion_packet(
|
||||||
|
|||||||
@@ -1059,11 +1059,11 @@ class LNWallet(LNWorker):
|
|||||||
invoice_features: int,
|
invoice_features: int,
|
||||||
attempts: int = 1,
|
attempts: int = 1,
|
||||||
full_path: LNPaymentPath = None,
|
full_path: LNPaymentPath = None,
|
||||||
trampoline_onion=None,
|
fwd_trampoline_onion=None,
|
||||||
trampoline_fee=None,
|
trampoline_fee=None,
|
||||||
trampoline_cltv_delta=None) -> None:
|
trampoline_cltv_delta=None) -> None:
|
||||||
|
|
||||||
if trampoline_onion:
|
if fwd_trampoline_onion:
|
||||||
# todo: compare to the fee of the actual route we found
|
# todo: compare to the fee of the actual route we found
|
||||||
if trampoline_fee < 1000:
|
if trampoline_fee < 1000:
|
||||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||||
@@ -1089,7 +1089,8 @@ class LNWallet(LNWorker):
|
|||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
full_path=full_path,
|
full_path=full_path,
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
payment_secret=payment_secret))
|
payment_secret=payment_secret,
|
||||||
|
fwd_trampoline_onion=fwd_trampoline_onion))
|
||||||
# 2. send htlcs
|
# 2. send htlcs
|
||||||
for route, amount_msat, total_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes:
|
for route, amount_msat, total_msat, cltv_delta, bucket_payment_secret, trampoline_onion in routes:
|
||||||
await self.pay_to_route(
|
await self.pay_to_route(
|
||||||
@@ -1301,6 +1302,7 @@ class LNWallet(LNWorker):
|
|||||||
invoice_features: int,
|
invoice_features: int,
|
||||||
payment_hash,
|
payment_hash,
|
||||||
payment_secret,
|
payment_secret,
|
||||||
|
fwd_trampoline_onion=None,
|
||||||
full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]:
|
full_path: LNPaymentPath = None) -> Sequence[Tuple[LNPaymentRoute, int]]:
|
||||||
|
|
||||||
"""Creates multiple routes for splitting a payment over the available
|
"""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,
|
r_tags=r_tags, t_tags=t_tags,
|
||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
outgoing_channel=None, full_path=full_path)
|
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:
|
except NoPathFound:
|
||||||
if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
|
if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
|
||||||
raise
|
raise
|
||||||
@@ -1438,7 +1440,7 @@ class LNWallet(LNWorker):
|
|||||||
r_tags=r_tags, t_tags=t_tags,
|
r_tags=r_tags, t_tags=t_tags,
|
||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
outgoing_channel=channel, full_path=None)
|
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]}")
|
self.logger.info(f"found acceptable split configuration: {list(s[0].values())} rating: {s[1]}")
|
||||||
break
|
break
|
||||||
except NoPathFound:
|
except NoPathFound:
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ def create_trampoline_route(
|
|||||||
invoice_routing_info = encode_routing_info(r_tags)
|
invoice_routing_info = encode_routing_info(r_tags)
|
||||||
route[-1].invoice_routing_info = invoice_routing_info
|
route[-1].invoice_routing_info = invoice_routing_info
|
||||||
route[-1].invoice_features = invoice_features
|
route[-1].invoice_features = invoice_features
|
||||||
|
route[-1].outgoing_node_id = invoice_pubkey
|
||||||
else:
|
else:
|
||||||
if t_tag:
|
if t_tag:
|
||||||
pubkey, feebase, feerate, cltv = t_tag
|
pubkey, feebase, feerate, cltv = t_tag
|
||||||
@@ -139,7 +140,7 @@ def create_trampoline_route(
|
|||||||
fee_proportional_millionths=feerate,
|
fee_proportional_millionths=feerate,
|
||||||
cltv_expiry_delta=cltv,
|
cltv_expiry_delta=cltv,
|
||||||
node_features=trampoline_features))
|
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(
|
route.append(
|
||||||
TrampolineEdge(
|
TrampolineEdge(
|
||||||
start_node=route[-1].end_node,
|
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.
|
# detect trampoline hops.
|
||||||
payment_path_pubkeys = [x.node_id for x in route]
|
payment_path_pubkeys = [x.node_id for x in route]
|
||||||
num_hops = len(payment_path_pubkeys)
|
num_hops = len(payment_path_pubkeys)
|
||||||
for i in range(num_hops-1):
|
for i in range(num_hops):
|
||||||
route_edge = route[i]
|
route_edge = route[i]
|
||||||
next_edge = route[i+1]
|
|
||||||
assert route_edge.is_trampoline()
|
assert route_edge.is_trampoline()
|
||||||
assert next_edge.is_trampoline()
|
payload = hops_data[i].payload
|
||||||
hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id}
|
if i < num_hops - 1:
|
||||||
if route_edge.invoice_features:
|
payload.pop('short_channel_id')
|
||||||
hops_data[i].payload["invoice_features"] = {"invoice_features":route_edge.invoice_features}
|
next_edge = route[i+1]
|
||||||
if route_edge.invoice_routing_info:
|
assert next_edge.is_trampoline()
|
||||||
hops_data[i].payload["invoice_routing_info"] = {"invoice_routing_info":route_edge.invoice_routing_info}
|
hops_data[i].payload["outgoing_node_id"] = {"outgoing_node_id":next_edge.node_id}
|
||||||
# only for final, legacy
|
# only for final
|
||||||
if i == num_hops - 2:
|
if i == num_hops - 1:
|
||||||
hops_data[i].payload["payment_data"] = {
|
payload["payment_data"] = {
|
||||||
"payment_secret":payment_secret,
|
"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_session_key = os.urandom(32)
|
||||||
trampoline_onion = new_onion_packet(payment_path_pubkeys, trampoline_session_key, hops_data, associated_data=payment_hash, trampoline=True)
|
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
|
return trampoline_onion, amount_msat, cltv
|
||||||
|
|||||||
Reference in New Issue
Block a user