Browse Source

Trampoline routing:

- add support for trampoline forwarding
 - add regtest with trampoline payment
patch-4
ThomasV 4 years ago
parent
commit
cf818fe08c
  1. 10
      electrum/lnonion.py
  2. 113
      electrum/lnpeer.py
  3. 22
      electrum/lnworker.py
  4. 3
      electrum/tests/regtest.py
  5. 26
      electrum/tests/regtest/regtest.sh

10
electrum/lnonion.py

@ -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

113
electrum/lnpeer.py

@ -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,6 +1520,8 @@ 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 not is_trampoline:
if cltv_from_onion != htlc.cltv_expiry:
raise OnionRoutingFailure(
code=OnionFailureCode.FINAL_INCORRECT_CLTV_EXPIRY,
@ -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:

22
electrum/lnworker.py

@ -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:

3
electrum/tests/regtest.py

@ -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'])

26
electrum/tests/regtest/regtest.sh

@ -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

Loading…
Cancel
Save