Fix: For inner trampoline onions amt_to_forward can be larger than the htlc amount
Add unittest to TestPeerForwarding which sends a multi trampoline payment. Wait another htlc_switch iteration in tests because trampolines might have different delays
This commit is contained in:
@@ -2082,15 +2082,17 @@ class Peer(Logger, EventListener):
|
|||||||
chan.receive_htlc(htlc, onion_packet)
|
chan.receive_htlc(htlc, onion_packet)
|
||||||
util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
|
util.trigger_callback('htlc_added', chan, htlc, RECEIVED)
|
||||||
|
|
||||||
def check_accepted_htlc(
|
@staticmethod
|
||||||
self, *,
|
def _check_accepted_final_htlc(
|
||||||
chan: Channel,
|
*, chan: Channel,
|
||||||
htlc: UpdateAddHtlc,
|
htlc: UpdateAddHtlc,
|
||||||
processed_onion: ProcessedOnionPacket,
|
processed_onion: ProcessedOnionPacket,
|
||||||
|
is_trampoline_onion: bool = False,
|
||||||
log_fail_reason: Callable[[str], None],
|
log_fail_reason: Callable[[str], None],
|
||||||
) -> tuple[bytes, int, int, OnionRoutingFailure]:
|
) -> tuple[bytes, int, int, OnionRoutingFailure]:
|
||||||
"""
|
"""
|
||||||
Perform checks that are invariant (results do not depend on height, network conditions, etc).
|
Perform checks that are invariant (results do not depend on height, network conditions, etc.)
|
||||||
|
for htlcs of which we are the receiver (forwarding htlcs will have their checks in maybe_forward_htlc).
|
||||||
May raise OnionRoutingFailure
|
May raise OnionRoutingFailure
|
||||||
"""
|
"""
|
||||||
assert processed_onion.are_we_final, processed_onion
|
assert processed_onion.are_we_final, processed_onion
|
||||||
@@ -2120,11 +2122,13 @@ class Peer(Logger, EventListener):
|
|||||||
else:
|
else:
|
||||||
channel_opening_fee = 0
|
channel_opening_fee = 0
|
||||||
|
|
||||||
if amt_to_forward > htlc.amount_msat:
|
if not is_trampoline_onion:
|
||||||
log_fail_reason(f"amt_to_forward != htlc.amount_msat")
|
# for inner trampoline onions amt_to_forward can be larger than the htlc amount
|
||||||
raise OnionRoutingFailure(
|
if amt_to_forward > htlc.amount_msat:
|
||||||
code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
log_fail_reason(f"{amt_to_forward=} > {htlc.amount_msat=}")
|
||||||
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
|
raise OnionRoutingFailure(
|
||||||
|
code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
||||||
|
data=htlc.amount_msat.to_bytes(8, byteorder="big"))
|
||||||
|
|
||||||
if (payment_secret_from_onion := processed_onion.payment_secret) is None:
|
if (payment_secret_from_onion := processed_onion.payment_secret) is None:
|
||||||
log_fail_reason(f"'payment_secret' missing from onion")
|
log_fail_reason(f"'payment_secret' missing from onion")
|
||||||
@@ -2166,6 +2170,7 @@ class Peer(Logger, EventListener):
|
|||||||
chan: Channel,
|
chan: Channel,
|
||||||
htlc: UpdateAddHtlc,
|
htlc: UpdateAddHtlc,
|
||||||
processed_onion: ProcessedOnionPacket,
|
processed_onion: ProcessedOnionPacket,
|
||||||
|
outer_onion_payment_secret: bytes = None, # used to group trampoline htlcs for forwarding
|
||||||
onion_packet_bytes: bytes,
|
onion_packet_bytes: bytes,
|
||||||
already_forwarded: bool = False,
|
already_forwarded: bool = False,
|
||||||
) -> Tuple[Optional[bytes], Optional[Tuple[str, Callable[[], Awaitable[Optional[str]]]]]]:
|
) -> Tuple[Optional[bytes], Optional[Tuple[str, Callable[[], Awaitable[Optional[str]]]]]]:
|
||||||
@@ -2199,10 +2204,11 @@ class Peer(Logger, EventListener):
|
|||||||
local_height = chain.height()
|
local_height = chain.height()
|
||||||
|
|
||||||
# parse parameters and perform checks that are invariant
|
# parse parameters and perform checks that are invariant
|
||||||
payment_secret_from_onion, total_msat, channel_opening_fee, exc_incorrect_or_unknown_pd = self.check_accepted_htlc(
|
payment_secret_from_onion, total_msat, channel_opening_fee, exc_incorrect_or_unknown_pd = self._check_accepted_final_htlc(
|
||||||
chan=chan,
|
chan=chan,
|
||||||
htlc=htlc,
|
htlc=htlc,
|
||||||
processed_onion=processed_onion,
|
processed_onion=processed_onion,
|
||||||
|
is_trampoline_onion=bool(outer_onion_payment_secret),
|
||||||
log_fail_reason=log_fail_reason)
|
log_fail_reason=log_fail_reason)
|
||||||
|
|
||||||
# payment key for final onions
|
# payment key for final onions
|
||||||
@@ -2244,6 +2250,7 @@ class Peer(Logger, EventListener):
|
|||||||
chan=chan,
|
chan=chan,
|
||||||
htlc=htlc,
|
htlc=htlc,
|
||||||
processed_onion=trampoline_onion,
|
processed_onion=trampoline_onion,
|
||||||
|
outer_onion_payment_secret=payment_secret_from_onion,
|
||||||
onion_packet_bytes=onion_packet_bytes,
|
onion_packet_bytes=onion_packet_bytes,
|
||||||
already_forwarded=already_forwarded,
|
already_forwarded=already_forwarded,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2022,6 +2022,9 @@ class TestPeerForwarding(TestPeer):
|
|||||||
async def pay(lnaddr, pay_req):
|
async def pay(lnaddr, pay_req):
|
||||||
self.assertEqual(PR_UNPAID, dest_w.get_payment_status(lnaddr.paymenthash))
|
self.assertEqual(PR_UNPAID, dest_w.get_payment_status(lnaddr.paymenthash))
|
||||||
result, log = await sender_w.pay_invoice(pay_req, attempts=attempts)
|
result, log = await sender_w.pay_invoice(pay_req, attempts=attempts)
|
||||||
|
async with OldTaskGroup() as g:
|
||||||
|
for peer in peers:
|
||||||
|
await g.spawn(peer.wait_one_htlc_switch_iteration())
|
||||||
async with OldTaskGroup() as g:
|
async with OldTaskGroup() as g:
|
||||||
for peer in peers:
|
for peer in peers:
|
||||||
await g.spawn(peer.wait_one_htlc_switch_iteration())
|
await g.spawn(peer.wait_one_htlc_switch_iteration())
|
||||||
@@ -2122,6 +2125,33 @@ class TestPeerForwarding(TestPeer):
|
|||||||
await self._run_trampoline_payment(
|
await self._run_trampoline_payment(
|
||||||
graph, sender_name='alice', destination_name='edward',tf_names=('bob', 'dave'))
|
graph, sender_name='alice', destination_name='edward',tf_names=('bob', 'dave'))
|
||||||
|
|
||||||
|
async def test_multi_trampoline_payment(self):
|
||||||
|
"""
|
||||||
|
Alice splits her payment to Dave between two trampoline forwarding nodes Carol and Bob.
|
||||||
|
This should test Multi-Trampoline MPP:
|
||||||
|
https://github.com/lightning/bolts/blob/bc7a1a0bc97b2293e7f43dd8a06529e5fdcf7cd2/proposals/trampoline.md#multi-trampoline-mpp
|
||||||
|
"""
|
||||||
|
graph_definition = self.GRAPH_DEFINITIONS['square_graph']
|
||||||
|
# payment amount is 100_000_000 msat, size the channels so that alice must use both to succeed
|
||||||
|
graph_definition['alice']['channels']['bob']['local_balance_msat'] = int(100_000_000 * 0.75)
|
||||||
|
graph_definition['alice']['channels']['carol']['local_balance_msat'] = int(100_000_000 * 0.75)
|
||||||
|
g = self.prepare_chans_and_peers_in_graph(graph_definition)
|
||||||
|
w = g.workers['alice'], g.workers['carol'], g.workers['bob'], g.workers['dave']
|
||||||
|
alice_w, carol_w, bob_w, dave_w = w
|
||||||
|
|
||||||
|
alice_w.config.TEST_FORCE_MPP = True
|
||||||
|
bob_w.config.TEST_FORCE_MPP = True
|
||||||
|
carol_w.config.TEST_FORCE_MPP = True
|
||||||
|
dave_w.features |= LnFeatures.BASIC_MPP_OPT
|
||||||
|
|
||||||
|
with self.assertRaises(PaymentDone):
|
||||||
|
await self._run_trampoline_payment(
|
||||||
|
g,
|
||||||
|
sender_name='alice',
|
||||||
|
destination_name='dave',
|
||||||
|
tf_names=('bob', 'carol'),
|
||||||
|
attempts=30, # the default used in LNWallet.pay_invoice()
|
||||||
|
)
|
||||||
|
|
||||||
class TestPeerDirectAnchors(TestPeerDirect):
|
class TestPeerDirectAnchors(TestPeerDirect):
|
||||||
TEST_ANCHOR_CHANNELS = True
|
TEST_ANCHOR_CHANNELS = True
|
||||||
|
|||||||
Reference in New Issue
Block a user