Trampoline routing:
- add support for trampoline forwarding - add regtest with trampoline payment
This commit is contained in:
@@ -349,7 +349,8 @@ class ProcessedOnionPacket(NamedTuple):
|
||||
def process_onion_packet(
|
||||
onion_packet: OnionPacket,
|
||||
associated_data: bytes,
|
||||
our_onion_private_key: bytes) -> ProcessedOnionPacket:
|
||||
our_onion_private_key: bytes,
|
||||
is_trampoline=False) -> ProcessedOnionPacket:
|
||||
if not ecc.ECPubkey.is_pubkey_bytes(onion_packet.public_key):
|
||||
raise InvalidOnionPubkey()
|
||||
shared_secret = get_ecdh(our_onion_private_key, onion_packet.public_key)
|
||||
@@ -362,8 +363,9 @@ def process_onion_packet(
|
||||
raise InvalidOnionMac()
|
||||
# peel an onion layer off
|
||||
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
|
||||
stream_bytes = generate_cipher_stream(rho_key, 2 * HOPS_DATA_SIZE)
|
||||
padded_header = onion_packet.hops_data + bytes(HOPS_DATA_SIZE)
|
||||
data_size = TRAMPOLINE_HOPS_DATA_SIZE if is_trampoline else HOPS_DATA_SIZE
|
||||
stream_bytes = generate_cipher_stream(rho_key, 2 * data_size)
|
||||
padded_header = onion_packet.hops_data + bytes(data_size)
|
||||
next_hops_data = xor_bytes(padded_header, stream_bytes)
|
||||
next_hops_data_fd = io.BytesIO(next_hops_data)
|
||||
hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
|
||||
@@ -386,7 +388,7 @@ def process_onion_packet(
|
||||
next_public_key = next_public_key_int.get_public_key_bytes()
|
||||
next_onion_packet = OnionPacket(
|
||||
public_key=next_public_key,
|
||||
hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE),
|
||||
hops_data=next_hops_data_fd.read(data_size),
|
||||
hmac=hop_data.hmac)
|
||||
if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
|
||||
# we are the destination / exit node
|
||||
|
||||
@@ -1196,7 +1196,8 @@ class Peer(Logger):
|
||||
self.send_message("commitment_signed", channel_id=chan.channel_id, signature=sig_64, num_htlcs=len(htlc_sigs), htlc_signature=b"".join(htlc_sigs))
|
||||
|
||||
def pay(self, *, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
|
||||
payment_hash: bytes, min_final_cltv_expiry: int, payment_secret: bytes = None) -> UpdateAddHtlc:
|
||||
payment_hash: bytes, min_final_cltv_expiry: int,
|
||||
payment_secret: bytes = None, fwd_trampoline_onion=None) -> UpdateAddHtlc:
|
||||
assert amount_msat > 0, "amount_msat is not greater zero"
|
||||
assert len(route) > 0
|
||||
if not chan.can_send_update_add_htlc():
|
||||
@@ -1227,6 +1228,25 @@ class Peer(Logger):
|
||||
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:
|
||||
self.logger.info(f'adding payment secret for legacy trampoline')
|
||||
hops_data[i].payload["payment_data"] = {
|
||||
"payment_secret":payment_secret,
|
||||
"total_msat": amount_msat,
|
||||
}
|
||||
|
||||
# if we are forwarding a trampoline payment, add trampoline onion
|
||||
if fwd_trampoline_onion:
|
||||
self.logger.info(f'adding trampoline onion to final payload')
|
||||
trampoline_payload = hops_data[num_hops-2].payload
|
||||
trampoline_payload["trampoline_onion_packet"] = {
|
||||
"version": fwd_trampoline_onion.version,
|
||||
"public_key": fwd_trampoline_onion.public_key,
|
||||
"hops_data": fwd_trampoline_onion.hops_data,
|
||||
"hmac": fwd_trampoline_onion.hmac
|
||||
}
|
||||
|
||||
# create trampoline onion
|
||||
for i in range(num_hops):
|
||||
route_edge = route[i]
|
||||
@@ -1424,6 +1444,62 @@ class Peer(Logger):
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, data=data)
|
||||
return next_chan_scid, next_htlc.htlc_id
|
||||
|
||||
def maybe_forward_trampoline(
|
||||
self, *,
|
||||
chan: Channel,
|
||||
htlc: UpdateAddHtlc,
|
||||
trampoline_onion: ProcessedOnionPacket):
|
||||
|
||||
payload = trampoline_onion.hop_data.payload
|
||||
payment_hash = htlc.payment_hash
|
||||
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:
|
||||
self.logger.info('forward_trampoline: legacy')
|
||||
next_trampoline_onion = None
|
||||
invoice_features = payload["invoice_features"]["invoice_features"]
|
||||
invoice_routing_info = payload["invoice_routing_info"]["invoice_routing_info"]
|
||||
else:
|
||||
self.logger.info('forward_trampoline: end-to-end')
|
||||
invoice_features = 0
|
||||
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')
|
||||
|
||||
trampoline_cltv_delta = htlc.cltv_expiry - cltv_from_onion
|
||||
trampoline_fee = htlc.amount_msat - amt_to_forward
|
||||
|
||||
@log_exceptions
|
||||
async def forward_trampoline_payment():
|
||||
try:
|
||||
await self.lnworker.pay_to_node(
|
||||
node_pubkey=outgoing_node_id,
|
||||
payment_hash=payment_hash,
|
||||
payment_secret=payment_secret,
|
||||
amount_to_pay=amt_to_forward,
|
||||
min_cltv_expiry=cltv_from_onion,
|
||||
r_tags=[],
|
||||
t_tags=[],
|
||||
invoice_features=invoice_features,
|
||||
trampoline_onion=next_trampoline_onion,
|
||||
trampoline_fee=trampoline_fee,
|
||||
trampoline_cltv_delta=trampoline_cltv_delta,
|
||||
attempts=1)
|
||||
except OnionRoutingFailure as e:
|
||||
# FIXME: cannot use payment_hash as key
|
||||
self.lnworker.trampoline_forwarding_failures[payment_hash] = e
|
||||
except PaymentFailure as e:
|
||||
# FIXME: adapt the error code
|
||||
error_reason = OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||
self.lnworker.trampoline_forwarding_failures[payment_hash] = error_reason
|
||||
|
||||
asyncio.ensure_future(forward_trampoline_payment())
|
||||
|
||||
|
||||
def maybe_fulfill_htlc(
|
||||
self, *,
|
||||
chan: Channel,
|
||||
@@ -1444,10 +1520,12 @@ class Peer(Logger):
|
||||
cltv_from_onion = processed_onion.hop_data.payload["outgoing_cltv_value"]["outgoing_cltv_value"]
|
||||
except:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_PAYLOAD, data=b'\x00\x00\x00')
|
||||
if cltv_from_onion != htlc.cltv_expiry:
|
||||
raise OnionRoutingFailure(
|
||||
code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
|
||||
data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
|
||||
|
||||
if not is_trampoline:
|
||||
if cltv_from_onion != htlc.cltv_expiry:
|
||||
raise OnionRoutingFailure(
|
||||
code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
|
||||
data=htlc.cltv_expiry.to_bytes(4, byteorder="big"))
|
||||
try:
|
||||
amt_to_forward = processed_onion.hop_data.payload["amt_to_forward"]["amt_to_forward"]
|
||||
except:
|
||||
@@ -1462,6 +1540,10 @@ class Peer(Logger):
|
||||
code=OnionFailureCode.FINAL_INCORRECT_HTLC_AMOUNT,
|
||||
data=total_msat.to_bytes(8, byteorder="big"))
|
||||
|
||||
outgoing_node_id = processed_onion.hop_data.payload.get("outgoing_node_id")
|
||||
if is_trampoline and outgoing_node_id:
|
||||
return
|
||||
|
||||
# if there is a trampoline_onion, perform the above checks on it
|
||||
if processed_onion.trampoline_onion_packet:
|
||||
trampoline_onion = process_onion_packet(
|
||||
@@ -1787,6 +1869,27 @@ class Peer(Logger):
|
||||
chan=chan,
|
||||
htlc=htlc,
|
||||
processed_onion=processed_onion)
|
||||
# trampoline forwarding
|
||||
if not preimage and processed_onion.trampoline_onion_packet:
|
||||
if not forwarding_info:
|
||||
trampoline_onion = self.process_onion_packet(
|
||||
processed_onion.trampoline_onion_packet,
|
||||
htlc.payment_hash,
|
||||
onion_packet_bytes,
|
||||
is_trampoline=True)
|
||||
self.maybe_forward_trampoline(
|
||||
chan=chan,
|
||||
htlc=htlc,
|
||||
trampoline_onion=trampoline_onion)
|
||||
# we return True so that this code gets executed only once
|
||||
return None, True, None
|
||||
else:
|
||||
preimage = self.lnworker.get_preimage(payment_hash)
|
||||
error_reason = self.lnworker.trampoline_forwarding_failures.pop(payment_hash, None)
|
||||
if error_reason:
|
||||
self.logger.info(f'trampoline forwarding failure {error_reason}')
|
||||
raise error_reason
|
||||
|
||||
elif not forwarding_info:
|
||||
next_chan_id, next_htlc_id = self.maybe_forward_htlc(
|
||||
chan=chan,
|
||||
@@ -1810,10 +1913,14 @@ class Peer(Logger):
|
||||
return preimage, None, None
|
||||
return None, None, None
|
||||
|
||||
def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes):
|
||||
def process_onion_packet(self, onion_packet, payment_hash, onion_packet_bytes, is_trampoline=False):
|
||||
failure_data = sha256(onion_packet_bytes)
|
||||
try:
|
||||
processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
|
||||
processed_onion = process_onion_packet(
|
||||
onion_packet,
|
||||
associated_data=payment_hash,
|
||||
our_onion_private_key=self.privkey,
|
||||
is_trampoline=is_trampoline)
|
||||
except UnsupportedOnionPacketVersion:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.INVALID_ONION_VERSION, data=failure_data)
|
||||
except InvalidOnionPubkey:
|
||||
|
||||
@@ -660,6 +660,8 @@ class LNWallet(LNWorker):
|
||||
for payment_hash in self.get_payments(status='inflight').keys():
|
||||
self.set_invoice_status(payment_hash.hex(), PR_INFLIGHT)
|
||||
|
||||
self.trampoline_forwarding_failures = {} # todo: should be persisted
|
||||
|
||||
@property
|
||||
def channels(self) -> Mapping[bytes, Channel]:
|
||||
"""Returns a read-only copy of channels."""
|
||||
@@ -1063,8 +1065,16 @@ class LNWallet(LNWorker):
|
||||
|
||||
async def pay_to_node(
|
||||
self, node_pubkey, payment_hash, payment_secret, amount_to_pay,
|
||||
min_cltv_expiry, r_tags, t_tags, invoice_features, *, attempts: int = 1,
|
||||
full_path: LNPaymentPath = None):
|
||||
min_cltv_expiry, r_tags, t_tags, invoice_features, *,
|
||||
attempts: int = 1, full_path: LNPaymentPath=None,
|
||||
trampoline_onion=None, trampoline_fee=None, trampoline_cltv_delta=None):
|
||||
|
||||
if 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'')
|
||||
if trampoline_cltv_delta < 576:
|
||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||
|
||||
self.logs[payment_hash.hex()] = log = []
|
||||
amount_inflight = 0 # what we sent in htlcs
|
||||
@@ -1084,7 +1094,7 @@ class LNWallet(LNWorker):
|
||||
routes = [(route, amount_to_send)]
|
||||
# 2. send htlcs
|
||||
for route, amount_msat in routes:
|
||||
await self.pay_to_route(route, amount_msat, payment_hash, payment_secret, min_cltv_expiry)
|
||||
await self.pay_to_route(route, amount_msat, payment_hash, payment_secret, min_cltv_expiry, trampoline_onion)
|
||||
amount_inflight += amount_msat
|
||||
util.trigger_callback('invoice_status', self.wallet, payment_hash.hex())
|
||||
# 3. await a queue
|
||||
@@ -1101,7 +1111,7 @@ class LNWallet(LNWorker):
|
||||
self.handle_error_code_from_failed_htlc(htlc_log)
|
||||
|
||||
|
||||
async def pay_to_route(self, route: LNPaymentRoute, amount_msat:int, payment_hash:bytes, payment_secret:bytes, min_cltv_expiry:int):
|
||||
async def pay_to_route(self, route: LNPaymentRoute, amount_msat:int, payment_hash:bytes, payment_secret:bytes, min_cltv_expiry:int, trampoline_onion:bytes =None):
|
||||
# send a single htlc
|
||||
short_channel_id = route[0].short_channel_id
|
||||
chan = self.get_channel_by_short_id(short_channel_id)
|
||||
@@ -1115,7 +1125,8 @@ class LNWallet(LNWorker):
|
||||
amount_msat=amount_msat,
|
||||
payment_hash=payment_hash,
|
||||
min_final_cltv_expiry=min_cltv_expiry,
|
||||
payment_secret=payment_secret)
|
||||
payment_secret=payment_secret,
|
||||
fwd_trampoline_onion=trampoline_onion)
|
||||
self.htlc_routes[(payment_hash, short_channel_id, htlc.htlc_id)] = route
|
||||
util.trigger_callback('htlc_added', chan, htlc, SENT)
|
||||
|
||||
@@ -1383,6 +1394,7 @@ class LNWallet(LNWorker):
|
||||
channels = list(self.channels.values())
|
||||
scid_to_my_channels = {chan.short_channel_id: chan for chan in channels
|
||||
if chan.short_channel_id is not None}
|
||||
|
||||
blacklist = self.network.channel_blacklist.get_current_list()
|
||||
for private_route in r_tags:
|
||||
if len(private_route) == 0:
|
||||
|
||||
@@ -58,5 +58,8 @@ class TestLightningABC(TestLightning):
|
||||
def test_forwarding(self):
|
||||
self.run_shell(['forwarding'])
|
||||
|
||||
def test_trampoline(self):
|
||||
self.run_shell(['trampoline'])
|
||||
|
||||
def test_watchtower(self):
|
||||
self.run_shell(['watchtower'])
|
||||
|
||||
@@ -128,6 +128,32 @@ if [[ $1 == "forwarding" ]]; then
|
||||
$carol close_channel $chan2
|
||||
fi
|
||||
|
||||
if [[ $1 == "trampoline" ]]; then
|
||||
$alice stop
|
||||
$alice setconfig -o use_gossip False
|
||||
$alice daemon -d
|
||||
$alice load_wallet
|
||||
sleep 1
|
||||
$bob setconfig lightning_forward_payments true
|
||||
bob_node=$($bob nodeid)
|
||||
channel_id1=$($alice open_channel $bob_node 0.002 --push_amount 0.001)
|
||||
channel_id2=$($carol open_channel $bob_node 0.002 --push_amount 0.001)
|
||||
echo "mining 3 blocks"
|
||||
new_blocks 3
|
||||
sleep 10 # time for channelDB
|
||||
request=$($carol add_lightning_request 0.0001 -m "blah" | jq -r ".invoice")
|
||||
$alice lnpay --attempts=2 $request
|
||||
carol_balance=$($carol list_channels | jq -r '.[0].local_balance')
|
||||
echo "carol balance: $carol_balance"
|
||||
if [[ $carol_balance != 110000 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
chan1=$($alice list_channels | jq -r ".[0].channel_point")
|
||||
chan2=$($carol list_channels | jq -r ".[0].channel_point")
|
||||
$alice close_channel $chan1
|
||||
$carol close_channel $chan2
|
||||
fi
|
||||
|
||||
# alice sends two payments, then broadcast ctx after first payment.
|
||||
# thus, bob needs to redeem both to_local and to_remote
|
||||
|
||||
|
||||
Reference in New Issue
Block a user