Factorize code in create_routes_for_payment.
Calling suggest_splits with exclude_single_part_payments=False
This commit is contained in:
@@ -1539,6 +1539,33 @@ class LNWallet(LNWorker):
|
||||
else:
|
||||
return random.choice(list(hardcoded_trampoline_nodes().values())).pubkey
|
||||
|
||||
def suggest_splits(self, amount_msat: int, my_active_channels, invoice_features, r_tags):
|
||||
channels_with_funds = {
|
||||
(chan.channel_id, chan.node_id): int(chan.available_to_spend(HTLCOwner.LOCAL))
|
||||
for chan in my_active_channels
|
||||
}
|
||||
self.logger.info(f"channels_with_funds: {channels_with_funds}")
|
||||
if self.uses_trampoline():
|
||||
# in the case of a legacy payment, we don't allow splitting via different
|
||||
# trampoline nodes, because of https://github.com/ACINQ/eclair/issues/2127
|
||||
is_legacy, _ = is_legacy_relay(invoice_features, r_tags)
|
||||
exclude_multinode_payments = is_legacy
|
||||
# we don't split within a channel when sending to a trampoline node,
|
||||
# the trampoline node will split for us
|
||||
exclude_single_channel_splits = True
|
||||
else:
|
||||
exclude_multinode_payments = False
|
||||
exclude_single_channel_splits = False
|
||||
split_configurations = suggest_splits(
|
||||
amount_msat,
|
||||
channels_with_funds,
|
||||
exclude_single_part_payments=False,
|
||||
exclude_multinode_payments=exclude_multinode_payments,
|
||||
exclude_single_channel_splits=exclude_single_channel_splits
|
||||
)
|
||||
self.logger.info(f'suggest_split {amount_msat} returned {len(split_configurations)} configurations')
|
||||
return split_configurations
|
||||
|
||||
async def create_routes_for_payment(
|
||||
self, *,
|
||||
amount_msat: int, # part of payment amount we want routes for now
|
||||
@@ -1572,203 +1599,100 @@ class LNWallet(LNWorker):
|
||||
chan.is_active() and not chan.is_frozen_for_sending()]
|
||||
# try random order
|
||||
random.shuffle(my_active_channels)
|
||||
try:
|
||||
self.logger.info("trying single-part payment")
|
||||
# try to send over a single channel
|
||||
if self.uses_trampoline():
|
||||
for chan in my_active_channels:
|
||||
if not self.is_trampoline_peer(chan.node_id):
|
||||
continue
|
||||
if chan.node_id == invoice_pubkey:
|
||||
trampoline_onion = None
|
||||
trampoline_route = None
|
||||
trampoline_payment_secret = payment_secret
|
||||
trampoline_total_msat = final_total_msat
|
||||
amount_with_fees = amount_msat
|
||||
cltv_delta = min_cltv_expiry
|
||||
else:
|
||||
trampoline_route, trampoline_onion, amount_with_fees, cltv_delta = create_trampoline_route_and_onion(
|
||||
amount_msat=amount_msat,
|
||||
total_msat=final_total_msat,
|
||||
min_cltv_expiry=min_cltv_expiry,
|
||||
my_pubkey=self.node_keypair.pubkey,
|
||||
invoice_pubkey=invoice_pubkey,
|
||||
invoice_features=invoice_features,
|
||||
node_id=chan.node_id,
|
||||
r_tags=r_tags,
|
||||
payment_hash=payment_hash,
|
||||
payment_secret=payment_secret,
|
||||
local_height=local_height,
|
||||
trampoline_fee_level=trampoline_fee_level,
|
||||
use_two_trampolines=use_two_trampolines,
|
||||
failed_routes=self.failed_trampoline_routes)
|
||||
trampoline_payment_secret = os.urandom(32)
|
||||
trampoline_total_msat = amount_with_fees
|
||||
if chan.available_to_spend(LOCAL, strict=True) < amount_with_fees:
|
||||
continue
|
||||
if trampoline_route:
|
||||
self.logger.info(f'created route with trampoline fee level={trampoline_fee_level}')
|
||||
self.logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in trampoline_route]}')
|
||||
route = [
|
||||
RouteEdge(
|
||||
start_node=self.node_keypair.pubkey,
|
||||
end_node=chan.node_id,
|
||||
short_channel_id=chan.short_channel_id,
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_expiry_delta=0,
|
||||
node_features=trampoline_features)
|
||||
]
|
||||
yield route, amount_with_fees, trampoline_total_msat, amount_msat, cltv_delta, trampoline_payment_secret, trampoline_onion, trampoline_route
|
||||
break
|
||||
else:
|
||||
raise NoPathFound()
|
||||
else: # local single-part route computation
|
||||
route = await run_in_thread(
|
||||
partial(
|
||||
self.create_route_for_payment,
|
||||
amount_msat=amount_msat,
|
||||
invoice_pubkey=invoice_pubkey,
|
||||
min_cltv_expiry=min_cltv_expiry,
|
||||
r_tags=r_tags,
|
||||
invoice_features=invoice_features,
|
||||
my_sending_channels=my_active_channels,
|
||||
full_path=full_path
|
||||
)
|
||||
)
|
||||
yield route, amount_msat, final_total_msat, amount_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion, None
|
||||
except NoPathFound: # fall back to payment splitting
|
||||
self.logger.info("no path found, trying multi-part payment")
|
||||
if not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
|
||||
raise
|
||||
channels_with_funds = {(chan.channel_id, chan.node_id): int(chan.available_to_spend(HTLCOwner.LOCAL))
|
||||
for chan in my_active_channels}
|
||||
self.logger.info(f"channels_with_funds: {channels_with_funds}")
|
||||
|
||||
if self.uses_trampoline():
|
||||
# in the case of a legacy payment, we don't allow splitting via different
|
||||
# trampoline nodes, because of https://github.com/ACINQ/eclair/issues/2127
|
||||
use_single_node, _ = is_legacy_relay(invoice_features, r_tags)
|
||||
split_configurations = suggest_splits(
|
||||
amount_msat,
|
||||
channels_with_funds,
|
||||
exclude_multinode_payments=use_single_node,
|
||||
exclude_single_part_payments=True,
|
||||
# we don't split within a channel when sending to a trampoline node,
|
||||
# the trampoline node will split for us
|
||||
exclude_single_channel_splits=True,
|
||||
)
|
||||
self.logger.info(f'suggest_split {amount_msat} returned {len(split_configurations)} configurations')
|
||||
|
||||
for sc in split_configurations:
|
||||
try:
|
||||
self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}")
|
||||
per_trampoline_channel_amounts = defaultdict(list)
|
||||
# categorize by trampoline nodes for trampolin mpp construction
|
||||
for (chan_id, _), part_amounts_msat in sc.config.items():
|
||||
split_configurations = self.suggest_splits(amount_msat, my_active_channels, invoice_features, r_tags)
|
||||
for sc in split_configurations:
|
||||
is_mpp = len(sc.config.items()) > 1
|
||||
routes = []
|
||||
if is_mpp and not invoice_features.supports(LnFeatures.BASIC_MPP_OPT):
|
||||
continue
|
||||
self.logger.info(f"trying split configuration: {sc.config.values()} rating: {sc.rating}")
|
||||
try:
|
||||
if self.uses_trampoline():
|
||||
per_trampoline_channel_amounts = defaultdict(list)
|
||||
# categorize by trampoline nodes for trampolin mpp construction
|
||||
for (chan_id, _), part_amounts_msat in sc.config.items():
|
||||
chan = self.channels[chan_id]
|
||||
for part_amount_msat in part_amounts_msat:
|
||||
per_trampoline_channel_amounts[chan.node_id].append((chan_id, part_amount_msat))
|
||||
# for each trampoline forwarder, construct mpp trampoline
|
||||
for trampoline_node_id, trampoline_parts in per_trampoline_channel_amounts.items():
|
||||
per_trampoline_amount = sum([x[1] for x in trampoline_parts])
|
||||
if trampoline_node_id == invoice_pubkey:
|
||||
trampoline_route = None
|
||||
trampoline_onion = None
|
||||
per_trampoline_secret = payment_secret
|
||||
per_trampoline_amount_with_fees = amount_msat
|
||||
per_trampoline_cltv_delta = min_cltv_expiry
|
||||
per_trampoline_fees = 0
|
||||
else:
|
||||
trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion(
|
||||
amount_msat=per_trampoline_amount,
|
||||
total_msat=final_total_msat,
|
||||
min_cltv_expiry=min_cltv_expiry,
|
||||
my_pubkey=self.node_keypair.pubkey,
|
||||
invoice_pubkey=invoice_pubkey,
|
||||
invoice_features=invoice_features,
|
||||
node_id=trampoline_node_id,
|
||||
r_tags=r_tags,
|
||||
payment_hash=payment_hash,
|
||||
payment_secret=payment_secret,
|
||||
local_height=local_height,
|
||||
trampoline_fee_level=trampoline_fee_level,
|
||||
use_two_trampolines=use_two_trampolines,
|
||||
failed_routes=self.failed_trampoline_routes)
|
||||
# node_features is only used to determine is_tlv
|
||||
per_trampoline_secret = os.urandom(32)
|
||||
per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount
|
||||
self.logger.info(f'created route with trampoline fee level={trampoline_fee_level}')
|
||||
self.logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in trampoline_route]}')
|
||||
self.logger.info(f'per trampoline fees: {per_trampoline_fees}')
|
||||
for chan_id, part_amount_msat in trampoline_parts:
|
||||
chan = self.channels[chan_id]
|
||||
for part_amount_msat in part_amounts_msat:
|
||||
per_trampoline_channel_amounts[chan.node_id].append((chan_id, part_amount_msat))
|
||||
# for each trampoline forwarder, construct mpp trampoline
|
||||
routes = []
|
||||
for trampoline_node_id, trampoline_parts in per_trampoline_channel_amounts.items():
|
||||
per_trampoline_amount = sum([x[1] for x in trampoline_parts])
|
||||
if trampoline_node_id == invoice_pubkey:
|
||||
trampoline_route = None
|
||||
trampoline_onion = None
|
||||
per_trampoline_secret = payment_secret
|
||||
per_trampoline_amount_with_fees = amount_msat
|
||||
per_trampoline_cltv_delta = min_cltv_expiry
|
||||
per_trampoline_fees = 0
|
||||
else:
|
||||
trampoline_route, trampoline_onion, per_trampoline_amount_with_fees, per_trampoline_cltv_delta = create_trampoline_route_and_onion(
|
||||
amount_msat=per_trampoline_amount,
|
||||
total_msat=final_total_msat,
|
||||
min_cltv_expiry=min_cltv_expiry,
|
||||
my_pubkey=self.node_keypair.pubkey,
|
||||
invoice_pubkey=invoice_pubkey,
|
||||
invoice_features=invoice_features,
|
||||
node_id=trampoline_node_id,
|
||||
r_tags=r_tags,
|
||||
payment_hash=payment_hash,
|
||||
payment_secret=payment_secret,
|
||||
local_height=local_height,
|
||||
trampoline_fee_level=trampoline_fee_level,
|
||||
use_two_trampolines=use_two_trampolines,
|
||||
failed_routes=self.failed_trampoline_routes)
|
||||
# node_features is only used to determine is_tlv
|
||||
per_trampoline_secret = os.urandom(32)
|
||||
per_trampoline_fees = per_trampoline_amount_with_fees - per_trampoline_amount
|
||||
self.logger.info(f'created route with trampoline fee level={trampoline_fee_level}')
|
||||
self.logger.info(f'trampoline hops: {[hop.end_node.hex() for hop in trampoline_route]}')
|
||||
self.logger.info(f'per trampoline fees: {per_trampoline_fees}')
|
||||
for chan_id, part_amount_msat in trampoline_parts:
|
||||
chan = self.channels[chan_id]
|
||||
margin = chan.available_to_spend(LOCAL, strict=True) - part_amount_msat
|
||||
delta_fee = min(per_trampoline_fees, margin)
|
||||
# TODO: distribute trampoline fee over several channels?
|
||||
part_amount_msat_with_fees = part_amount_msat + delta_fee
|
||||
per_trampoline_fees -= delta_fee
|
||||
route = [
|
||||
RouteEdge(
|
||||
start_node=self.node_keypair.pubkey,
|
||||
end_node=trampoline_node_id,
|
||||
short_channel_id=chan.short_channel_id,
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_expiry_delta=0,
|
||||
node_features=trampoline_features)
|
||||
]
|
||||
self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}')
|
||||
routes.append((route, part_amount_msat_with_fees, per_trampoline_amount_with_fees, part_amount_msat, per_trampoline_cltv_delta, per_trampoline_secret, trampoline_onion, trampoline_route))
|
||||
if per_trampoline_fees != 0:
|
||||
self.logger.info('not enough margin to pay trampoline fee')
|
||||
raise NoPathFound()
|
||||
for route in routes:
|
||||
yield route
|
||||
return
|
||||
except NoPathFound:
|
||||
continue
|
||||
else:
|
||||
split_configurations = suggest_splits(
|
||||
amount_msat,
|
||||
channels_with_funds,
|
||||
exclude_single_part_payments=True,
|
||||
)
|
||||
# We atomically loop through a split configuration. If there was
|
||||
# a failure to find a path for a single part, we try the next configuration.
|
||||
self.logger.info(f'suggest_split {amount_msat} returned {len(split_configurations)} configurations')
|
||||
for sc in split_configurations:
|
||||
self.logger.info(f"trying split configuration: {list(sc.config.values())} rating: {sc.rating}")
|
||||
sc_routes = []
|
||||
sc_success = True
|
||||
margin = chan.available_to_spend(LOCAL, strict=True) - part_amount_msat
|
||||
delta_fee = min(per_trampoline_fees, margin)
|
||||
# TODO: distribute trampoline fee over several channels?
|
||||
part_amount_msat_with_fees = part_amount_msat + delta_fee
|
||||
per_trampoline_fees -= delta_fee
|
||||
route = [
|
||||
RouteEdge(
|
||||
start_node=self.node_keypair.pubkey,
|
||||
end_node=trampoline_node_id,
|
||||
short_channel_id=chan.short_channel_id,
|
||||
fee_base_msat=0,
|
||||
fee_proportional_millionths=0,
|
||||
cltv_expiry_delta=0,
|
||||
node_features=trampoline_features)
|
||||
]
|
||||
self.logger.info(f'adding route {part_amount_msat} {delta_fee} {margin}')
|
||||
routes.append((route, part_amount_msat_with_fees, per_trampoline_amount_with_fees, part_amount_msat, per_trampoline_cltv_delta, per_trampoline_secret, trampoline_onion, trampoline_route))
|
||||
if per_trampoline_fees != 0:
|
||||
self.logger.info('not enough margin to pay trampoline fee')
|
||||
raise NoPathFound()
|
||||
else:
|
||||
# We atomically loop through a split configuration. If there was
|
||||
# a failure to find a path for a single part, we try the next configuration
|
||||
for (chan_id, _), part_amounts_msat in sc.config.items():
|
||||
for part_amount_msat in part_amounts_msat:
|
||||
channel = self.channels[chan_id]
|
||||
try:
|
||||
route = await run_in_thread(
|
||||
partial(
|
||||
self.create_route_for_payment,
|
||||
amount_msat=part_amount_msat,
|
||||
invoice_pubkey=invoice_pubkey,
|
||||
min_cltv_expiry=min_cltv_expiry,
|
||||
r_tags=r_tags,
|
||||
invoice_features=invoice_features,
|
||||
my_sending_channels=[channel],
|
||||
full_path=None
|
||||
)
|
||||
route = await run_in_thread(
|
||||
partial(
|
||||
self.create_route_for_payment,
|
||||
amount_msat=part_amount_msat,
|
||||
invoice_pubkey=invoice_pubkey,
|
||||
min_cltv_expiry=min_cltv_expiry,
|
||||
r_tags=r_tags,
|
||||
invoice_features=invoice_features,
|
||||
my_sending_channels=[channel] if is_mpp else my_active_channels,
|
||||
full_path=full_path,
|
||||
)
|
||||
sc_routes.append((route, part_amount_msat, final_total_msat, part_amount_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion, None))
|
||||
except NoPathFound:
|
||||
sc_success = False
|
||||
break
|
||||
if sc_success:
|
||||
for r in sc_routes:
|
||||
yield r
|
||||
return
|
||||
else:
|
||||
continue
|
||||
raise NoPathFound()
|
||||
)
|
||||
routes.append((route, part_amount_msat, final_total_msat, part_amount_msat, min_cltv_expiry, payment_secret, fwd_trampoline_onion, None))
|
||||
except NoPathFound:
|
||||
continue
|
||||
for route in routes:
|
||||
yield route
|
||||
return
|
||||
raise NoPathFound()
|
||||
|
||||
@profiler
|
||||
def create_route_for_payment(
|
||||
|
||||
@@ -265,6 +265,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]):
|
||||
_handle_chanupd_from_failed_htlc = LNWallet._handle_chanupd_from_failed_htlc
|
||||
_on_maybe_forwarded_htlc_resolved = LNWallet._on_maybe_forwarded_htlc_resolved
|
||||
_force_close_channel = LNWallet._force_close_channel
|
||||
suggest_splits = LNWallet.suggest_splits
|
||||
|
||||
|
||||
class MockTransport:
|
||||
|
||||
Reference in New Issue
Block a user