///
/// [`BlindedPath`]: crate::blinded_path::BlindedPath
pub cltv_expiry_delta: u32,
+ /// Indicates whether this hop is possibly announced in the public network graph.
+ ///
+ /// Will be `true` if there is a possibility that the channel is publicly known, i.e., if we
+ /// either know for sure it's announced in the public graph, or if any public channels exist
+ /// for which the given `short_channel_id` could be an alias for. Will be `false` if we believe
+ /// the channel to be unannounced.
+ ///
+ /// Will be `true` for objects serialized with LDK version 0.0.116 and before.
+ pub maybe_announced_channel: bool,
}
impl_writeable_tlv_based!(RouteHop, {
(0, pubkey, required),
+ (1, maybe_announced_channel, (default_value, true)),
(2, node_features, required),
(4, short_channel_id, required),
(6, channel_features, required),
overpaid_value_msat + self.paths.iter().map(|path| path.fee_msat()).sum::<u64>()
}
- /// Returns the total amount paid on this [`Route`], excluding the fees. Might be more than
- /// requested if we had to reach htlc_minimum_msat.
+ /// Returns the total amount paid on this [`Route`], excluding the fees.
+ ///
+ /// Might be more than requested as part of the given [`RouteParameters::final_value_msat`] if
+ /// we had to reach the [`htlc_minimum_msat`] limits.
+ ///
+ /// [`htlc_minimum_msat`]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message
pub fn get_total_amount(&self) -> u64 {
self.paths.iter().map(|path| path.final_value_msat()).sum()
}
(1, self.route_params.as_ref().map(|p| &p.payment_params), option),
(2, blinded_tails, optional_vec),
(3, self.route_params.as_ref().map(|p| p.final_value_msat), option),
+ (5, self.route_params.as_ref().map(|p| p.max_total_routing_fee_msat), option),
});
Ok(())
}
(1, payment_params, (option: ReadableArgs, min_final_cltv_expiry_delta)),
(2, blinded_tails, optional_vec),
(3, final_value_msat, option),
+ (5, max_total_routing_fee_msat, option)
});
let blinded_tails = blinded_tails.unwrap_or(Vec::new());
if blinded_tails.len() != 0 {
// If we previously wrote the corresponding fields, reconstruct RouteParameters.
let route_params = match (payment_params, final_value_msat) {
(Some(payment_params), Some(final_value_msat)) => {
- Some(RouteParameters { payment_params, final_value_msat })
+ Some(RouteParameters { payment_params, final_value_msat, max_total_routing_fee_msat })
}
_ => None,
};
/// The amount in msats sent on the failed payment path.
pub final_value_msat: u64,
+
+ /// The maximum total fees, in millisatoshi, that may accrue during route finding.
+ ///
+ /// This limit also applies to the total fees that may arise while retrying failed payment
+ /// paths.
+ ///
+ /// Default value: `None`
+ pub max_total_routing_fee_msat: Option<u64>,
}
impl RouteParameters {
/// Constructs [`RouteParameters`] from the given [`PaymentParameters`] and a payment amount.
pub fn from_payment_params_and_value(payment_params: PaymentParameters, final_value_msat: u64) -> Self {
- Self { payment_params, final_value_msat }
+ Self { payment_params, final_value_msat, max_total_routing_fee_msat: None }
}
}
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
write_tlv_fields!(writer, {
(0, self.payment_params, required),
+ (1, self.max_total_routing_fee_msat, option),
(2, self.final_value_msat, required),
// LDK versions prior to 0.0.114 had the `final_cltv_expiry_delta` parameter in
// `RouteParameters` directly. For compatibility, we write it here.
fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
_init_and_read_len_prefixed_tlv_fields!(reader, {
(0, payment_params, (required: ReadableArgs, 0)),
+ (1, max_total_routing_fee_msat, option),
(2, final_value_msat, required),
(4, final_cltv_delta, option),
});
Ok(Self {
payment_params,
final_value_msat: final_value_msat.0.unwrap(),
+ max_total_routing_fee_msat,
})
}
}
let mut num_ignored_path_length_limit = 0;
let mut num_ignored_cltv_delta_limit = 0;
let mut num_ignored_previously_failed = 0;
+ let mut num_ignored_total_fee_limit = 0;
macro_rules! add_entry {
// Adds entry which goes from $src_node_id to $dest_node_id over the $candidate hop.
total_fee_msat = total_fee_msat.saturating_add(hop_use_fee_msat);
}
- let channel_usage = ChannelUsage {
- amount_msat: amount_to_transfer_over_msat,
- inflight_htlc_msat: used_liquidity_msat,
- effective_capacity,
- };
- let channel_penalty_msat = scid_opt.map_or(0,
- |scid| scorer.channel_penalty_msat(scid, &$src_node_id, &$dest_node_id,
- channel_usage, score_params));
- let path_penalty_msat = $next_hops_path_penalty_msat
- .saturating_add(channel_penalty_msat);
- let new_graph_node = RouteGraphNode {
- node_id: $src_node_id,
- lowest_fee_to_node: total_fee_msat,
- total_cltv_delta: hop_total_cltv_delta,
- value_contribution_msat,
- path_htlc_minimum_msat,
- path_penalty_msat,
- path_length_to_node,
- };
-
- // Update the way of reaching $src_node_id with the given short_channel_id (from $dest_node_id),
- // if this way is cheaper than the already known
- // (considering the cost to "reach" this channel from the route destination,
- // the cost of using this channel,
- // and the cost of routing to the source node of this channel).
- // Also, consider that htlc_minimum_msat_difference, because we might end up
- // paying it. Consider the following exploit:
- // we use 2 paths to transfer 1.5 BTC. One of them is 0-fee normal 1 BTC path,
- // and for the other one we picked a 1sat-fee path with htlc_minimum_msat of
- // 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
- // by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
- // to this channel.
- // Ideally the scoring could be smarter (e.g. 0.5*htlc_minimum_msat here),
- // but it may require additional tracking - we don't want to double-count
- // the fees included in $next_hops_path_htlc_minimum_msat, but also
- // can't use something that may decrease on future hops.
- let old_cost = cmp::max(old_entry.total_fee_msat, old_entry.path_htlc_minimum_msat)
- .saturating_add(old_entry.path_penalty_msat);
- let new_cost = cmp::max(total_fee_msat, path_htlc_minimum_msat)
- .saturating_add(path_penalty_msat);
-
- if !old_entry.was_processed && new_cost < old_cost {
- targets.push(new_graph_node);
- old_entry.next_hops_fee_msat = $next_hops_fee_msat;
- old_entry.hop_use_fee_msat = hop_use_fee_msat;
- old_entry.total_fee_msat = total_fee_msat;
- old_entry.node_id = $dest_node_id.clone();
- old_entry.candidate = $candidate.clone();
- old_entry.fee_msat = 0; // This value will be later filled with hop_use_fee_msat of the following channel
- old_entry.path_htlc_minimum_msat = path_htlc_minimum_msat;
- old_entry.path_penalty_msat = path_penalty_msat;
- #[cfg(all(not(ldk_bench), any(test, fuzzing)))]
- {
- old_entry.value_contribution_msat = value_contribution_msat;
+ // Ignore hops if augmenting the current path to them would put us over `max_total_routing_fee_msat`
+ let max_total_routing_fee_msat = route_params.max_total_routing_fee_msat.unwrap_or(u64::max_value());
+ if total_fee_msat > max_total_routing_fee_msat {
+ if should_log_candidate {
+ log_trace!(logger, "Ignoring {} due to exceeding max total routing fee limit.", LoggedCandidateHop(&$candidate));
}
- did_add_update_path_to_src_node = Some(value_contribution_msat);
- } else if old_entry.was_processed && new_cost < old_cost {
- #[cfg(all(not(ldk_bench), any(test, fuzzing)))]
- {
- // If we're skipping processing a node which was previously
- // processed even though we found another path to it with a
- // cheaper fee, check that it was because the second path we
- // found (which we are processing now) has a lower value
- // contribution due to an HTLC minimum limit.
- //
- // e.g. take a graph with two paths from node 1 to node 2, one
- // through channel A, and one through channel B. Channel A and
- // B are both in the to-process heap, with their scores set by
- // a higher htlc_minimum than fee.
- // Channel A is processed first, and the channels onwards from
- // node 1 are added to the to-process heap. Thereafter, we pop
- // Channel B off of the heap, note that it has a much more
- // restrictive htlc_maximum_msat, and recalculate the fees for
- // all of node 1's channels using the new, reduced, amount.
- //
- // This would be bogus - we'd be selecting a higher-fee path
- // with a lower htlc_maximum_msat instead of the one we'd
- // already decided to use.
- debug_assert!(path_htlc_minimum_msat < old_entry.path_htlc_minimum_msat);
- debug_assert!(
- value_contribution_msat + path_penalty_msat <
- old_entry.value_contribution_msat + old_entry.path_penalty_msat
- );
+ num_ignored_total_fee_limit += 1;
+ } else {
+ let channel_usage = ChannelUsage {
+ amount_msat: amount_to_transfer_over_msat,
+ inflight_htlc_msat: used_liquidity_msat,
+ effective_capacity,
+ };
+ let channel_penalty_msat = scid_opt.map_or(0,
+ |scid| scorer.channel_penalty_msat(scid, &$src_node_id, &$dest_node_id,
+ channel_usage, score_params));
+ let path_penalty_msat = $next_hops_path_penalty_msat
+ .saturating_add(channel_penalty_msat);
+ let new_graph_node = RouteGraphNode {
+ node_id: $src_node_id,
+ lowest_fee_to_node: total_fee_msat,
+ total_cltv_delta: hop_total_cltv_delta,
+ value_contribution_msat,
+ path_htlc_minimum_msat,
+ path_penalty_msat,
+ path_length_to_node,
+ };
+
+ // Update the way of reaching $src_node_id with the given short_channel_id (from $dest_node_id),
+ // if this way is cheaper than the already known
+ // (considering the cost to "reach" this channel from the route destination,
+ // the cost of using this channel,
+ // and the cost of routing to the source node of this channel).
+ // Also, consider that htlc_minimum_msat_difference, because we might end up
+ // paying it. Consider the following exploit:
+ // we use 2 paths to transfer 1.5 BTC. One of them is 0-fee normal 1 BTC path,
+ // and for the other one we picked a 1sat-fee path with htlc_minimum_msat of
+ // 1 BTC. Now, since the latter is more expensive, we gonna try to cut it
+ // by 0.5 BTC, but then match htlc_minimum_msat by paying a fee of 0.5 BTC
+ // to this channel.
+ // Ideally the scoring could be smarter (e.g. 0.5*htlc_minimum_msat here),
+ // but it may require additional tracking - we don't want to double-count
+ // the fees included in $next_hops_path_htlc_minimum_msat, but also
+ // can't use something that may decrease on future hops.
+ let old_cost = cmp::max(old_entry.total_fee_msat, old_entry.path_htlc_minimum_msat)
+ .saturating_add(old_entry.path_penalty_msat);
+ let new_cost = cmp::max(total_fee_msat, path_htlc_minimum_msat)
+ .saturating_add(path_penalty_msat);
+
+ if !old_entry.was_processed && new_cost < old_cost {
+ targets.push(new_graph_node);
+ old_entry.next_hops_fee_msat = $next_hops_fee_msat;
+ old_entry.hop_use_fee_msat = hop_use_fee_msat;
+ old_entry.total_fee_msat = total_fee_msat;
+ old_entry.node_id = $dest_node_id.clone();
+ old_entry.candidate = $candidate.clone();
+ old_entry.fee_msat = 0; // This value will be later filled with hop_use_fee_msat of the following channel
+ old_entry.path_htlc_minimum_msat = path_htlc_minimum_msat;
+ old_entry.path_penalty_msat = path_penalty_msat;
+ #[cfg(all(not(ldk_bench), any(test, fuzzing)))]
+ {
+ old_entry.value_contribution_msat = value_contribution_msat;
+ }
+ did_add_update_path_to_src_node = Some(value_contribution_msat);
+ } else if old_entry.was_processed && new_cost < old_cost {
+ #[cfg(all(not(ldk_bench), any(test, fuzzing)))]
+ {
+ // If we're skipping processing a node which was previously
+ // processed even though we found another path to it with a
+ // cheaper fee, check that it was because the second path we
+ // found (which we are processing now) has a lower value
+ // contribution due to an HTLC minimum limit.
+ //
+ // e.g. take a graph with two paths from node 1 to node 2, one
+ // through channel A, and one through channel B. Channel A and
+ // B are both in the to-process heap, with their scores set by
+ // a higher htlc_minimum than fee.
+ // Channel A is processed first, and the channels onwards from
+ // node 1 are added to the to-process heap. Thereafter, we pop
+ // Channel B off of the heap, note that it has a much more
+ // restrictive htlc_maximum_msat, and recalculate the fees for
+ // all of node 1's channels using the new, reduced, amount.
+ //
+ // This would be bogus - we'd be selecting a higher-fee path
+ // with a lower htlc_maximum_msat instead of the one we'd
+ // already decided to use.
+ debug_assert!(path_htlc_minimum_msat < old_entry.path_htlc_minimum_msat);
+ debug_assert!(
+ value_contribution_msat + path_penalty_msat <
+ old_entry.value_contribution_msat + old_entry.path_penalty_msat
+ );
+ }
}
}
}
}
let num_ignored_total = num_ignored_value_contribution + num_ignored_path_length_limit +
- num_ignored_cltv_delta_limit + num_ignored_previously_failed;
+ num_ignored_cltv_delta_limit + num_ignored_previously_failed + num_ignored_total_fee_limit;
if num_ignored_total > 0 {
- log_trace!(logger, "Ignored {} candidate hops due to insufficient value contribution, {} due to path length limit, {} due to CLTV delta limit, {} due to previous payment failure. Total: {} ignored candidates.", num_ignored_value_contribution, num_ignored_path_length_limit, num_ignored_cltv_delta_limit, num_ignored_previously_failed, num_ignored_total);
+ log_trace!(logger, "Ignored {} candidate hops due to insufficient value contribution, {} due to path length limit, {} due to CLTV delta limit, {} due to previous payment failure, {} due to maximum total fee limit. Total: {} ignored candidates.", num_ignored_value_contribution, num_ignored_path_length_limit, num_ignored_cltv_delta_limit, num_ignored_previously_failed, num_ignored_total_fee_limit, num_ignored_total);
}
// Step (5).
let mut paths = Vec::new();
for payment_path in selected_route {
let mut hops = Vec::with_capacity(payment_path.hops.len());
+ let mut prev_hop_node_id = our_node_id;
for (hop, node_features) in payment_path.hops.iter()
.filter(|(h, _)| h.candidate.short_channel_id().is_some())
{
+ let maybe_announced_channel = if let CandidateRouteHop::PublicHop { .. } = hop.candidate {
+ // If we sourced the hop from the graph we're sure the target node is announced.
+ true
+ } else if let CandidateRouteHop::FirstHop { details } = hop.candidate {
+ // If this is a first hop we also know if it's announced.
+ details.is_public
+ } else {
+ // If we sourced it any other way, we double-check the network graph to see if
+ // there are announced channels between the endpoints. If so, the hop might be
+ // referring to any of the announced channels, as its `short_channel_id` might be
+ // an alias, in which case we don't take any chances here.
+ network_graph.node(&hop.node_id).map_or(false, |hop_node|
+ hop_node.channels.iter().any(|scid| network_graph.channel(*scid)
+ .map_or(false, |c| c.as_directed_from(&prev_hop_node_id).is_some()))
+ )
+ };
+
hops.push(RouteHop {
pubkey: PublicKey::from_slice(hop.node_id.as_slice()).map_err(|_| LightningError{err: format!("Public key {:?} is invalid", &hop.node_id), action: ErrorAction::IgnoreAndLog(Level::Trace)})?,
node_features: node_features.clone(),
channel_features: hop.candidate.features(),
fee_msat: hop.fee_msat,
cltv_expiry_delta: hop.candidate.cltv_expiry_delta(),
+ maybe_announced_channel,
});
+
+ prev_hop_node_id = hop.node_id;
}
let mut final_cltv_delta = final_cltv_expiry_delta;
let blinded_tail = payment_path.hops.last().and_then(|(h, _)| {
// Make sure we would never create a route with more paths than we allow.
debug_assert!(paths.len() <= payment_params.max_path_count.into());
+ // Make sure we would never create a route whose total fees exceed max_total_routing_fee_msat.
+ if let Some(max_total_routing_fee_msat) = route_params.max_total_routing_fee_msat {
+ if paths.iter().map(|p| p.fee_msat()).sum::<u64>() > max_total_routing_fee_msat {
+ return Err(LightningError{err: format!("Failed to find route that adheres to the maximum total fee limit of {}msat",
+ max_total_routing_fee_msat), action: ErrorAction::IgnoreError});
+ }
+ }
+
if let Some(node_features) = payment_params.payee.node_features() {
for path in paths.iter_mut() {
path.hops.last_mut().unwrap().node_features = node_features.clone();
} else { panic!(); }
}
+ {
+ // Attempt to route while setting max_total_routing_fee_msat to 149_999 results in a failure.
+ let route_params = RouteParameters { payment_params: payment_params.clone(), final_value_msat: 200_000,
+ max_total_routing_fee_msat: Some(149_999) };
+ if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(
+ &our_id, &route_params, &network_graph.read_only(), None, Arc::clone(&logger),
+ &scorer, &(), &random_seed_bytes) {
+ assert_eq!(err, "Failed to find a sufficient route to the given destination");
+ } else { panic!(); }
+ }
+
{
// Now, attempt to route 200 sats (exact amount we can route).
- let route_params = RouteParameters::from_payment_params_and_value(payment_params, 200_000);
+ let route_params = RouteParameters { payment_params: payment_params.clone(), final_value_msat: 200_000,
+ max_total_routing_fee_msat: Some(150_000) };
let route = get_route(&our_id, &route_params, &network_graph.read_only(), None,
Arc::clone(&logger), &scorer, &(), &random_seed_bytes).unwrap();
assert_eq!(route.paths.len(), 2);
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
], blinded_tail: None }],
route_params: None,
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
], blinded_tail: None }, Path { hops: vec![
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 100, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
RouteHop {
pubkey: PublicKey::from_slice(&hex::decode("0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c").unwrap()[..]).unwrap(),
channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
- short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
+ short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true,
},
], blinded_tail: None }],
route_params: None,
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}],
blinded_tail: Some(BlindedTail {
hops: blinded_path_1.blinded_hops,
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: true,
}], blinded_tail: None }],
route_params: None,
};
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
},
RouteHop {
pubkey: blinded_path.introduction_node_id,
channel_features: ChannelFeatures::empty(),
fee_msat: 1,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
}],
blinded_tail: Some(BlindedTail {
hops: blinded_path.blinded_hops,
channel_features: ChannelFeatures::empty(),
fee_msat: 100,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
},
RouteHop {
pubkey: blinded_path.introduction_node_id,
channel_features: ChannelFeatures::empty(),
fee_msat: 1,
cltv_expiry_delta: 0,
+ maybe_announced_channel: false,
}
],
blinded_tail: Some(BlindedTail {