Merge pull request #9837 from spesmilo/htlc_slots_left
pass number of htlc_slots_left to suggest_splits
This commit is contained in:
@@ -1113,7 +1113,7 @@ class Channel(AbstractChannel):
|
|||||||
if amount_msat < chan_config.htlc_minimum_msat:
|
if amount_msat < chan_config.htlc_minimum_msat:
|
||||||
raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
|
raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
|
||||||
|
|
||||||
if self.too_many_htlcs(htlc_proposer):
|
if self.htlc_slots_left(htlc_proposer) == 0:
|
||||||
raise PaymentFailure('Too many HTLCs already in channel')
|
raise PaymentFailure('Too many HTLCs already in channel')
|
||||||
|
|
||||||
if amount_msat > self.remaining_max_inflight(htlc_receiver):
|
if amount_msat > self.remaining_max_inflight(htlc_receiver):
|
||||||
@@ -1126,7 +1126,7 @@ class Channel(AbstractChannel):
|
|||||||
if max_can_send_msat < amount_msat:
|
if max_can_send_msat < amount_msat:
|
||||||
raise PaymentFailure(f'Not enough balance. can send: {max_can_send_msat}, tried: {amount_msat}')
|
raise PaymentFailure(f'Not enough balance. can send: {max_can_send_msat}, tried: {amount_msat}')
|
||||||
|
|
||||||
def too_many_htlcs(self, htlc_proposer: HTLCOwner) -> bool:
|
def htlc_slots_left(self, htlc_proposer: HTLCOwner) -> int:
|
||||||
# check "max_accepted_htlcs"
|
# check "max_accepted_htlcs"
|
||||||
htlc_receiver = htlc_proposer.inverted()
|
htlc_receiver = htlc_proposer.inverted()
|
||||||
ctn = self.get_next_ctn(htlc_receiver)
|
ctn = self.get_next_ctn(htlc_receiver)
|
||||||
@@ -1134,17 +1134,16 @@ class Channel(AbstractChannel):
|
|||||||
# If proposer is LOCAL we apply stricter checks as that is behaviour we can control.
|
# If proposer is LOCAL we apply stricter checks as that is behaviour we can control.
|
||||||
# This should lead to fewer disagreements (i.e. channels failing).
|
# This should lead to fewer disagreements (i.e. channels failing).
|
||||||
strict = (htlc_proposer == LOCAL)
|
strict = (htlc_proposer == LOCAL)
|
||||||
# this is the loose check BOLT-02 specifies:
|
if not strict:
|
||||||
if len(self.hm.htlcs_by_direction(htlc_receiver, direction=RECEIVED, ctn=ctn)) + 1 > chan_config.max_accepted_htlcs:
|
# this is the loose check BOLT-02 specifies:
|
||||||
return True
|
return chan_config.max_accepted_htlcs - len(self.hm.htlcs_by_direction(htlc_receiver, direction=RECEIVED, ctn=ctn))
|
||||||
# however, c-lightning is a lot stricter, so extra checks:
|
else:
|
||||||
# https://github.com/ElementsProject/lightning/blob/4dcd4ca1556b13b6964a10040ba1d5ef82de4788/channeld/full_channel.c#L581
|
# however, c-lightning is a lot stricter, so extra checks:
|
||||||
if strict:
|
# https://github.com/ElementsProject/lightning/blob/4dcd4ca1556b13b6964a10040ba1d5ef82de4788/channeld/full_channel.c#L581
|
||||||
max_concurrent_htlcs = min(self.config[htlc_proposer].max_accepted_htlcs,
|
max_concurrent_htlcs = min(
|
||||||
self.config[htlc_receiver].max_accepted_htlcs)
|
self.config[htlc_proposer].max_accepted_htlcs,
|
||||||
if len(self.hm.htlcs(htlc_receiver, ctn=ctn)) + 1 > max_concurrent_htlcs:
|
self.config[htlc_receiver].max_accepted_htlcs)
|
||||||
return True
|
return max_concurrent_htlcs - len(self.hm.htlcs(htlc_receiver, ctn=ctn))
|
||||||
return False
|
|
||||||
|
|
||||||
def remaining_max_inflight(self, htlc_receiver: HTLCOwner) -> int:
|
def remaining_max_inflight(self, htlc_receiver: HTLCOwner) -> int:
|
||||||
# check "max_htlc_value_in_flight_msat"
|
# check "max_htlc_value_in_flight_msat"
|
||||||
@@ -1579,7 +1578,7 @@ class Channel(AbstractChannel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
max_send_msat = min(max_send_msat, self.remaining_max_inflight(receiver))
|
max_send_msat = min(max_send_msat, self.remaining_max_inflight(receiver))
|
||||||
if self.too_many_htlcs(sender):
|
if self.htlc_slots_left(sender) == 0:
|
||||||
max_send_msat = 0
|
max_send_msat = 0
|
||||||
|
|
||||||
max_send_msat = max(max_send_msat, 0)
|
max_send_msat = max(max_send_msat, 0)
|
||||||
|
|||||||
@@ -1926,7 +1926,7 @@ class LNWallet(LNWorker):
|
|||||||
receiver_pubkey: bytes,
|
receiver_pubkey: bytes,
|
||||||
) -> List['SplitConfigRating']:
|
) -> List['SplitConfigRating']:
|
||||||
channels_with_funds = {
|
channels_with_funds = {
|
||||||
(chan.channel_id, chan.node_id): int(chan.available_to_spend(HTLCOwner.LOCAL))
|
(chan.channel_id, chan.node_id): ( int(chan.available_to_spend(HTLCOwner.LOCAL)), chan.htlc_slots_left(HTLCOwner.LOCAL))
|
||||||
for chan in my_active_channels
|
for chan in my_active_channels
|
||||||
}
|
}
|
||||||
# if we have a direct channel it's preferrable to send a single part directly through this
|
# if we have a direct channel it's preferrable to send a single part directly through this
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ MAX_PARTS = 5 # maximum number of parts for splitting
|
|||||||
|
|
||||||
|
|
||||||
# maps a channel (channel_id, node_id) to the funds it has available
|
# maps a channel (channel_id, node_id) to the funds it has available
|
||||||
ChannelsFundsInfo = Dict[Tuple[bytes, bytes], int]
|
ChannelsFundsInfo = Dict[Tuple[bytes, bytes], Tuple[int, int]]
|
||||||
|
|
||||||
|
|
||||||
class SplitConfig(dict, Dict[Tuple[bytes, bytes], List[int]]):
|
class SplitConfig(dict, Dict[Tuple[bytes, bytes], List[int]]):
|
||||||
@@ -105,7 +105,7 @@ def rate_config(
|
|||||||
total_amount = config.total_config_amount()
|
total_amount = config.total_config_amount()
|
||||||
|
|
||||||
for channel, amounts in config.items():
|
for channel, amounts in config.items():
|
||||||
funds = channels_with_funds[channel]
|
funds, slots = channels_with_funds[channel]
|
||||||
if amounts:
|
if amounts:
|
||||||
for amount in amounts:
|
for amount in amounts:
|
||||||
rating += amount * amount / (total_amount * total_amount) # penalty to favor equal distribution of amounts
|
rating += amount * amount / (total_amount * total_amount) # penalty to favor equal distribution of amounts
|
||||||
@@ -116,7 +116,8 @@ def rate_config(
|
|||||||
|
|
||||||
|
|
||||||
def suggest_splits(
|
def suggest_splits(
|
||||||
amount_msat: int, channels_with_funds: ChannelsFundsInfo,
|
amount_msat: int,
|
||||||
|
channels_with_funds: ChannelsFundsInfo,
|
||||||
exclude_single_part_payments=False,
|
exclude_single_part_payments=False,
|
||||||
exclude_multinode_payments=False,
|
exclude_multinode_payments=False,
|
||||||
exclude_single_channel_splits=False
|
exclude_single_channel_splits=False
|
||||||
@@ -132,32 +133,37 @@ def suggest_splits(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
configs = []
|
configs = []
|
||||||
channels_order = list(channels_with_funds.keys())
|
channel_keys = list(channels_with_funds.keys())
|
||||||
|
|
||||||
# generate multiple configurations to get more configurations (there is randomness in this loop)
|
# generate multiple configurations to get more configurations (there is randomness in this loop)
|
||||||
for _ in range(CANDIDATES_PER_LEVEL):
|
for _ in range(CANDIDATES_PER_LEVEL):
|
||||||
# we want to have configurations with no splitting to many splittings
|
# we want to have configurations with no splitting to many splittings
|
||||||
for target_parts in range(1, MAX_PARTS):
|
for target_parts in range(1, MAX_PARTS):
|
||||||
config = SplitConfig()
|
config = SplitConfig()
|
||||||
|
|
||||||
# randomly split amount into target_parts chunks
|
# randomly split amount into target_parts chunks
|
||||||
split_amounts = split_amount_normal(amount_msat, target_parts)
|
split_amounts = split_amount_normal(amount_msat, target_parts)
|
||||||
# randomly distribute amounts over channels
|
# randomly distribute amounts over channels
|
||||||
for amount in split_amounts:
|
for amount in split_amounts:
|
||||||
random.shuffle(channels_order)
|
random.shuffle(channel_keys)
|
||||||
# we check each channel and try to put the funds inside, break if we succeed
|
# we check each channel and try to put the funds inside, break if we succeed
|
||||||
for c in channels_order:
|
for c in channel_keys:
|
||||||
if c not in config:
|
if c not in config:
|
||||||
config[c] = []
|
config[c] = []
|
||||||
if sum(config[c]) + amount <= channels_with_funds[c]:
|
channel_funds, channel_slots = channels_with_funds[c]
|
||||||
|
if sum(config[c]) + amount <= channel_funds and len(config[c]) < channel_slots:
|
||||||
config[c].append(amount)
|
config[c].append(amount)
|
||||||
break
|
break
|
||||||
# if we don't succeed to put the amount anywhere,
|
# if we don't succeed to put the amount anywhere,
|
||||||
# we try to fill up channels and put the rest somewhere else
|
# we try to fill up channels and put the rest somewhere else
|
||||||
else:
|
else:
|
||||||
distribute_amount = amount
|
distribute_amount = amount
|
||||||
for c in channels_order:
|
for c in channel_keys:
|
||||||
funds_left = channels_with_funds[c] - sum(config[c])
|
channel_funds, channel_slots = channels_with_funds[c]
|
||||||
|
slots_left = channel_slots - len(config[c])
|
||||||
|
if slots_left == 0:
|
||||||
|
# no slot left in that channel
|
||||||
|
continue
|
||||||
|
funds_left = channel_funds - sum(config[c])
|
||||||
# it would be good to not fill the full channel if possible
|
# it would be good to not fill the full channel if possible
|
||||||
add_amount = min(funds_left, distribute_amount)
|
add_amount = min(funds_left, distribute_amount)
|
||||||
config[c].append(add_amount)
|
config[c].append(add_amount)
|
||||||
@@ -165,7 +171,7 @@ def suggest_splits(
|
|||||||
if distribute_amount == 0:
|
if distribute_amount == 0:
|
||||||
break
|
break
|
||||||
if config.total_config_amount() != amount_msat:
|
if config.total_config_amount() != amount_msat:
|
||||||
raise NoPathFound('Cannot distribute payment over channels.')
|
continue
|
||||||
if target_parts > 1 and config.is_any_amount_smaller_than_min_part_size():
|
if target_parts > 1 and config.is_any_amount_smaller_than_min_part_size():
|
||||||
if target_parts == 2:
|
if target_parts == 2:
|
||||||
# if there are already too small parts at the first split excluding single
|
# if there are already too small parts at the first split excluding single
|
||||||
@@ -175,6 +181,8 @@ def suggest_splits(
|
|||||||
continue
|
continue
|
||||||
assert config.total_config_amount() == amount_msat
|
assert config.total_config_amount() == amount_msat
|
||||||
configs.append(config)
|
configs.append(config)
|
||||||
|
if not configs:
|
||||||
|
raise NoPathFound('Cannot distribute payment over channels.')
|
||||||
|
|
||||||
configs = remove_duplicates(configs)
|
configs = remove_duplicates(configs)
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ class TestMppSplit(ElectrumTestCase):
|
|||||||
random.seed(0)
|
random.seed(0)
|
||||||
# key tuple denotes (channel_id, node_id)
|
# key tuple denotes (channel_id, node_id)
|
||||||
self.channels_with_funds = {
|
self.channels_with_funds = {
|
||||||
(b"0", b"0"): 1_000_000_000,
|
(b"0", b"0"): (1_000_000_000, 3),
|
||||||
(b"1", b"1"): 500_000_000,
|
(b"1", b"1"): (500_000_000, 2),
|
||||||
(b"2", b"0"): 302_000_000,
|
(b"2", b"0"): (302_000_000, 2),
|
||||||
(b"3", b"2"): 101_000_000,
|
(b"3", b"2"): (101_000_000, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
@@ -51,7 +51,7 @@ class TestMppSplit(ElectrumTestCase):
|
|||||||
|
|
||||||
with self.subTest(msg="do a payment with the maximal amount spendable over all channels"):
|
with self.subTest(msg="do a payment with the maximal amount spendable over all channels"):
|
||||||
splits = mpp_split.suggest_splits(
|
splits = mpp_split.suggest_splits(
|
||||||
sum(self.channels_with_funds.values()), self.channels_with_funds, exclude_single_part_payments=True)
|
sum([x[0] for x in self.channels_with_funds.values()]), self.channels_with_funds, exclude_single_part_payments=True)
|
||||||
self.assertEqual({
|
self.assertEqual({
|
||||||
(b"0", b"0"): [1_000_000_000],
|
(b"0", b"0"): [1_000_000_000],
|
||||||
(b"1", b"1"): [500_000_000],
|
(b"1", b"1"): [500_000_000],
|
||||||
@@ -68,6 +68,25 @@ class TestMppSplit(ElectrumTestCase):
|
|||||||
# a splitting of the parts into two
|
# a splitting of the parts into two
|
||||||
self.assertEqual(2, splits[4].config.number_parts())
|
self.assertEqual(2, splits[4].config.number_parts())
|
||||||
|
|
||||||
|
with self.subTest(msg="no htlc slots available"):
|
||||||
|
channels = self.channels_with_funds.copy()
|
||||||
|
# set all available slots to 0
|
||||||
|
for chan, (amount, _slots) in channels.items():
|
||||||
|
channels[chan] = (amount, 0)
|
||||||
|
with self.assertRaises(NoPathFound):
|
||||||
|
mpp_split.suggest_splits(20_000_000, channels, exclude_single_part_payments=False)
|
||||||
|
|
||||||
|
with self.subTest(msg="only one channel can add htlcs"):
|
||||||
|
channels = self.channels_with_funds.copy()
|
||||||
|
# set all available slots to 0 except for the first channel
|
||||||
|
for chan, (amount, _slots) in channels.items():
|
||||||
|
if chan != (b"0", b"0"):
|
||||||
|
channels[chan] = (amount, 0)
|
||||||
|
splits = mpp_split.suggest_splits(1_000_000_000, channels, exclude_single_part_payments=True)
|
||||||
|
for split in splits:
|
||||||
|
# check that the whole amount has been split on this channel
|
||||||
|
self.assertEqual(sum(split.config[(b"0", b"0")]), 1_000_000_000)
|
||||||
|
|
||||||
def test_send_to_single_node(self):
|
def test_send_to_single_node(self):
|
||||||
splits = mpp_split.suggest_splits(1_000_000_000, self.channels_with_funds, exclude_single_part_payments=False, exclude_multinode_payments=True)
|
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:
|
for split in splits:
|
||||||
@@ -75,7 +94,10 @@ class TestMppSplit(ElectrumTestCase):
|
|||||||
|
|
||||||
def test_saturation(self):
|
def test_saturation(self):
|
||||||
"""Split configurations which spend the full amount in a channel should be avoided."""
|
"""Split configurations which spend the full amount in a channel should be avoided."""
|
||||||
channels_with_funds = {(b"0", b"0"): 159_799_733_076, (b"1", b"1"): 499_986_152_000}
|
channels_with_funds = {
|
||||||
|
(b"0", b"0"): (159_799_733_076, 1),
|
||||||
|
(b"1", b"1"): (499_986_152_000, 1)
|
||||||
|
}
|
||||||
splits = mpp_split.suggest_splits(600_000_000_000, channels_with_funds, exclude_single_part_payments=True)
|
splits = mpp_split.suggest_splits(600_000_000_000, channels_with_funds, exclude_single_part_payments=True)
|
||||||
|
|
||||||
uses_full_amount = False
|
uses_full_amount = False
|
||||||
@@ -114,18 +136,21 @@ class TestMppSplit(ElectrumTestCase):
|
|||||||
|
|
||||||
def test_suggest_splits_single_channel(self):
|
def test_suggest_splits_single_channel(self):
|
||||||
channels_with_funds = {
|
channels_with_funds = {
|
||||||
(b"0", b"0"): 1_000_000_000,
|
(b"0", b"0"): (1_000_000_000, 3),
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.subTest(msg="do a payment with the maximal amount spendable on a single channel"):
|
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_part_payments=False)
|
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
|
||||||
|
self.assertEqual(1, len(splits[0].config[(b"0", b"0")]))
|
||||||
self.assertEqual({(b"0", b"0"): [1_000_000_000]}, splits[0].config)
|
self.assertEqual({(b"0", b"0"): [1_000_000_000]}, splits[0].config)
|
||||||
|
|
||||||
with self.subTest(msg="test sending an amount greater than what we have available"):
|
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))
|
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"):
|
with self.subTest(msg="test sending a large amount over a single channel in chunks"):
|
||||||
mpp_split.PART_PENALTY = 0.5
|
mpp_split.PART_PENALTY = 0.5
|
||||||
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
|
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
|
||||||
self.assertEqual(2, len(splits[0].config[(b"0", b"0")]))
|
self.assertEqual(2, len(splits[0].config[(b"0", b"0")]))
|
||||||
|
|
||||||
with self.subTest(msg="test sending a large amount over a single channel in chunks"):
|
with self.subTest(msg="test sending a large amount over a single channel in chunks"):
|
||||||
mpp_split.PART_PENALTY = 0.3
|
mpp_split.PART_PENALTY = 0.3
|
||||||
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
|
splits = mpp_split.suggest_splits(1_000_000_000, channels_with_funds, exclude_single_part_payments=False)
|
||||||
|
|||||||
Reference in New Issue
Block a user