lnworker: rework num_sats_can_receive and routing_hints_for_invoice
follow-up https://github.com/spesmilo/electrum/pull/7818 - note it matters whether a sender pays us end-to-end-trampoline or just via legacy - consider: Alice has 0.1 BTC recv cap in chan1 and 1 BTC recv cap in chan2, both with border-node T1 - if sender is paying e2e trampoline, it can realistically pay even ~1.1 BTC, as T1 can resplit the HTLCs - if sender is paying legacy, it will have a hard time trying to pay more than 1 BTC, in practice - although note if T1 has implemented non-strict-forwarding (see BOLT-04), achieving 1 BTC is easy, as T1 can redirect HTLCs (but cannot split them, in this case) - to make num_sats_can_receive realistic, it assumes the legacy case - To calc num_sats_can_receive, we sort our channels in decreasing order of receive-capacities, iterate over them and calculate a running sum - we stop adding channels when the next chan's recv cap is small compared to the running total. - When putting routing hints in an invoice, we do the same, with the added condition that we keep adding channels if their recv cap is larger than the invoice amount. - consider: Alice has 0.1 BTC recv cap in chan1 with Bob, and 1 BTC recv cap in chan2 with Carol - if Alice wants to recv 100 sats, it is useful to add hints for both channels into the invoice, for redundancy - if Alice wants to recv 0.9 BTC, it is questionable whether adding the smaller chan is useful - the code here won't add it
This commit is contained in:
@@ -92,8 +92,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
SAVED_PR_STATUS = [PR_PAID, PR_UNPAID, PR_SCHEDULED] # status that are persisted
|
SAVED_PR_STATUS = [PR_PAID, PR_UNPAID, PR_SCHEDULED] # status that are persisted
|
||||||
|
|
||||||
MPP_RECEIVE_CUTOFF = 0.2
|
|
||||||
|
|
||||||
NUM_PEERS_TARGET = 4
|
NUM_PEERS_TARGET = 4
|
||||||
|
|
||||||
# onchain channel backup data
|
# onchain channel backup data
|
||||||
@@ -1998,15 +1996,7 @@ class LNWallet(LNWorker):
|
|||||||
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int]):
|
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int]):
|
||||||
"""calculate routing hints (BOLT-11 'r' field)"""
|
"""calculate routing hints (BOLT-11 'r' field)"""
|
||||||
routing_hints = []
|
routing_hints = []
|
||||||
with self.lock:
|
channels = list(self.get_channels_to_include_in_invoice(amount_msat))
|
||||||
nodes = self.border_nodes_that_can_receive(amount_msat)
|
|
||||||
channels = []
|
|
||||||
for c in self.channels.values():
|
|
||||||
if c.node_id in nodes:
|
|
||||||
channels.append(c)
|
|
||||||
# cap max channels to include to keep QR code reasonably scannable
|
|
||||||
channels = sorted(channels, key=lambda chan: (not chan.is_active(), -chan.available_to_spend(REMOTE)))
|
|
||||||
channels = channels[:15]
|
|
||||||
random.shuffle(channels) # let's not leak channel order
|
random.shuffle(channels) # let's not leak channel order
|
||||||
scid_to_my_channels = {chan.short_channel_id: chan for chan in channels
|
scid_to_my_channels = {chan.short_channel_id: chan for chan in channels
|
||||||
if chan.short_channel_id is not None}
|
if chan.short_channel_id is not None}
|
||||||
@@ -2082,37 +2072,51 @@ class LNWallet(LNWorker):
|
|||||||
can_send_minus_fees = max(0, can_send_minus_fees)
|
can_send_minus_fees = max(0, can_send_minus_fees)
|
||||||
return Decimal(can_send_minus_fees) / 1000
|
return Decimal(can_send_minus_fees) / 1000
|
||||||
|
|
||||||
def border_nodes_that_can_receive(self, amount_msat=None):
|
def get_channels_to_include_in_invoice(self, amount_msat=None) -> Sequence[Channel]:
|
||||||
# if amount_msat is None, use the max amount we can receive
|
if not amount_msat: # assume we want to recv a large amt, e.g. finding max.
|
||||||
#
|
amount_msat = float('inf')
|
||||||
# Filter out nodes that have very low receive capacity compared to invoice amt.
|
|
||||||
# Even with MPP, below a certain threshold, including these channels probably
|
|
||||||
# hurts more than help, as they lead to many failed attempts for the sender.
|
|
||||||
#
|
|
||||||
# We condider nodes instead of channels because both non-strict forwardring
|
|
||||||
# and trampoline end-to-end payments allow it
|
|
||||||
nodes_that_can_receive = defaultdict(int)
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
for c in self.channels.values():
|
channels = list(self.channels.values())
|
||||||
if not c.is_active() or c.is_frozen_for_receiving():
|
# we exclude channels that cannot *right now* receive (e.g. peer offline)
|
||||||
continue
|
channels = [chan for chan in channels
|
||||||
nodes_that_can_receive[c.node_id] += c.available_to_spend(REMOTE)
|
if (chan.is_active() and not chan.is_frozen_for_receiving())]
|
||||||
while True:
|
# Filter out nodes that have low receive capacity compared to invoice amt.
|
||||||
max_can_receive = sum(nodes_that_can_receive.values())
|
# Even with MPP, below a certain threshold, including these channels probably
|
||||||
receive_amount = amount_msat or max_can_receive
|
# hurts more than help, as they lead to many failed attempts for the sender.
|
||||||
items = sorted(list(nodes_that_can_receive.items()), key=operator.itemgetter(1))
|
channels = sorted(channels, key=lambda chan: -chan.available_to_spend(REMOTE))
|
||||||
for node_id, v in items:
|
selected_channels = []
|
||||||
if v < receive_amount * MPP_RECEIVE_CUTOFF:
|
running_sum = 0
|
||||||
nodes_that_can_receive.pop(node_id)
|
cutoff_factor = 0.2 # heuristic
|
||||||
# break immediately because max_can_receive needs to be recomputed
|
for chan in channels:
|
||||||
|
recv_capacity = chan.available_to_spend(REMOTE)
|
||||||
|
chan_can_handle_payment_as_single_part = recv_capacity >= amount_msat
|
||||||
|
chan_small_compared_to_running_sum = recv_capacity < cutoff_factor * running_sum
|
||||||
|
if not chan_can_handle_payment_as_single_part and chan_small_compared_to_running_sum:
|
||||||
break
|
break
|
||||||
else:
|
running_sum += recv_capacity
|
||||||
break
|
selected_channels.append(chan)
|
||||||
return nodes_that_can_receive
|
channels = selected_channels
|
||||||
|
del selected_channels
|
||||||
|
# cap max channels to include to keep QR code reasonably scannable
|
||||||
|
channels = channels[:10]
|
||||||
|
return channels
|
||||||
|
|
||||||
def num_sats_can_receive(self) -> Decimal:
|
def num_sats_can_receive(self) -> Decimal:
|
||||||
can_receive_nodes = self.border_nodes_that_can_receive(None)
|
"""Return a conservative estimate of max sat value we can realistically receive
|
||||||
can_receive_msat = sum(can_receive_nodes.values())
|
in a single payment. (MPP is allowed)
|
||||||
|
|
||||||
|
The theoretical max would be `sum(chan.available_to_spend(REMOTE) for chan in self.channels)`,
|
||||||
|
but that would require a sender using MPP to magically guess all our channel liquidities.
|
||||||
|
"""
|
||||||
|
with self.lock:
|
||||||
|
recv_channels = self.get_channels_to_include_in_invoice()
|
||||||
|
recv_chan_msats = [chan.available_to_spend(REMOTE) for chan in recv_channels]
|
||||||
|
if not recv_chan_msats:
|
||||||
|
return Decimal(0)
|
||||||
|
can_receive_msat = max(
|
||||||
|
max(recv_chan_msats), # single-part payment baseline
|
||||||
|
sum(recv_chan_msats) // 2, # heuristic for MPP
|
||||||
|
)
|
||||||
return Decimal(can_receive_msat) / 1000
|
return Decimal(can_receive_msat) / 1000
|
||||||
|
|
||||||
def num_sats_can_receive_no_mpp(self) -> Decimal:
|
def num_sats_can_receive_no_mpp(self) -> Decimal:
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
|
|||||||
get_channel_by_id = LNWallet.get_channel_by_id
|
get_channel_by_id = LNWallet.get_channel_by_id
|
||||||
channels_for_peer = LNWallet.channels_for_peer
|
channels_for_peer = LNWallet.channels_for_peer
|
||||||
calc_routing_hints_for_invoice = LNWallet.calc_routing_hints_for_invoice
|
calc_routing_hints_for_invoice = LNWallet.calc_routing_hints_for_invoice
|
||||||
border_nodes_that_can_receive = LNWallet.border_nodes_that_can_receive
|
get_channels_to_include_in_invoice = LNWallet.get_channels_to_include_in_invoice
|
||||||
handle_error_code_from_failed_htlc = LNWallet.handle_error_code_from_failed_htlc
|
handle_error_code_from_failed_htlc = LNWallet.handle_error_code_from_failed_htlc
|
||||||
is_trampoline_peer = LNWallet.is_trampoline_peer
|
is_trampoline_peer = LNWallet.is_trampoline_peer
|
||||||
wait_for_received_pending_htlcs_to_get_removed = LNWallet.wait_for_received_pending_htlcs_to_get_removed
|
wait_for_received_pending_htlcs_to_get_removed = LNWallet.wait_for_received_pending_htlcs_to_get_removed
|
||||||
|
|||||||
Reference in New Issue
Block a user