Trampoline routing.
- trampoline is enabled by default in config, to prevent download of `gossip_db`.
(if disabled, `gossip_db` will be downloaded, regardless of the existence of channels)
- if trampoline is enabled:
- the wallet can only open channels with trampoline nodes
- already-existing channels with non-trampoline nodes are frozen for sending.
- there are two types of trampoline payments: legacy and end-to-end (e2e).
- we decide to perform legacy or e2e based on the invoice:
- we use trampoline_routing_opt in features to detect Eclair and Phoenix invoices
- we use trampoline_routing_hints to detect Electrum invoices
- when trying a legacy payment, we add a second trampoline to the path to preserve privacy.
(we fall back to a single trampoline if the payment fails for all trampolines)
- the trampoline list is hardcoded, it will remain so until `trampoline_routing_opt` feature flag is in INIT.
- there are currently only two nodes in the hardcoded list, it would be nice to have more.
- similar to Phoenix, we find the fee/cltv by trial-and-error.
- if there is a second trampoline in the path, we use the same fee for both.
- the final spec should add fee info in error messages, so we will be able to fine-tune fees
This commit is contained in:
@@ -40,8 +40,8 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04
|
||||
TRAMPOLINE_HOPS_DATA_SIZE = 400
|
||||
LEGACY_PER_HOP_FULL_SIZE = 65
|
||||
NUM_STREAM_BYTES = 2 * HOPS_DATA_SIZE
|
||||
PER_HOP_HMAC_SIZE = 32
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ class OnionPacket:
|
||||
|
||||
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
|
||||
assert len(public_key) == 33
|
||||
assert len(hops_data) == HOPS_DATA_SIZE
|
||||
assert len(hops_data) in [ HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE ]
|
||||
assert len(hmac) == PER_HOP_HMAC_SIZE
|
||||
self.version = 0
|
||||
self.public_key = public_key
|
||||
@@ -183,21 +183,21 @@ class OnionPacket:
|
||||
ret += self.public_key
|
||||
ret += self.hops_data
|
||||
ret += self.hmac
|
||||
if len(ret) != 1366:
|
||||
if len(ret) - 66 not in [ HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE ]:
|
||||
raise Exception('unexpected length {}'.format(len(ret)))
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, b: bytes):
|
||||
if len(b) != 1366:
|
||||
if len(b) - 66 not in [ HOPS_DATA_SIZE, TRAMPOLINE_HOPS_DATA_SIZE ]:
|
||||
raise Exception('unexpected length {}'.format(len(b)))
|
||||
version = b[0]
|
||||
if version != 0:
|
||||
raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
|
||||
return OnionPacket(
|
||||
public_key=b[1:34],
|
||||
hops_data=b[34:1334],
|
||||
hmac=b[1334:]
|
||||
hops_data=b[34:-32],
|
||||
hmac=b[-32:]
|
||||
)
|
||||
|
||||
|
||||
@@ -226,25 +226,26 @@ def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
|
||||
|
||||
|
||||
def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
|
||||
hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket:
|
||||
hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes, trampoline=False) -> OnionPacket:
|
||||
num_hops = len(payment_path_pubkeys)
|
||||
assert num_hops == len(hops_data)
|
||||
hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
|
||||
|
||||
filler = _generate_filler(b'rho', hops_data, hop_shared_secrets)
|
||||
data_size = TRAMPOLINE_HOPS_DATA_SIZE if trampoline else HOPS_DATA_SIZE
|
||||
filler = _generate_filler(b'rho', hops_data, hop_shared_secrets, data_size)
|
||||
next_hmac = bytes(PER_HOP_HMAC_SIZE)
|
||||
|
||||
# Our starting packet needs to be filled out with random bytes, we
|
||||
# generate some deterministically using the session private key.
|
||||
pad_key = get_bolt04_onion_key(b'pad', session_key)
|
||||
mix_header = generate_cipher_stream(pad_key, HOPS_DATA_SIZE)
|
||||
mix_header = generate_cipher_stream(pad_key, data_size)
|
||||
|
||||
# compute routing info and MAC for each hop
|
||||
for i in range(num_hops-1, -1, -1):
|
||||
rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
|
||||
mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
|
||||
hops_data[i].hmac = next_hmac
|
||||
stream_bytes = generate_cipher_stream(rho_key, HOPS_DATA_SIZE)
|
||||
stream_bytes = generate_cipher_stream(rho_key, data_size)
|
||||
hop_data_bytes = hops_data[i].to_bytes()
|
||||
mix_header = mix_header[:-len(hop_data_bytes)]
|
||||
mix_header = hop_data_bytes + mix_header
|
||||
@@ -283,21 +284,28 @@ def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int,
|
||||
# payloads, backwards from last hop (but excluding the first edge):
|
||||
for edge_index in range(len(route) - 1, 0, -1):
|
||||
route_edge = route[edge_index]
|
||||
is_trampoline = route_edge.is_trampoline()
|
||||
if is_trampoline:
|
||||
amt += route_edge.fee_for_edge(amt)
|
||||
cltv += route_edge.cltv_expiry_delta
|
||||
hop_payload = {
|
||||
"amt_to_forward": {"amt_to_forward": amt},
|
||||
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
||||
"short_channel_id": {"short_channel_id": route_edge.short_channel_id},
|
||||
}
|
||||
hops_data += [OnionHopsDataSingle(is_tlv_payload=route[edge_index-1].has_feature_varonion(),
|
||||
payload=hop_payload)]
|
||||
amt += route_edge.fee_for_edge(amt)
|
||||
cltv += route_edge.cltv_expiry_delta
|
||||
hops_data.append(
|
||||
OnionHopsDataSingle(
|
||||
is_tlv_payload=route[edge_index-1].has_feature_varonion(),
|
||||
payload=hop_payload))
|
||||
if not is_trampoline:
|
||||
amt += route_edge.fee_for_edge(amt)
|
||||
cltv += route_edge.cltv_expiry_delta
|
||||
hops_data.reverse()
|
||||
return hops_data, amt, cltv
|
||||
|
||||
|
||||
def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
|
||||
shared_secrets: Sequence[bytes]) -> bytes:
|
||||
shared_secrets: Sequence[bytes], data_size:int) -> bytes:
|
||||
num_hops = len(hops_data)
|
||||
|
||||
# generate filler that matches all but the last hop (no HMAC for last hop)
|
||||
@@ -308,16 +316,16 @@ def _generate_filler(key_type: bytes, hops_data: Sequence[OnionHopsDataSingle],
|
||||
|
||||
for i in range(0, num_hops-1): # -1, as last hop does not obfuscate
|
||||
# Sum up how many frames were used by prior hops.
|
||||
filler_start = HOPS_DATA_SIZE
|
||||
filler_start = data_size
|
||||
for hop_data in hops_data[:i]:
|
||||
filler_start -= len(hop_data.to_bytes())
|
||||
# The filler is the part dangling off of the end of the
|
||||
# routingInfo, so offset it from there, and use the current
|
||||
# hop's frame count as its size.
|
||||
filler_end = HOPS_DATA_SIZE + len(hops_data[i].to_bytes())
|
||||
filler_end = data_size + len(hops_data[i].to_bytes())
|
||||
|
||||
stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
|
||||
stream_bytes = generate_cipher_stream(stream_key, NUM_STREAM_BYTES)
|
||||
stream_bytes = generate_cipher_stream(stream_key, 2 * data_size)
|
||||
filler = xor_bytes(filler, stream_bytes[filler_start:filler_end])
|
||||
filler += bytes(filler_size - len(filler)) # right pad with zeroes
|
||||
|
||||
@@ -334,48 +342,59 @@ class ProcessedOnionPacket(NamedTuple):
|
||||
are_we_final: bool
|
||||
hop_data: OnionHopsDataSingle
|
||||
next_packet: OnionPacket
|
||||
trampoline_onion_packet: OnionPacket
|
||||
|
||||
|
||||
# TODO replay protection
|
||||
def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
|
||||
our_onion_private_key: bytes) -> ProcessedOnionPacket:
|
||||
def process_onion_packet(
|
||||
onion_packet: OnionPacket,
|
||||
associated_data: bytes,
|
||||
our_onion_private_key: bytes) -> 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)
|
||||
|
||||
# check message integrity
|
||||
mu_key = get_bolt04_onion_key(b'mu', shared_secret)
|
||||
calculated_mac = hmac_oneshot(mu_key, msg=onion_packet.hops_data+associated_data,
|
||||
digest=hashlib.sha256)
|
||||
calculated_mac = hmac_oneshot(
|
||||
mu_key, msg=onion_packet.hops_data+associated_data,
|
||||
digest=hashlib.sha256)
|
||||
if onion_packet.hmac != calculated_mac:
|
||||
raise InvalidOnionMac()
|
||||
|
||||
# peel an onion layer off
|
||||
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
|
||||
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
|
||||
stream_bytes = generate_cipher_stream(rho_key, 2 * HOPS_DATA_SIZE)
|
||||
padded_header = onion_packet.hops_data + bytes(HOPS_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)
|
||||
# trampoline
|
||||
trampoline_onion_packet = hop_data.payload.get('trampoline_onion_packet')
|
||||
if trampoline_onion_packet:
|
||||
top_version = trampoline_onion_packet.get('version')
|
||||
top_public_key = trampoline_onion_packet.get('public_key')
|
||||
top_hops_data = trampoline_onion_packet.get('hops_data')
|
||||
top_hops_data_fd = io.BytesIO(top_hops_data)
|
||||
top_hmac = trampoline_onion_packet.get('hmac')
|
||||
trampoline_onion_packet = OnionPacket(
|
||||
public_key=top_public_key,
|
||||
hops_data=top_hops_data_fd.read(TRAMPOLINE_HOPS_DATA_SIZE),
|
||||
hmac=top_hmac)
|
||||
# calc next ephemeral key
|
||||
blinding_factor = sha256(onion_packet.public_key + shared_secret)
|
||||
blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
|
||||
next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
|
||||
next_public_key = next_public_key_int.get_public_key_bytes()
|
||||
|
||||
hop_data = OnionHopsDataSingle.from_fd(next_hops_data_fd)
|
||||
next_onion_packet = OnionPacket(
|
||||
public_key=next_public_key,
|
||||
hops_data=next_hops_data_fd.read(HOPS_DATA_SIZE),
|
||||
hmac=hop_data.hmac
|
||||
)
|
||||
hmac=hop_data.hmac)
|
||||
if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
|
||||
# we are the destination / exit node
|
||||
are_we_final = True
|
||||
else:
|
||||
# we are an intermediate node; forwarding
|
||||
are_we_final = False
|
||||
return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet)
|
||||
return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet, trampoline_onion_packet)
|
||||
|
||||
|
||||
class FailedToDecodeOnionError(Exception): pass
|
||||
@@ -498,6 +517,8 @@ class OnionFailureCode(IntEnum):
|
||||
EXPIRY_TOO_FAR = 21
|
||||
INVALID_ONION_PAYLOAD = PERM | 22
|
||||
MPP_TIMEOUT = 23
|
||||
TRAMPOLINE_FEE_INSUFFICIENT = NODE | 51
|
||||
TRAMPOLINE_EXPIRY_TOO_SOON = NODE | 52
|
||||
|
||||
|
||||
# don't use these elsewhere, the names are ambiguous without context
|
||||
|
||||
Reference in New Issue
Block a user