1
0

lnrouter+lnworker: use liquidity hints

Adds liquidity hints for the sending capabilities of routing channels in the
graph. The channel blacklist is incorporated into liquidity hints.
Liquidity hints are updated when a payment fails with a temporary
channel failure or when it succeeds. Liquidity hints are used to give a
penalty in the _edge_cost heuristics used by the pathfinding algorithm.
The base penalty in (_edge_cost) is removed because it is now part of the
liquidity penalty. We don't return early from get_distances, as we want
to explore all channels.
This commit is contained in:
bitromortac
2021-03-09 08:47:30 +01:00
parent 209449bec4
commit 4df67a4f78
7 changed files with 418 additions and 60 deletions

View File

@@ -33,7 +33,7 @@ from electrum import lnmsg
from electrum.logging import console_stderr_handler, Logger
from electrum.lnworker import PaymentInfo, RECEIVED
from electrum.lnonion import OnionFailureCode
from electrum.lnutil import ChannelBlackList, derive_payment_secret_from_payment_preimage
from electrum.lnutil import derive_payment_secret_from_payment_preimage
from electrum.lnutil import LOCAL, REMOTE
from electrum.invoices import PR_PAID, PR_UNPAID
@@ -66,7 +66,6 @@ class MockNetwork:
self.path_finder = LNPathFinder(self.channel_db)
self.tx_queue = tx_queue
self._blockchain = MockBlockchain()
self.channel_blacklist = ChannelBlackList()
@property
def callback_lock(self):
@@ -807,7 +806,7 @@ class TestPeer(TestCaseForTestnet):
run(f())
@needs_test_with_all_chacha20_implementations
def test_payment_with_temp_channel_failure(self):
def test_payment_with_temp_channel_failure_and_liquidty_hints(self):
# prepare channels such that a temporary channel failure happens at c->d
funds_distribution = {
'ac': (200_000_000, 200_000_000), # low fees
@@ -831,6 +830,27 @@ class TestPeer(TestCaseForTestnet):
self.assertEqual(PR_PAID, graph.w_d.get_payment_status(lnaddr.paymenthash))
self.assertEqual(OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, log[0].failure_msg.code)
self.assertEqual(OnionFailureCode.TEMPORARY_CHANNEL_FAILURE, log[1].failure_msg.code)
liquidity_hints = graph.w_a.network.path_finder.liquidity_hints
pubkey_a = graph.w_a.node_keypair.pubkey
pubkey_b = graph.w_b.node_keypair.pubkey
pubkey_c = graph.w_c.node_keypair.pubkey
pubkey_d = graph.w_d.node_keypair.pubkey
# check liquidity hints for failing route:
hint_ac = liquidity_hints.get_hint(graph.chan_ac.short_channel_id)
hint_cd = liquidity_hints.get_hint(graph.chan_cd.short_channel_id)
self.assertEqual(amount_to_pay, hint_ac.can_send(pubkey_a < pubkey_c))
self.assertEqual(None, hint_ac.cannot_send(pubkey_a < pubkey_c))
self.assertEqual(None, hint_cd.can_send(pubkey_c < pubkey_d))
self.assertEqual(amount_to_pay, hint_cd.cannot_send(pubkey_c < pubkey_d))
# check liquidity hints for successful route:
hint_ab = liquidity_hints.get_hint(graph.chan_ab.short_channel_id)
hint_bd = liquidity_hints.get_hint(graph.chan_bd.short_channel_id)
self.assertEqual(amount_to_pay, hint_ab.can_send(pubkey_a < pubkey_b))
self.assertEqual(None, hint_ab.cannot_send(pubkey_a < pubkey_b))
self.assertEqual(amount_to_pay, hint_bd.can_send(pubkey_b < pubkey_d))
self.assertEqual(None, hint_bd.cannot_send(pubkey_b < pubkey_d))
raise PaymentDone()
async def f():
async with TaskGroup() as group:

View File

@@ -1,3 +1,4 @@
from math import inf
import unittest
import tempfile
import shutil
@@ -11,7 +12,7 @@ from electrum.lnonion import (OnionHopsDataSingle, new_onion_packet,
from electrum import bitcoin, lnrouter
from electrum.constants import BitcoinTestnet
from electrum.simple_config import SimpleConfig
from electrum.lnrouter import PathEdge
from electrum.lnrouter import PathEdge, LiquidityHintMgr, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH, DEFAULT_PENALTY_BASE_MSAT, fee_for_edge_msat
from . import TestCaseForTestnet
from .test_bitcoin import needs_test_with_all_chacha20_implementations
@@ -153,6 +154,104 @@ class Test_LNRouter(TestCaseForTestnet):
self.cdb.stop()
asyncio.run_coroutine_threadsafe(self.cdb.stopped_event.wait(), self.asyncio_loop).result()
def test_find_path_liquidity_hints_failure(self):
self.prepare_graph()
amount_to_send = 100000
"""
assume failure over channel 2, B -> E
A -3-> B |-2-> E
A -6-> D -5-> E <= chosen path
A -6-> D -4-> C -7-> E
A -3-> B -1-> C -7-> E
A -6-> D -4-> C -1-> B -2-> E
A -3-> B -1-> C -4-> D -5-> E
"""
self.path_finder.liquidity_hints.update_cannot_send(node('b'), node('e'), channel(2), amount_to_send - 1)
path = self.path_finder.find_path_for_payment(
nodeA=node('a'),
nodeB=node('e'),
invoice_amount_msat=amount_to_send)
self.assertEqual(channel(6), path[0].short_channel_id)
self.assertEqual(channel(5), path[1].short_channel_id)
"""
assume failure over channel 5, D -> E
A -3-> B |-2-> E
A -6-> D |-5-> E
A -6-> D -4-> C -7-> E
A -3-> B -1-> C -7-> E <= chosen path
A -6-> D -4-> C -1-> B |-2-> E
A -3-> B -1-> C -4-> D |-5-> E
"""
self.path_finder.liquidity_hints.update_cannot_send(node('d'), node('e'), channel(5), amount_to_send - 1)
path = self.path_finder.find_path_for_payment(
nodeA=node('a'),
nodeB=node('e'),
invoice_amount_msat=amount_to_send)
self.assertEqual(channel(3), path[0].short_channel_id)
self.assertEqual(channel(1), path[1].short_channel_id)
self.assertEqual(channel(7), path[2].short_channel_id)
"""
assume success over channel 4, D -> C
A -3-> B |-2-> E
A -6-> D |-5-> E
A -6-> D -4-> C -7-> E <= chosen path
A -3-> B -1-> C -7-> E
A -6-> D -4-> C -1-> B |-2-> E
A -3-> B -1-> C -4-> D |-5-> E
"""
self.path_finder.liquidity_hints.update_can_send(node('d'), node('c'), channel(4), amount_to_send + 1000)
path = self.path_finder.find_path_for_payment(
nodeA=node('a'),
nodeB=node('e'),
invoice_amount_msat=amount_to_send)
self.assertEqual(channel(6), path[0].short_channel_id)
self.assertEqual(channel(4), path[1].short_channel_id)
self.assertEqual(channel(7), path[2].short_channel_id)
self.cdb.stop()
asyncio.run_coroutine_threadsafe(self.cdb.stopped_event.wait(), self.asyncio_loop).result()
def test_liquidity_hints(self):
liquidity_hints = LiquidityHintMgr()
node_from = bytes(0)
node_to = bytes(1)
channel_id = ShortChannelID.from_components(0, 0, 0)
amount_to_send = 1_000_000
# check default penalty
self.assertEqual(
fee_for_edge_msat(amount_to_send, DEFAULT_PENALTY_BASE_MSAT, DEFAULT_PENALTY_PROPORTIONAL_MILLIONTH),
liquidity_hints.penalty(node_from, node_to, channel_id, amount_to_send)
)
liquidity_hints.update_can_send(node_from, node_to, channel_id, 1_000_000)
liquidity_hints.update_cannot_send(node_from, node_to, channel_id, 2_000_000)
hint = liquidity_hints.get_hint(channel_id)
self.assertEqual(1_000_000, hint.can_send(node_from < node_to))
self.assertEqual(None, hint.cannot_send(node_to < node_from))
self.assertEqual(2_000_000, hint.cannot_send(node_from < node_to))
# the can_send backward hint is set automatically
self.assertEqual(2_000_000, hint.can_send(node_to < node_from))
# check penalties
self.assertEqual(0., liquidity_hints.penalty(node_from, node_to, channel_id, 1_000_000))
self.assertEqual(650, liquidity_hints.penalty(node_from, node_to, channel_id, 1_500_000))
self.assertEqual(inf, liquidity_hints.penalty(node_from, node_to, channel_id, 2_000_000))
# test that we don't overwrite significant info with less significant info
liquidity_hints.update_can_send(node_from, node_to, channel_id, 500_000)
hint = liquidity_hints.get_hint(channel_id)
self.assertEqual(1_000_000, hint.can_send(node_from < node_to))
# test case when can_send > cannot_send
liquidity_hints.update_can_send(node_from, node_to, channel_id, 3_000_000)
hint = liquidity_hints.get_hint(channel_id)
self.assertEqual(3_000_000, hint.can_send(node_from < node_to))
self.assertEqual(None, hint.cannot_send(node_from < node_to))
@needs_test_with_all_chacha20_implementations
def test_new_onion_packet_legacy(self):
# test vector from bolt-04