lnworker: use PaymentFeeBudget
- introduce PaymentFeeBudget, which contains limits for fee budget and cltv budget
- when splitting a payment,
- the fee budget is linearly distributed between the parts
- this resolves a FIXME in lnrouter ("FIXME in case of MPP")
- the cltv budget is simply copied
- we could also add other kinds of budgets later, e.g. for the num in-flight htlcs
- resolves TODO in lnworker ("todo: compare to the fee of the actual route we found")
This commit is contained in:
@@ -43,7 +43,7 @@ from .lnutil import (Outpoint, LocalConfig, RECEIVED, UpdateAddHtlc, ChannelConf
|
|||||||
RemoteMisbehaving, ShortChannelID,
|
RemoteMisbehaving, ShortChannelID,
|
||||||
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
|
IncompatibleLightningFeatures, derive_payment_secret_from_payment_preimage,
|
||||||
ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures)
|
ChannelType, LNProtocolWarning, validate_features, IncompatibleOrInsaneFeatures)
|
||||||
from .lnutil import FeeUpdate, channel_id_from_funding_tx
|
from .lnutil import FeeUpdate, channel_id_from_funding_tx, PaymentFeeBudget
|
||||||
from .lntransport import LNTransport, LNTransportBase
|
from .lntransport import LNTransport, LNTransportBase
|
||||||
from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg
|
from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg
|
||||||
from .interface import GracefulDisconnect
|
from .interface import GracefulDisconnect
|
||||||
@@ -1795,11 +1795,13 @@ class Peer(Logger):
|
|||||||
|
|
||||||
# these are the fee/cltv paid by the sender
|
# these are the fee/cltv paid by the sender
|
||||||
# pay_to_node will raise if they are not sufficient
|
# pay_to_node will raise if they are not sufficient
|
||||||
trampoline_cltv_delta = inc_cltv_abs - out_cltv_abs # cltv budget
|
|
||||||
total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"]
|
total_msat = outer_onion.hop_data.payload["payment_data"]["total_msat"]
|
||||||
trampoline_fee = total_msat - amt_to_forward
|
budget = PaymentFeeBudget(
|
||||||
self.logger.info(f'trampoline forwarding. fee_budget={trampoline_fee}')
|
fee_msat=total_msat - amt_to_forward,
|
||||||
self.logger.info(f'trampoline forwarding. cltv_budget={trampoline_cltv_delta}. (inc={inc_cltv_abs}. out={out_cltv_abs})')
|
cltv=inc_cltv_abs - out_cltv_abs,
|
||||||
|
)
|
||||||
|
self.logger.info(f'trampoline forwarding. budget={budget}')
|
||||||
|
self.logger.info(f'trampoline forwarding. {inc_cltv_abs=}, {out_cltv_abs=}')
|
||||||
# To convert abs vs rel cltvs, we need to guess blockheight used by original sender as "current blockheight".
|
# To convert abs vs rel cltvs, we need to guess blockheight used by original sender as "current blockheight".
|
||||||
# Blocks might have been mined since.
|
# Blocks might have been mined since.
|
||||||
# - if we skew towards the past, we decrease our own cltv_budget accordingly (which is ok)
|
# - if we skew towards the past, we decrease our own cltv_budget accordingly (which is ok)
|
||||||
@@ -1809,22 +1811,24 @@ class Peer(Logger):
|
|||||||
local_height_of_onion_creator = self.network.get_local_height() - 1
|
local_height_of_onion_creator = self.network.get_local_height() - 1
|
||||||
cltv_budget_for_rest_of_route = out_cltv_abs - local_height_of_onion_creator
|
cltv_budget_for_rest_of_route = out_cltv_abs - local_height_of_onion_creator
|
||||||
|
|
||||||
|
if budget.fee_msat < 1000:
|
||||||
|
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
||||||
|
if budget.cltv < 576:
|
||||||
|
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.lnworker.pay_to_node(
|
await self.lnworker.pay_to_node(
|
||||||
node_pubkey=outgoing_node_id,
|
node_pubkey=outgoing_node_id,
|
||||||
payment_hash=payment_hash,
|
payment_hash=payment_hash,
|
||||||
payment_secret=payment_secret,
|
payment_secret=payment_secret,
|
||||||
amount_to_pay=amt_to_forward,
|
amount_to_pay=amt_to_forward,
|
||||||
# FIXME this API (min_final_cltv_delta) is confusing. The value will be added to local_height
|
|
||||||
# to form the abs cltv used on the last edge on the path to the *next trampoline* node.
|
|
||||||
# We should rewrite pay_to_node to operate on a cltv-budget (and fee-budget).
|
|
||||||
min_final_cltv_delta=cltv_budget_for_rest_of_route,
|
min_final_cltv_delta=cltv_budget_for_rest_of_route,
|
||||||
r_tags=r_tags,
|
r_tags=r_tags,
|
||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
fwd_trampoline_onion=next_trampoline_onion,
|
fwd_trampoline_onion=next_trampoline_onion,
|
||||||
fwd_trampoline_fee=trampoline_fee,
|
budget=budget,
|
||||||
fwd_trampoline_cltv_delta=trampoline_cltv_delta,
|
attempts=1,
|
||||||
attempts=1)
|
)
|
||||||
except OnionRoutingFailure as e:
|
except OnionRoutingFailure as e:
|
||||||
raise
|
raise
|
||||||
except PaymentFailure as e:
|
except PaymentFailure as e:
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ from math import inf
|
|||||||
from .util import profiler, with_lock
|
from .util import profiler, with_lock
|
||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
|
from .lnutil import (NUM_MAX_EDGES_IN_PAYMENT_PATH, ShortChannelID, LnFeatures,
|
||||||
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE)
|
NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE, PaymentFeeBudget)
|
||||||
from .channel_db import ChannelDB, Policy, NodeInfo
|
from .channel_db import ChannelDB, Policy, NodeInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -111,7 +111,7 @@ class RouteEdge(PathEdge):
|
|||||||
if self.cltv_delta > 14 * 144:
|
if self.cltv_delta > 14 * 144:
|
||||||
return False
|
return False
|
||||||
total_fee = self.fee_for_edge(amount_msat)
|
total_fee = self.fee_for_edge(amount_msat)
|
||||||
if not is_fee_sane(total_fee, payment_amount_msat=amount_msat):
|
if total_fee > get_default_fee_budget_msat(invoice_amount_msat=amount_msat):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -138,38 +138,41 @@ LNPaymentRoute = Sequence[RouteEdge]
|
|||||||
LNPaymentTRoute = Sequence[TrampolineEdge]
|
LNPaymentTRoute = Sequence[TrampolineEdge]
|
||||||
|
|
||||||
|
|
||||||
def is_route_sane_to_use(route: LNPaymentRoute, *, amount_msat_for_dest: int, cltv_delta_for_dest: int) -> bool:
|
def is_route_within_budget(
|
||||||
|
route: LNPaymentRoute,
|
||||||
|
*,
|
||||||
|
budget: PaymentFeeBudget,
|
||||||
|
amount_msat_for_dest: int, # that final receiver gets
|
||||||
|
cltv_delta_for_dest: int, # that final receiver gets
|
||||||
|
) -> bool:
|
||||||
"""Run some sanity checks on the whole route, before attempting to use it.
|
"""Run some sanity checks on the whole route, before attempting to use it.
|
||||||
called when we are paying; so e.g. lower cltv is better
|
called when we are paying; so e.g. lower cltv is better
|
||||||
"""
|
"""
|
||||||
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
|
if len(route) > NUM_MAX_EDGES_IN_PAYMENT_PATH:
|
||||||
return False
|
return False
|
||||||
amt = amount_msat_for_dest
|
amt = amount_msat_for_dest
|
||||||
cltv_delta = cltv_delta_for_dest
|
cltv_cost_of_route = 0 # excluding cltv_delta_for_dest
|
||||||
for route_edge in reversed(route[1:]):
|
for route_edge in reversed(route[1:]):
|
||||||
if not route_edge.is_sane_to_use(amt): return False
|
if not route_edge.is_sane_to_use(amt): return False
|
||||||
amt += route_edge.fee_for_edge(amt)
|
amt += route_edge.fee_for_edge(amt)
|
||||||
cltv_delta += route_edge.cltv_delta
|
cltv_cost_of_route += route_edge.cltv_delta
|
||||||
total_fee = amt - amount_msat_for_dest
|
fee_cost = amt - amount_msat_for_dest
|
||||||
# TODO revise ad-hoc heuristics
|
# check against budget
|
||||||
if cltv_delta > NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
|
if cltv_cost_of_route > budget.cltv:
|
||||||
return False
|
return False
|
||||||
# FIXME in case of MPP, the fee checks are done independently for each part,
|
if fee_cost > budget.fee_msat:
|
||||||
# which is ok for the proportional checks but not for the absolute ones.
|
return False
|
||||||
# This is not that big of a deal though as we don't split into *too many* parts.
|
# sanity check
|
||||||
if not is_fee_sane(total_fee, payment_amount_msat=amount_msat_for_dest):
|
total_cltv_delta = cltv_cost_of_route + cltv_delta_for_dest
|
||||||
|
if total_cltv_delta > NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_fee_sane(fee_msat: int, *, payment_amount_msat: int) -> bool:
|
def get_default_fee_budget_msat(*, invoice_amount_msat: int) -> int:
|
||||||
# fees <= 5 sat are fine
|
|
||||||
if fee_msat <= 5_000:
|
|
||||||
return True
|
|
||||||
# fees <= 1 % of payment are fine
|
# fees <= 1 % of payment are fine
|
||||||
if 100 * fee_msat <= payment_amount_msat:
|
# fees <= 5 sat are fine
|
||||||
return True
|
return max(5_000, invoice_amount_msat // 100)
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class LiquidityHint:
|
class LiquidityHint:
|
||||||
|
|||||||
@@ -1637,3 +1637,19 @@ class OnionFailureCodeMetaFlag(IntFlag):
|
|||||||
UPDATE = 0x1000
|
UPDATE = 0x1000
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentFeeBudget(NamedTuple):
|
||||||
|
fee_msat: int
|
||||||
|
|
||||||
|
# The cltv budget covers the cost of route to get to the destination, but excluding the
|
||||||
|
# cltv-delta the destination wants for itself. (e.g. "min_final_cltv_delta" is excluded)
|
||||||
|
cltv: int # this is cltv-delta-like, no absolute heights here!
|
||||||
|
|
||||||
|
#num_htlc: int
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def default(cls, *, invoice_amount_msat: int) -> 'PaymentFeeBudget':
|
||||||
|
from .lnrouter import get_default_fee_budget_msat
|
||||||
|
return PaymentFeeBudget(
|
||||||
|
fee_msat=get_default_fee_budget_msat(invoice_amount_msat=invoice_amount_msat),
|
||||||
|
cltv=NBLOCK_CLTV_DELTA_TOO_FAR_INTO_FUTURE,
|
||||||
|
)
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ from .lnutil import (Outpoint, LNPeerAddr,
|
|||||||
UpdateAddHtlc, Direction, LnFeatures, ShortChannelID,
|
UpdateAddHtlc, Direction, LnFeatures, ShortChannelID,
|
||||||
HtlcLog, derive_payment_secret_from_payment_preimage,
|
HtlcLog, derive_payment_secret_from_payment_preimage,
|
||||||
NoPathFound, InvalidGossipMsg)
|
NoPathFound, InvalidGossipMsg)
|
||||||
from .lnutil import ln_compare_features, IncompatibleLightningFeatures
|
from .lnutil import ln_compare_features, IncompatibleLightningFeatures, PaymentFeeBudget
|
||||||
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
|
from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
|
||||||
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket
|
from .lnonion import decode_onion_error, OnionFailureCode, OnionRoutingFailure, OnionPacket
|
||||||
from .lnmsg import decode_msg
|
from .lnmsg import decode_msg
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_sane_to_use,
|
from .lnrouter import (RouteEdge, LNPaymentRoute, LNPaymentPath, is_route_within_budget,
|
||||||
NoChannelPolicy, LNPathInconsistent)
|
NoChannelPolicy, LNPathInconsistent)
|
||||||
from .address_synchronizer import TX_HEIGHT_LOCAL, TX_TIMESTAMP_INF
|
from .address_synchronizer import TX_HEIGHT_LOCAL, TX_TIMESTAMP_INF
|
||||||
from . import lnsweep
|
from . import lnsweep
|
||||||
@@ -662,7 +662,7 @@ class PaySession(Logger):
|
|||||||
initial_trampoline_fee_level: int,
|
initial_trampoline_fee_level: int,
|
||||||
invoice_features: int,
|
invoice_features: int,
|
||||||
r_tags,
|
r_tags,
|
||||||
min_final_cltv_delta: int, # delta for last edge (typically from invoice)
|
min_final_cltv_delta: int, # delta for last node (typically from invoice)
|
||||||
amount_to_pay: int, # total payment amount final receiver will get
|
amount_to_pay: int, # total payment amount final receiver will get
|
||||||
invoice_pubkey: bytes,
|
invoice_pubkey: bytes,
|
||||||
uses_trampoline: bool, # whether sender uses trampoline or gossip
|
uses_trampoline: bool, # whether sender uses trampoline or gossip
|
||||||
@@ -1419,6 +1419,7 @@ class LNWallet(LNWorker):
|
|||||||
f"using_trampoline={self.uses_trampoline()}. "
|
f"using_trampoline={self.uses_trampoline()}. "
|
||||||
f"invoice_features={invoice_features.get_names()}")
|
f"invoice_features={invoice_features.get_names()}")
|
||||||
self.set_invoice_status(key, PR_INFLIGHT)
|
self.set_invoice_status(key, PR_INFLIGHT)
|
||||||
|
budget = PaymentFeeBudget.default(invoice_amount_msat=amount_to_pay)
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
await self.pay_to_node(
|
await self.pay_to_node(
|
||||||
@@ -1431,7 +1432,9 @@ class LNWallet(LNWorker):
|
|||||||
invoice_features=invoice_features,
|
invoice_features=invoice_features,
|
||||||
attempts=attempts,
|
attempts=attempts,
|
||||||
full_path=full_path,
|
full_path=full_path,
|
||||||
channels=channels)
|
channels=channels,
|
||||||
|
budget=budget,
|
||||||
|
)
|
||||||
success = True
|
success = True
|
||||||
except PaymentFailure as e:
|
except PaymentFailure as e:
|
||||||
self.logger.info(f'payment failure: {e!r}')
|
self.logger.info(f'payment failure: {e!r}')
|
||||||
@@ -1462,17 +1465,13 @@ class LNWallet(LNWorker):
|
|||||||
attempts: int = None,
|
attempts: int = None,
|
||||||
full_path: LNPaymentPath = None,
|
full_path: LNPaymentPath = None,
|
||||||
fwd_trampoline_onion: OnionPacket = None,
|
fwd_trampoline_onion: OnionPacket = None,
|
||||||
fwd_trampoline_fee: int = None,
|
budget: PaymentFeeBudget,
|
||||||
fwd_trampoline_cltv_delta: int = None,
|
|
||||||
channels: Optional[Sequence[Channel]] = None,
|
channels: Optional[Sequence[Channel]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if fwd_trampoline_onion:
|
assert budget
|
||||||
# todo: compare to the fee of the actual route we found
|
assert budget.fee_msat >= 0, budget
|
||||||
if fwd_trampoline_fee < 1000:
|
assert budget.cltv >= 0, budget
|
||||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_FEE_INSUFFICIENT, data=b'')
|
|
||||||
if fwd_trampoline_cltv_delta < 576:
|
|
||||||
raise OnionRoutingFailure(code=OnionFailureCode.TRAMPOLINE_EXPIRY_TOO_SOON, data=b'')
|
|
||||||
|
|
||||||
payment_key = payment_hash + payment_secret
|
payment_key = payment_hash + payment_secret
|
||||||
assert payment_key not in self._paysessions
|
assert payment_key not in self._paysessions
|
||||||
@@ -1499,12 +1498,14 @@ class LNWallet(LNWorker):
|
|||||||
# 1. create a set of routes for remaining amount.
|
# 1. create a set of routes for remaining amount.
|
||||||
# note: path-finding runs in a separate thread so that we don't block the asyncio loop
|
# note: path-finding runs in a separate thread so that we don't block the asyncio loop
|
||||||
# graph updates might occur during the computation
|
# graph updates might occur during the computation
|
||||||
|
remaining_fee_budget_msat = (budget.fee_msat * amount_to_send) // amount_to_pay
|
||||||
routes = self.create_routes_for_payment(
|
routes = self.create_routes_for_payment(
|
||||||
paysession=paysession,
|
paysession=paysession,
|
||||||
amount_msat=amount_to_send,
|
amount_msat=amount_to_send,
|
||||||
full_path=full_path,
|
full_path=full_path,
|
||||||
fwd_trampoline_onion=fwd_trampoline_onion,
|
fwd_trampoline_onion=fwd_trampoline_onion,
|
||||||
channels=channels,
|
channels=channels,
|
||||||
|
budget=budget._replace(fee_msat=remaining_fee_budget_msat),
|
||||||
)
|
)
|
||||||
# 2. send htlcs
|
# 2. send htlcs
|
||||||
async for sent_htlc_info, cltv_delta, trampoline_onion in routes:
|
async for sent_htlc_info, cltv_delta, trampoline_onion in routes:
|
||||||
@@ -1815,6 +1816,7 @@ class LNWallet(LNWorker):
|
|||||||
fwd_trampoline_onion: OnionPacket = None,
|
fwd_trampoline_onion: OnionPacket = None,
|
||||||
full_path: LNPaymentPath = None,
|
full_path: LNPaymentPath = None,
|
||||||
channels: Optional[Sequence[Channel]] = None,
|
channels: Optional[Sequence[Channel]] = None,
|
||||||
|
budget: PaymentFeeBudget,
|
||||||
) -> AsyncGenerator[Tuple[SentHtlcInfo, int, Optional[OnionPacket]], None]:
|
) -> AsyncGenerator[Tuple[SentHtlcInfo, int, Optional[OnionPacket]], None]:
|
||||||
|
|
||||||
"""Creates multiple routes for splitting a payment over the available
|
"""Creates multiple routes for splitting a payment over the available
|
||||||
@@ -1853,7 +1855,7 @@ class LNWallet(LNWorker):
|
|||||||
try:
|
try:
|
||||||
if self.uses_trampoline():
|
if self.uses_trampoline():
|
||||||
per_trampoline_channel_amounts = defaultdict(list)
|
per_trampoline_channel_amounts = defaultdict(list)
|
||||||
# categorize by trampoline nodes for trampolin mpp construction
|
# categorize by trampoline nodes for trampoline mpp construction
|
||||||
for (chan_id, _), part_amounts_msat in sc.config.items():
|
for (chan_id, _), part_amounts_msat in sc.config.items():
|
||||||
chan = self.channels[chan_id]
|
chan = self.channels[chan_id]
|
||||||
for part_amount_msat in part_amounts_msat:
|
for part_amount_msat in part_amounts_msat:
|
||||||
@@ -1883,7 +1885,9 @@ class LNWallet(LNWorker):
|
|||||||
local_height=local_height,
|
local_height=local_height,
|
||||||
trampoline_fee_level=paysession.trampoline_fee_level,
|
trampoline_fee_level=paysession.trampoline_fee_level,
|
||||||
use_two_trampolines=paysession.use_two_trampolines,
|
use_two_trampolines=paysession.use_two_trampolines,
|
||||||
failed_routes=paysession.failed_trampoline_routes)
|
failed_routes=paysession.failed_trampoline_routes,
|
||||||
|
budget=budget._replace(fee_msat=budget.fee_msat // len(per_trampoline_channel_amounts)),
|
||||||
|
)
|
||||||
# node_features is only used to determine is_tlv
|
# node_features is only used to determine is_tlv
|
||||||
per_trampoline_secret = os.urandom(32)
|
per_trampoline_secret = os.urandom(32)
|
||||||
per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount
|
per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount
|
||||||
@@ -1930,7 +1934,7 @@ class LNWallet(LNWorker):
|
|||||||
channel = self.channels[chan_id]
|
channel = self.channels[chan_id]
|
||||||
route = await run_in_thread(
|
route = await run_in_thread(
|
||||||
partial(
|
partial(
|
||||||
self.create_route_for_payment,
|
self.create_route_for_single_htlc,
|
||||||
amount_msat=part_amount_msat,
|
amount_msat=part_amount_msat,
|
||||||
invoice_pubkey=paysession.invoice_pubkey,
|
invoice_pubkey=paysession.invoice_pubkey,
|
||||||
min_final_cltv_delta=paysession.min_final_cltv_delta,
|
min_final_cltv_delta=paysession.min_final_cltv_delta,
|
||||||
@@ -1938,6 +1942,7 @@ class LNWallet(LNWorker):
|
|||||||
invoice_features=paysession.invoice_features,
|
invoice_features=paysession.invoice_features,
|
||||||
my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,
|
my_sending_channels=[channel] if is_multichan_mpp else my_active_channels,
|
||||||
full_path=full_path,
|
full_path=full_path,
|
||||||
|
budget=budget._replace(fee_msat=budget.fee_msat // sc.config.number_parts()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
shi = SentHtlcInfo(
|
shi = SentHtlcInfo(
|
||||||
@@ -1959,15 +1964,17 @@ class LNWallet(LNWorker):
|
|||||||
raise NoPathFound()
|
raise NoPathFound()
|
||||||
|
|
||||||
@profiler
|
@profiler
|
||||||
def create_route_for_payment(
|
def create_route_for_single_htlc(
|
||||||
self, *,
|
self, *,
|
||||||
amount_msat: int,
|
amount_msat: int, # that final receiver gets
|
||||||
invoice_pubkey: bytes,
|
invoice_pubkey: bytes,
|
||||||
min_final_cltv_delta: int,
|
min_final_cltv_delta: int,
|
||||||
r_tags,
|
r_tags,
|
||||||
invoice_features: int,
|
invoice_features: int,
|
||||||
my_sending_channels: List[Channel],
|
my_sending_channels: List[Channel],
|
||||||
full_path: Optional[LNPaymentPath]) -> LNPaymentRoute:
|
full_path: Optional[LNPaymentPath],
|
||||||
|
budget: PaymentFeeBudget,
|
||||||
|
) -> LNPaymentRoute:
|
||||||
|
|
||||||
my_sending_aliases = set(chan.get_local_scid_alias() for chan in my_sending_channels)
|
my_sending_aliases = set(chan.get_local_scid_alias() for chan in my_sending_channels)
|
||||||
my_sending_channels = {chan.short_channel_id: chan for chan in my_sending_channels
|
my_sending_channels = {chan.short_channel_id: chan for chan in my_sending_channels
|
||||||
@@ -2022,9 +2029,10 @@ class LNWallet(LNWorker):
|
|||||||
raise NoPathFound() from e
|
raise NoPathFound() from e
|
||||||
if not route:
|
if not route:
|
||||||
raise NoPathFound()
|
raise NoPathFound()
|
||||||
# test sanity
|
if not is_route_within_budget(
|
||||||
if not is_route_sane_to_use(route, amount_msat_for_dest=amount_msat, cltv_delta_for_dest=min_final_cltv_delta):
|
route, budget=budget, amount_msat_for_dest=amount_msat, cltv_delta_for_dest=min_final_cltv_delta,
|
||||||
self.logger.info(f"rejecting insane route {route}")
|
):
|
||||||
|
self.logger.info(f"rejecting route (exceeds budget): {route=}. {budget=}")
|
||||||
raise NoPathFound()
|
raise NoPathFound()
|
||||||
assert len(route) > 0
|
assert len(route) > 0
|
||||||
if route[-1].end_node != invoice_pubkey:
|
if route[-1].end_node != invoice_pubkey:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from electrum.bitcoin import COIN, sha256
|
|||||||
from electrum.util import NetworkRetryManager, bfh, OldTaskGroup, EventListener, InvoiceError
|
from electrum.util import NetworkRetryManager, bfh, OldTaskGroup, EventListener, InvoiceError
|
||||||
from electrum.lnpeer import Peer
|
from electrum.lnpeer import Peer
|
||||||
from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
|
from electrum.lnutil import LNPeerAddr, Keypair, privkey_to_pubkey
|
||||||
from electrum.lnutil import PaymentFailure, LnFeatures, HTLCOwner
|
from electrum.lnutil import PaymentFailure, LnFeatures, HTLCOwner, PaymentFeeBudget
|
||||||
from electrum.lnchannel import ChannelState, PeerState, Channel
|
from electrum.lnchannel import ChannelState, PeerState, Channel
|
||||||
from electrum.lnrouter import LNPathFinder, PathEdge, LNPathInconsistent
|
from electrum.lnrouter import LNPathFinder, PathEdge, LNPathInconsistent
|
||||||
from electrum.channel_db import ChannelDB
|
from electrum.channel_db import ChannelDB
|
||||||
@@ -250,7 +250,9 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
|
|||||||
return [r async for r in self.create_routes_for_payment(
|
return [r async for r in self.create_routes_for_payment(
|
||||||
amount_msat=amount_msat,
|
amount_msat=amount_msat,
|
||||||
paysession=paysession,
|
paysession=paysession,
|
||||||
full_path=full_path)]
|
full_path=full_path,
|
||||||
|
budget=PaymentFeeBudget.default(invoice_amount_msat=amount_msat),
|
||||||
|
)]
|
||||||
|
|
||||||
get_payments = LNWallet.get_payments
|
get_payments = LNWallet.get_payments
|
||||||
get_payment_secret = LNWallet.get_payment_secret
|
get_payment_secret = LNWallet.get_payment_secret
|
||||||
@@ -265,7 +267,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
|
|||||||
htlc_failed = LNWallet.htlc_failed
|
htlc_failed = LNWallet.htlc_failed
|
||||||
save_preimage = LNWallet.save_preimage
|
save_preimage = LNWallet.save_preimage
|
||||||
get_preimage = LNWallet.get_preimage
|
get_preimage = LNWallet.get_preimage
|
||||||
create_route_for_payment = LNWallet.create_route_for_payment
|
create_route_for_single_htlc = LNWallet.create_route_for_single_htlc
|
||||||
create_routes_for_payment = LNWallet.create_routes_for_payment
|
create_routes_for_payment = LNWallet.create_routes_for_payment
|
||||||
_check_invoice = LNWallet._check_invoice
|
_check_invoice = LNWallet._check_invoice
|
||||||
pay_to_route = LNWallet.pay_to_route
|
pay_to_route = LNWallet.pay_to_route
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import random
|
|||||||
|
|
||||||
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence, Set
|
from typing import Mapping, DefaultDict, Tuple, Optional, Dict, List, Iterable, Sequence, Set
|
||||||
|
|
||||||
from .lnutil import LnFeatures
|
from .lnutil import LnFeatures, PaymentFeeBudget
|
||||||
from .lnonion import calc_hops_data_for_payment, new_onion_packet, OnionPacket
|
from .lnonion import calc_hops_data_for_payment, new_onion_packet, OnionPacket
|
||||||
from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_sane_to_use, LNPaymentTRoute
|
from .lnrouter import RouteEdge, TrampolineEdge, LNPaymentRoute, is_route_within_budget, LNPaymentTRoute
|
||||||
from .lnutil import NoPathFound, LNPeerAddr
|
from .lnutil import NoPathFound, LNPeerAddr
|
||||||
from . import constants
|
from . import constants
|
||||||
from .logging import get_logger
|
from .logging import get_logger
|
||||||
@@ -222,6 +222,7 @@ def create_trampoline_route(
|
|||||||
trampoline_fee_level: int,
|
trampoline_fee_level: int,
|
||||||
use_two_trampolines: bool,
|
use_two_trampolines: bool,
|
||||||
failed_routes: Iterable[Sequence[str]],
|
failed_routes: Iterable[Sequence[str]],
|
||||||
|
budget: PaymentFeeBudget,
|
||||||
) -> LNPaymentTRoute:
|
) -> LNPaymentTRoute:
|
||||||
# we decide whether to convert to a legacy payment
|
# we decide whether to convert to a legacy payment
|
||||||
is_legacy, invoice_trampolines = is_legacy_relay(invoice_features, r_tags)
|
is_legacy, invoice_trampolines = is_legacy_relay(invoice_features, r_tags)
|
||||||
@@ -268,12 +269,13 @@ def create_trampoline_route(
|
|||||||
# Also needed for fees for last TF!
|
# Also needed for fees for last TF!
|
||||||
_extend_trampoline_route(route, end_node=invoice_pubkey, trampoline_fee_level=trampoline_fee_level)
|
_extend_trampoline_route(route, end_node=invoice_pubkey, trampoline_fee_level=trampoline_fee_level)
|
||||||
# check that we can pay amount and fees
|
# check that we can pay amount and fees
|
||||||
if not is_route_sane_to_use(
|
if not is_route_within_budget(
|
||||||
route=route,
|
route=route,
|
||||||
|
budget=budget,
|
||||||
amount_msat_for_dest=amount_msat,
|
amount_msat_for_dest=amount_msat,
|
||||||
cltv_delta_for_dest=min_final_cltv_delta,
|
cltv_delta_for_dest=min_final_cltv_delta,
|
||||||
):
|
):
|
||||||
raise NoPathFound("We cannot afford to pay the fees.")
|
raise NoPathFound("route exceeds budget")
|
||||||
return route
|
return route
|
||||||
|
|
||||||
|
|
||||||
@@ -342,6 +344,7 @@ def create_trampoline_route_and_onion(
|
|||||||
trampoline_fee_level: int,
|
trampoline_fee_level: int,
|
||||||
use_two_trampolines: bool,
|
use_two_trampolines: bool,
|
||||||
failed_routes: Iterable[Sequence[str]],
|
failed_routes: Iterable[Sequence[str]],
|
||||||
|
budget: PaymentFeeBudget,
|
||||||
) -> Tuple[LNPaymentTRoute, OnionPacket, int, int]:
|
) -> Tuple[LNPaymentTRoute, OnionPacket, int, int]:
|
||||||
# create route for the trampoline_onion
|
# create route for the trampoline_onion
|
||||||
trampoline_route = create_trampoline_route(
|
trampoline_route = create_trampoline_route(
|
||||||
@@ -355,6 +358,7 @@ def create_trampoline_route_and_onion(
|
|||||||
trampoline_fee_level=trampoline_fee_level,
|
trampoline_fee_level=trampoline_fee_level,
|
||||||
use_two_trampolines=use_two_trampolines,
|
use_two_trampolines=use_two_trampolines,
|
||||||
failed_routes=failed_routes,
|
failed_routes=failed_routes,
|
||||||
|
budget=budget,
|
||||||
)
|
)
|
||||||
# compute onion and fees
|
# compute onion and fees
|
||||||
final_cltv_abs = local_height + min_final_cltv_delta
|
final_cltv_abs = local_height + min_final_cltv_delta
|
||||||
|
|||||||
Reference in New Issue
Block a user