Browse Source

Factorize code in create_routes_for_payment.

Calling suggest_splits with exclude_single_part_payments=False
patch-4
ThomasV 2 years ago
parent
commit
20bcb15480
  1. 308
      electrum/lnworker.py
  2. 1
      electrum/tests/test_lnpeer.py

308
electrum/lnworker.py

@ -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(

1
electrum/tests/test_lnpeer.py

@ -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:

Loading…
Cancel
Save