1
0

Merge pull request #7202 from bitromortac/2104-mpp-channel-splitting

MPP splitting algorithm: redesign and split within channels
This commit is contained in:
ghost43
2021-12-17 13:58:54 +00:00
committed by GitHub
5 changed files with 321 additions and 321 deletions

View File

@@ -205,7 +205,7 @@ class MockLNWallet(Logger, NetworkRetryManager[LNPeerAddr]):
min_cltv_expiry=decoded_invoice.get_min_final_cltv_expiry(),
r_tags=decoded_invoice.get_routing_info('r'),
invoice_features=decoded_invoice.get_features(),
trampoline_fee_level=0,
trampoline_fee_levels=defaultdict[int],
use_two_trampolines=False,
payment_hash=decoded_invoice.paymenthash,
payment_secret=decoded_invoice.payment_secret,
@@ -888,15 +888,18 @@ class TestPeer(TestCaseForTestnet):
with self.assertRaises(PaymentDone):
run(f())
def _run_mpp(self, graph, kwargs1, kwargs2):
def _run_mpp(self, graph, fail_kwargs, success_kwargs):
"""Tests a multipart payment scenario for failing and successful cases."""
self.assertEqual(500_000_000_000, graph.chan_ab.balance(LOCAL))
self.assertEqual(500_000_000_000, graph.chan_ac.balance(LOCAL))
amount_to_pay = 600_000_000_000
peers = graph.all_peers()
async def pay(attempts=1,
alice_uses_trampoline=False,
bob_forwarding=True,
mpp_invoice=True):
async def pay(
attempts=1,
alice_uses_trampoline=False,
bob_forwarding=True,
mpp_invoice=True
):
if mpp_invoice:
graph.w_d.features |= LnFeatures.BASIC_MPP_OPT
if not bob_forwarding:
@@ -930,22 +933,22 @@ class TestPeer(TestCaseForTestnet):
await group.spawn(pay(**kwargs))
with self.assertRaises(NoPathFound):
run(f(kwargs1))
run(f(fail_kwargs))
with self.assertRaises(PaymentDone):
run(f(kwargs2))
run(f(success_kwargs))
@needs_test_with_all_chacha20_implementations
def test_multipart_payment_with_timeout(self):
def test_payment_multipart_with_timeout(self):
graph = self.prepare_chans_and_peers_in_square()
self._run_mpp(graph, {'bob_forwarding':False}, {'bob_forwarding':True})
self._run_mpp(graph, {'bob_forwarding': False}, {'bob_forwarding': True})
@needs_test_with_all_chacha20_implementations
def test_multipart_payment(self):
def test_payment_multipart(self):
graph = self.prepare_chans_and_peers_in_square()
self._run_mpp(graph, {'mpp_invoice':False}, {'mpp_invoice':True})
self._run_mpp(graph, {'mpp_invoice': False}, {'mpp_invoice': True})
@needs_test_with_all_chacha20_implementations
def test_multipart_payment_with_trampoline(self):
def test_payment_multipart_trampoline(self):
# single attempt will fail with insufficient trampoline fee
graph = self.prepare_chans_and_peers_in_square()
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {
@@ -953,7 +956,10 @@ class TestPeer(TestCaseForTestnet):
graph.w_c.name: LNPeerAddr(host="127.0.0.1", port=9735, pubkey=graph.w_c.node_keypair.pubkey),
}
try:
self._run_mpp(graph, {'alice_uses_trampoline':True, 'attempts':1}, {'alice_uses_trampoline':True, 'attempts':30})
self._run_mpp(
graph,
{'alice_uses_trampoline': True, 'attempts': 1},
{'alice_uses_trampoline': True, 'attempts': 30})
finally:
electrum.trampoline._TRAMPOLINE_NODES_UNITTESTS = {}

View File

@@ -28,43 +28,58 @@ class TestMppSplit(ElectrumTestCase):
def test_suggest_splits(self):
with self.subTest(msg="do a payment with the maximal amount spendable over a single channel"):
splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_parts=True)
self.assertEqual({(0, 0): 660_000_000, (1, 1): 340_000_000, (2, 0): 0, (3, 2): 0}, splits[0][0])
splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_part_payments=True)
self.assertEqual({
(0, 0): [671_020_676],
(1, 1): [328_979_324],
(2, 0): [],
(3, 2): []},
splits[0].config
)
with self.subTest(msg="do a payment with a larger amount than what is supported by a single channel"):
splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds, exclude_single_parts=True)
self.assertEqual(2, mpp_split.number_nonzero_parts(splits[0][0]))
splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds, exclude_single_part_payments=False)
self.assertEqual(2, mpp_split.number_parts(splits[0].config))
with self.subTest(msg="do a payment with the maximal amount spendable over all channels"):
splits = mpp_split.suggest_splits(sum(self.channels_with_funds.values()), self.channels_with_funds, exclude_single_parts=True)
self.assertEqual({(0, 0): 1_000_000_000, (1, 1): 500_000_000, (2, 0): 302_000_000, (3, 2): 101_000_000}, splits[0][0])
splits = mpp_split.suggest_splits(
sum(self.channels_with_funds.values()), self.channels_with_funds, exclude_single_part_payments=True)
self.assertEqual({
(0, 0): [1_000_000_000],
(1, 1): [500_000_000],
(2, 0): [302_000_000],
(3, 2): [101_000_000]},
splits[0].config
)
with self.subTest(msg="do a payment with the amount supported by all channels"):
splits = mpp_split.suggest_splits(101_000_000, self.channels_with_funds, exclude_single_parts=False)
for s in splits[:4]:
self.assertEqual(1, mpp_split.number_nonzero_parts(s[0]))
splits = mpp_split.suggest_splits(101_000_000, self.channels_with_funds, exclude_single_part_payments=False)
for split in splits[:3]:
self.assertEqual(1, mpp_split.number_nonzero_channels(split.config))
# due to exhaustion of the smallest channel, the algorithm favors
# a splitting of the parts into two
self.assertEqual(2, mpp_split.number_parts(splits[4].config))
def test_send_to_single_node(self):
splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_parts=True, single_node=True)
self.assertEqual({(0, 0): 738_000_000, (1, 1): 0, (2, 0): 262_000_000, (3, 2): 0}, splits[0][0])
splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_part_payments=False, exclude_multinode_payments=True)
for split in splits:
assert mpp_split.number_nonzero_nodes(split[0]) == 1
assert mpp_split.number_nonzero_nodes(split.config) == 1
def test_saturation(self):
"""Split configurations which spend the full amount in a channel should be avoided."""
channels_with_funds = {(0, 0): 159_799_733_076, (1, 1): 499_986_152_000}
splits = mpp_split.suggest_splits(600_000_000_000, channels_with_funds, exclude_single_parts=True)
splits = mpp_split.suggest_splits(600_000_000_000, channels_with_funds, exclude_single_part_payments=True)
uses_full_amount = False
for c, a in splits[0][0].items():
for c, a in splits[0].config.items():
if a == channels_with_funds[c]:
uses_full_amount |= True
self.assertFalse(uses_full_amount)
def test_payment_below_min_part_size(self):
amount = mpp_split.MIN_PART_MSAT // 2
splits = mpp_split.suggest_splits(amount, self.channels_with_funds, exclude_single_parts=False)
amount = mpp_split.MIN_PART_SIZE_MSAT // 2
splits = mpp_split.suggest_splits(amount, self.channels_with_funds, exclude_single_part_payments=False)
# we only get four configurations that end up spending the full amount
# in a single channel
self.assertEqual(4, len(splits))
@@ -77,25 +92,37 @@ class TestMppSplit(ElectrumTestCase):
with self.subTest(msg="split payments with intermediate part penalty"):
mpp_split.PART_PENALTY = 1.0
splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds)
self.assertEqual(2, mpp_split.number_nonzero_parts(splits[0][0]))
self.assertEqual(2, mpp_split.number_parts(splits[0].config))
with self.subTest(msg="split payments with intermediate part penalty"):
mpp_split.PART_PENALTY = 0.3
splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds)
self.assertEqual(3, mpp_split.number_nonzero_parts(splits[0][0]))
self.assertEqual(4, mpp_split.number_parts(splits[0].config))
with self.subTest(msg="split payments with no part penalty"):
mpp_split.PART_PENALTY = 0.0
splits = mpp_split.suggest_splits(1_100_000_000, self.channels_with_funds)
self.assertEqual(4, mpp_split.number_nonzero_parts(splits[0][0]))
self.assertEqual(5, mpp_split.number_parts(splits[0].config))
def test_suggest_splits_single_channel(self):
channels_with_funds = {
0: 1_000_000_000,
(0, 0): 1_000_000_000,
}
with self.subTest(msg="do a payment with the maximal amount spendable on a single channel"):
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_parts=False)
self.assertEqual({0: 1_000_000_000}, splits[0][0])
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
self.assertEqual({(0, 0): [1_000_000_000]}, splits[0].config)
with self.subTest(msg="test sending an amount greater than what we have available"):
self.assertRaises(NoPathFound, mpp_split.suggest_splits, *(1_100_000_000, channels_with_funds))
with self.subTest(msg="test sending a large amount over a single channel in chunks"):
mpp_split.PART_PENALTY = 0.5
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
self.assertEqual(2, len(splits[0].config[(0, 0)]))
with self.subTest(msg="test sending a large amount over a single channel in chunks"):
mpp_split.PART_PENALTY = 0.3
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
self.assertEqual(3, len(splits[0].config[(0, 0)]))
with self.subTest(msg="exclude all single channel splits"):
mpp_split.PART_PENALTY = 0.3
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_channel_splits=True)
self.assertEqual(1, len(splits[0].config[(0, 0)]))