mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-27 07:23:25 +00:00
use option varonion: actually use TLV payloads, and signal support
This commit is contained in:
parent
4b78bf94d4
commit
30bf32b34b
5 changed files with 64 additions and 28 deletions
|
@ -161,6 +161,9 @@ class OnionHopsDataSingle: # called HopData in lnd
|
||||||
assert len(ret.hmac) == PER_HOP_HMAC_SIZE
|
assert len(ret.hmac) == PER_HOP_HMAC_SIZE
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<OnionHopsDataSingle. is_tlv_payload={self.is_tlv_payload}. payload={self.payload}. hmac={self.hmac}>"
|
||||||
|
|
||||||
|
|
||||||
class OnionPacket:
|
class OnionPacket:
|
||||||
|
|
||||||
|
@ -265,22 +268,24 @@ def calc_hops_data_for_payment(route: 'LNPaymentRoute', amount_msat: int, final_
|
||||||
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
|
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
|
||||||
raise PaymentFailure(f"too long route ({len(route)} edges)")
|
raise PaymentFailure(f"too long route ({len(route)} edges)")
|
||||||
|
|
||||||
|
# payload that will be seen by the last hop:
|
||||||
amt = amount_msat
|
amt = amount_msat
|
||||||
cltv = final_cltv
|
cltv = final_cltv
|
||||||
hop_payload = {
|
hop_payload = {
|
||||||
"amt_to_forward": {"amt_to_forward": amt},
|
"amt_to_forward": {"amt_to_forward": amt},
|
||||||
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
||||||
"short_channel_id": {"short_channel_id": b"\x00" * 8}, # TODO omit if tlv
|
|
||||||
}
|
}
|
||||||
hops_data = [OnionHopsDataSingle(is_tlv_payload=False, # TODO
|
hops_data = [OnionHopsDataSingle(is_tlv_payload=route[-1].has_feature_varonion(),
|
||||||
payload=hop_payload)]
|
payload=hop_payload)]
|
||||||
for route_edge in reversed(route[1:]):
|
# 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]
|
||||||
hop_payload = {
|
hop_payload = {
|
||||||
"amt_to_forward": {"amt_to_forward": amt},
|
"amt_to_forward": {"amt_to_forward": amt},
|
||||||
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
"outgoing_cltv_value": {"outgoing_cltv_value": cltv},
|
||||||
"short_channel_id": {"short_channel_id": route_edge.short_channel_id},
|
"short_channel_id": {"short_channel_id": route_edge.short_channel_id},
|
||||||
}
|
}
|
||||||
hops_data += [OnionHopsDataSingle(is_tlv_payload=False, # TODO
|
hops_data += [OnionHopsDataSingle(is_tlv_payload=route[edge_index-1].has_feature_varonion(),
|
||||||
payload=hop_payload)]
|
payload=hop_payload)]
|
||||||
amt += route_edge.fee_for_edge(amt)
|
amt += route_edge.fee_for_edge(amt)
|
||||||
cltv += route_edge.cltv_expiry_delta
|
cltv += route_edge.cltv_expiry_delta
|
||||||
|
|
|
@ -1040,12 +1040,15 @@ class Peer(Logger):
|
||||||
def pay(self, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
|
def pay(self, route: 'LNPaymentRoute', chan: Channel, amount_msat: int,
|
||||||
payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
|
payment_hash: bytes, min_final_cltv_expiry: int) -> UpdateAddHtlc:
|
||||||
assert amount_msat > 0, "amount_msat is not greater zero"
|
assert amount_msat > 0, "amount_msat is not greater zero"
|
||||||
|
assert len(route) > 0
|
||||||
if not chan.can_send_update_add_htlc():
|
if not chan.can_send_update_add_htlc():
|
||||||
raise PaymentFailure("Channel cannot send update_add_htlc")
|
raise PaymentFailure("Channel cannot send update_add_htlc")
|
||||||
|
# add features learned during "init" for direct neighbour:
|
||||||
|
route[0].node_features |= self.features
|
||||||
local_height = self.network.get_local_height()
|
local_height = self.network.get_local_height()
|
||||||
# create onion packet
|
# create onion packet
|
||||||
final_cltv = local_height + min_final_cltv_expiry
|
final_cltv = local_height + min_final_cltv_expiry
|
||||||
hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv) # TODO varonion
|
hops_data, amount_msat, cltv = calc_hops_data_for_payment(route, amount_msat, final_cltv)
|
||||||
assert final_cltv <= cltv, (final_cltv, cltv)
|
assert final_cltv <= cltv, (final_cltv, cltv)
|
||||||
secret_key = os.urandom(32)
|
secret_key = os.urandom(32)
|
||||||
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
|
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
|
||||||
|
@ -1055,7 +1058,8 @@ class Peer(Logger):
|
||||||
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
|
htlc = UpdateAddHtlc(amount_msat=amount_msat, payment_hash=payment_hash, cltv_expiry=cltv, timestamp=int(time.time()))
|
||||||
htlc = chan.add_htlc(htlc)
|
htlc = chan.add_htlc(htlc)
|
||||||
chan.set_onion_key(htlc.htlc_id, secret_key)
|
chan.set_onion_key(htlc.htlc_id, secret_key)
|
||||||
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. htlc: {htlc}")
|
self.logger.info(f"starting payment. len(route)={len(route)}. route: {route}. "
|
||||||
|
f"htlc: {htlc}. hops_data={hops_data!r}")
|
||||||
self.send_message(
|
self.send_message(
|
||||||
"update_add_htlc",
|
"update_add_htlc",
|
||||||
channel_id=chan.channel_id,
|
channel_id=chan.channel_id,
|
||||||
|
|
|
@ -27,11 +27,13 @@ import queue
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set
|
from typing import Sequence, List, Tuple, Optional, Dict, NamedTuple, TYPE_CHECKING, Set
|
||||||
|
|
||||||
|
import attr
|
||||||
|
|
||||||
from .util import bh2u, profiler
|
from .util import bh2u, profiler
|
||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
from .lnutil import NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID
|
from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
|
||||||
from .channel_db import ChannelDB, Policy
|
NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE)
|
||||||
from .lnutil import NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE
|
from .channel_db import ChannelDB, Policy, NodeInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .lnchannel import Channel
|
from .lnchannel import Channel
|
||||||
|
@ -48,13 +50,15 @@ def fee_for_edge_msat(forwarded_amount_msat: int, fee_base_msat: int, fee_propor
|
||||||
+ (forwarded_amount_msat * fee_proportional_millionths // 1_000_000)
|
+ (forwarded_amount_msat * fee_proportional_millionths // 1_000_000)
|
||||||
|
|
||||||
|
|
||||||
class RouteEdge(NamedTuple):
|
@attr.s
|
||||||
|
class RouteEdge:
|
||||||
"""if you travel through short_channel_id, you will reach node_id"""
|
"""if you travel through short_channel_id, you will reach node_id"""
|
||||||
node_id: bytes
|
node_id = attr.ib(type=bytes, kw_only=True)
|
||||||
short_channel_id: ShortChannelID
|
short_channel_id = attr.ib(type=ShortChannelID, kw_only=True)
|
||||||
fee_base_msat: int
|
fee_base_msat = attr.ib(type=int, kw_only=True)
|
||||||
fee_proportional_millionths: int
|
fee_proportional_millionths = attr.ib(type=int, kw_only=True)
|
||||||
cltv_expiry_delta: int
|
cltv_expiry_delta = attr.ib(type=int, kw_only=True)
|
||||||
|
node_features = attr.ib(type=int, kw_only=True) # note: for end node!
|
||||||
|
|
||||||
def fee_for_edge(self, amount_msat: int) -> int:
|
def fee_for_edge(self, amount_msat: int) -> int:
|
||||||
return fee_for_edge_msat(forwarded_amount_msat=amount_msat,
|
return fee_for_edge_msat(forwarded_amount_msat=amount_msat,
|
||||||
|
@ -63,14 +67,16 @@ class RouteEdge(NamedTuple):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_channel_policy(cls, channel_policy: 'Policy',
|
def from_channel_policy(cls, channel_policy: 'Policy',
|
||||||
short_channel_id: bytes, end_node: bytes) -> 'RouteEdge':
|
short_channel_id: bytes, end_node: bytes, *,
|
||||||
|
node_info: Optional[NodeInfo]) -> 'RouteEdge':
|
||||||
assert isinstance(short_channel_id, bytes)
|
assert isinstance(short_channel_id, bytes)
|
||||||
assert type(end_node) is bytes
|
assert type(end_node) is bytes
|
||||||
return RouteEdge(end_node,
|
return RouteEdge(node_id=end_node,
|
||||||
ShortChannelID.normalize(short_channel_id),
|
short_channel_id=ShortChannelID.normalize(short_channel_id),
|
||||||
channel_policy.fee_base_msat,
|
fee_base_msat=channel_policy.fee_base_msat,
|
||||||
channel_policy.fee_proportional_millionths,
|
fee_proportional_millionths=channel_policy.fee_proportional_millionths,
|
||||||
channel_policy.cltv_expiry_delta)
|
cltv_expiry_delta=channel_policy.cltv_expiry_delta,
|
||||||
|
node_features=node_info.features if node_info else 0)
|
||||||
|
|
||||||
def is_sane_to_use(self, amount_msat: int) -> bool:
|
def is_sane_to_use(self, amount_msat: int) -> bool:
|
||||||
# TODO revise ad-hoc heuristics
|
# TODO revise ad-hoc heuristics
|
||||||
|
@ -82,6 +88,10 @@ class RouteEdge(NamedTuple):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def has_feature_varonion(self) -> bool:
|
||||||
|
features = self.node_features
|
||||||
|
return bool(features & LnFeatures.VAR_ONION_REQ or features & LnFeatures.VAR_ONION_OPT)
|
||||||
|
|
||||||
|
|
||||||
LNPaymentRoute = Sequence[RouteEdge]
|
LNPaymentRoute = Sequence[RouteEdge]
|
||||||
|
|
||||||
|
@ -154,7 +164,9 @@ class LNPathFinder(Logger):
|
||||||
if channel_policy.htlc_maximum_msat is not None and \
|
if channel_policy.htlc_maximum_msat is not None and \
|
||||||
payment_amt_msat > channel_policy.htlc_maximum_msat:
|
payment_amt_msat > channel_policy.htlc_maximum_msat:
|
||||||
return float('inf'), 0 # payment amount too large
|
return float('inf'), 0 # payment amount too large
|
||||||
route_edge = RouteEdge.from_channel_policy(channel_policy, short_channel_id, end_node)
|
node_info = self.channel_db.get_node_info_for_node_id(node_id=end_node)
|
||||||
|
route_edge = RouteEdge.from_channel_policy(channel_policy, short_channel_id, end_node,
|
||||||
|
node_info=node_info)
|
||||||
if not route_edge.is_sane_to_use(payment_amt_msat):
|
if not route_edge.is_sane_to_use(payment_amt_msat):
|
||||||
return float('inf'), 0 # thanks but no thanks
|
return float('inf'), 0 # thanks but no thanks
|
||||||
|
|
||||||
|
@ -268,6 +280,8 @@ class LNPathFinder(Logger):
|
||||||
my_channels=my_channels)
|
my_channels=my_channels)
|
||||||
if channel_policy is None:
|
if channel_policy is None:
|
||||||
raise NoChannelPolicy(short_channel_id)
|
raise NoChannelPolicy(short_channel_id)
|
||||||
route.append(RouteEdge.from_channel_policy(channel_policy, short_channel_id, node_id))
|
node_info = self.channel_db.get_node_info_for_node_id(node_id=node_id)
|
||||||
|
route.append(RouteEdge.from_channel_policy(channel_policy, short_channel_id, node_id,
|
||||||
|
node_info=node_info))
|
||||||
prev_node_id = node_id
|
prev_node_id = node_id
|
||||||
return route
|
return route
|
||||||
|
|
|
@ -8,8 +8,8 @@ import json
|
||||||
from collections import namedtuple, defaultdict
|
from collections import namedtuple, defaultdict
|
||||||
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
|
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
|
||||||
import re
|
import re
|
||||||
import attr
|
|
||||||
|
|
||||||
|
import attr
|
||||||
from aiorpcx import NetAddress
|
from aiorpcx import NetAddress
|
||||||
|
|
||||||
from .util import bfh, bh2u, inv_dict, UserFacingException
|
from .util import bfh, bh2u, inv_dict, UserFacingException
|
||||||
|
@ -838,6 +838,7 @@ LN_FEATURES_IMPLEMENTED = (
|
||||||
| LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
| LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
||||||
| LnFeatures.GOSSIP_QUERIES_OPT | LnFeatures.GOSSIP_QUERIES_REQ
|
| LnFeatures.GOSSIP_QUERIES_OPT | LnFeatures.GOSSIP_QUERIES_REQ
|
||||||
| LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
| LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
||||||
|
| LnFeatures.VAR_ONION_OPT | LnFeatures.VAR_ONION_REQ
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -150,6 +150,7 @@ class LNWorker(Logger):
|
||||||
self.features = LnFeatures(0)
|
self.features = LnFeatures(0)
|
||||||
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT
|
||||||
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT
|
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT
|
||||||
|
self.features |= LnFeatures.VAR_ONION_OPT
|
||||||
|
|
||||||
def channels_for_peer(self, node_id):
|
def channels_for_peer(self, node_id):
|
||||||
return {}
|
return {}
|
||||||
|
@ -1047,7 +1048,7 @@ class LNWallet(LNWorker):
|
||||||
return addr
|
return addr
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
def _create_route_from_invoice(self, decoded_invoice) -> LNPaymentRoute:
|
def _create_route_from_invoice(self, decoded_invoice: 'LnAddr') -> LNPaymentRoute:
|
||||||
amount_msat = int(decoded_invoice.amount * COIN * 1000)
|
amount_msat = int(decoded_invoice.amount * COIN * 1000)
|
||||||
invoice_pubkey = decoded_invoice.pubkey.serialize()
|
invoice_pubkey = decoded_invoice.pubkey.serialize()
|
||||||
# use 'r' field from invoice
|
# use 'r' field from invoice
|
||||||
|
@ -1091,8 +1092,13 @@ class LNWallet(LNWorker):
|
||||||
fee_base_msat = channel_policy.fee_base_msat
|
fee_base_msat = channel_policy.fee_base_msat
|
||||||
fee_proportional_millionths = channel_policy.fee_proportional_millionths
|
fee_proportional_millionths = channel_policy.fee_proportional_millionths
|
||||||
cltv_expiry_delta = channel_policy.cltv_expiry_delta
|
cltv_expiry_delta = channel_policy.cltv_expiry_delta
|
||||||
route.append(RouteEdge(node_pubkey, short_channel_id, fee_base_msat, fee_proportional_millionths,
|
node_info = self.channel_db.get_node_info_for_node_id(node_id=node_pubkey)
|
||||||
cltv_expiry_delta))
|
route.append(RouteEdge(node_id=node_pubkey,
|
||||||
|
short_channel_id=short_channel_id,
|
||||||
|
fee_base_msat=fee_base_msat,
|
||||||
|
fee_proportional_millionths=fee_proportional_millionths,
|
||||||
|
cltv_expiry_delta=cltv_expiry_delta,
|
||||||
|
node_features=node_info.features if node_info else 0))
|
||||||
prev_node_id = node_pubkey
|
prev_node_id = node_pubkey
|
||||||
# test sanity
|
# test sanity
|
||||||
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
|
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
|
||||||
|
@ -1111,6 +1117,11 @@ class LNWallet(LNWorker):
|
||||||
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
|
if not is_route_sane_to_use(route, amount_msat, decoded_invoice.get_min_final_cltv_expiry()):
|
||||||
self.logger.info(f"rejecting insane route {route}")
|
self.logger.info(f"rejecting insane route {route}")
|
||||||
raise NoPathFound()
|
raise NoPathFound()
|
||||||
|
assert len(route) > 0
|
||||||
|
assert route[-1].node_id == invoice_pubkey
|
||||||
|
# add features from invoice
|
||||||
|
invoice_features = decoded_invoice.get_tag('9') or 0
|
||||||
|
route[-1].node_features |= invoice_features
|
||||||
return route
|
return route
|
||||||
|
|
||||||
def add_request(self, amount_sat, message, expiry):
|
def add_request(self, amount_sat, message, expiry):
|
||||||
|
@ -1141,7 +1152,8 @@ class LNWallet(LNWorker):
|
||||||
lnaddr = LnAddr(payment_hash, amount_btc,
|
lnaddr = LnAddr(payment_hash, amount_btc,
|
||||||
tags=[('d', message),
|
tags=[('d', message),
|
||||||
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
|
('c', MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE),
|
||||||
('x', expiry)]
|
('x', expiry),
|
||||||
|
('9', self.features.for_invoice())]
|
||||||
+ routing_hints,
|
+ routing_hints,
|
||||||
date = timestamp)
|
date = timestamp)
|
||||||
invoice = lnencode(lnaddr, self.node_keypair.privkey)
|
invoice = lnencode(lnaddr, self.node_keypair.privkey)
|
||||||
|
|
Loading…
Add table
Reference in a new issue