Merge pull request #7821 from SomberNight/202205_lnworker_num_sats_can_receive
lnworker: rework num_sats_can_receive and routing_hints_for_invoice
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
|
||||
|
||||
MPP_RECEIVE_CUTOFF = 0.2
|
||||
|
||||
NUM_PEERS_TARGET = 4
|
||||
|
||||
# onchain channel backup data
|
||||
@@ -1998,15 +1996,7 @@ class LNWallet(LNWorker):
|
||||
def calc_routing_hints_for_invoice(self, amount_msat: Optional[int]):
|
||||
"""calculate routing hints (BOLT-11 'r' field)"""
|
||||
routing_hints = []
|
||||
with self.lock:
|
||||
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]
|
||||
channels = list(self.get_channels_to_include_in_invoice(amount_msat))
|
||||
random.shuffle(channels) # let's not leak channel order
|
||||
scid_to_my_channels = {chan.short_channel_id: chan for chan in channels
|
||||
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)
|
||||
return Decimal(can_send_minus_fees) / 1000
|
||||
|
||||
def border_nodes_that_can_receive(self, amount_msat=None):
|
||||
# if amount_msat is None, use the max amount we can receive
|
||||
#
|
||||
# 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)
|
||||
def get_channels_to_include_in_invoice(self, amount_msat=None) -> Sequence[Channel]:
|
||||
if not amount_msat: # assume we want to recv a large amt, e.g. finding max.
|
||||
amount_msat = float('inf')
|
||||
with self.lock:
|
||||
for c in self.channels.values():
|
||||
if not c.is_active() or c.is_frozen_for_receiving():
|
||||
continue
|
||||
nodes_that_can_receive[c.node_id] += c.available_to_spend(REMOTE)
|
||||
while True:
|
||||
max_can_receive = sum(nodes_that_can_receive.values())
|
||||
receive_amount = amount_msat or max_can_receive
|
||||
items = sorted(list(nodes_that_can_receive.items()), key=operator.itemgetter(1))
|
||||
for node_id, v in items:
|
||||
if v < receive_amount * MPP_RECEIVE_CUTOFF:
|
||||
nodes_that_can_receive.pop(node_id)
|
||||
# break immediately because max_can_receive needs to be recomputed
|
||||
channels = list(self.channels.values())
|
||||
# we exclude channels that cannot *right now* receive (e.g. peer offline)
|
||||
channels = [chan for chan in channels
|
||||
if (chan.is_active() and not chan.is_frozen_for_receiving())]
|
||||
# Filter out nodes that have 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.
|
||||
channels = sorted(channels, key=lambda chan: -chan.available_to_spend(REMOTE))
|
||||
selected_channels = []
|
||||
running_sum = 0
|
||||
cutoff_factor = 0.2 # heuristic
|
||||
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
|
||||
else:
|
||||
break
|
||||
return nodes_that_can_receive
|
||||
running_sum += recv_capacity
|
||||
selected_channels.append(chan)
|
||||
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:
|
||||
can_receive_nodes = self.border_nodes_that_can_receive(None)
|
||||
can_receive_msat = sum(can_receive_nodes.values())
|
||||
"""Return a conservative estimate of max sat value we can realistically receive
|
||||
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
|
||||
|
||||
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
|
||||
channels_for_peer = LNWallet.channels_for_peer
|
||||
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
|
||||
is_trampoline_peer = LNWallet.is_trampoline_peer
|
||||
wait_for_received_pending_htlcs_to_get_removed = LNWallet.wait_for_received_pending_htlcs_to_get_removed
|
||||
|
||||
Reference in New Issue
Block a user