Introduce RouteParameters::max_total_routing_fee_msat

Currently, users have no means to upper-bound the total fees accruing
when finding a route. Here, we add a corresponding field to
`RouteParameters` which will be used to limit the candidate set during
path finding in the following commits.
This commit is contained in:
Elias Rohrer 2023-07-14 11:47:22 +02:00
parent 7e7e7a0573
commit d7e2ff6220
No known key found for this signature in database
GPG key ID: 36153082BDF676FD
6 changed files with 85 additions and 39 deletions

View file

@ -116,10 +116,7 @@ fn pay_invoice_using_amount<P: Deref>(
if let Some(features) = invoice.features() {
payment_params = payment_params.with_bolt11_features(features.clone()).unwrap();
}
let route_params = RouteParameters {
payment_params,
final_value_msat: amount_msats,
};
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amount_msats);
payer.send_payment(payment_hash, recipient_onion, payment_id, route_params, retry_strategy)
}
@ -148,7 +145,7 @@ pub fn preflight_probe_invoice<C: AChannelManager>(
if let Some(features) = invoice.features() {
payment_params = payment_params.with_bolt11_features(features.clone()).unwrap();
}
let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amount_msat);
channelmanager.get_cm().send_preflight_probes(route_params, liquidity_limit_multiplier)
.map_err(ProbingError::Sending)
@ -178,7 +175,7 @@ pub fn preflight_probe_zero_value_invoice<C: AChannelManager>(
if let Some(features) = invoice.features() {
payment_params = payment_params.with_bolt11_features(features.clone()).unwrap();
}
let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amount_msat);
channelmanager.get_cm().send_preflight_probes(route_params, liquidity_limit_multiplier)
.map_err(ProbingError::Sending)

View file

@ -47,10 +47,10 @@ fn do_one_hop_blinded_path(success: bool) {
nodes[1].node.get_our_node_id(), payee_tlvs, &chanmon_cfgs[1].keys_manager, &secp_ctx
).unwrap();
let route_params = RouteParameters {
payment_params: PaymentParameters::blinded(vec![blinded_path]),
final_value_msat: amt_msat
};
let route_params = RouteParameters::from_payment_params_and_value(
PaymentParameters::blinded(vec![blinded_path]),
amt_msat,
);
nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(),
PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap();
check_added_monitors(&nodes[0], 1);
@ -90,11 +90,11 @@ fn mpp_to_one_hop_blinded_path() {
let bolt12_features: Bolt12InvoiceFeatures =
channelmanager::provided_invoice_features(&UserConfig::default()).to_context();
let route_params = RouteParameters {
payment_params: PaymentParameters::blinded(vec![blinded_path])
let route_params = RouteParameters::from_payment_params_and_value(
PaymentParameters::blinded(vec![blinded_path])
.with_bolt12_features(bolt12_features).unwrap(),
final_value_msat: amt_msat,
};
amt_msat,
);
nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap();
check_added_monitors(&nodes[0], 2);

View file

@ -3570,7 +3570,7 @@ where
let payment_params =
PaymentParameters::from_node_id(node_id, final_cltv_expiry_delta);
let route_params = RouteParameters { payment_params, final_value_msat: amount_msat };
let route_params = RouteParameters::from_payment_params_and_value(payment_params, amount_msat);
self.send_preflight_probes(route_params, liquidity_limit_multiplier)
}
@ -9559,6 +9559,7 @@ where
pending_fee_msat: Some(path_fee),
total_msat: path_amt,
starting_block_height: best_block_height,
remaining_max_total_routing_fee_msat: None, // only used for retries, and we'll never retry on startup
});
log_info!(args.logger, "Added a pending payment for {} msat with payment hash {} for path with session priv {}",
path_amt, &htlc.payment_hash, log_bytes!(session_priv_bytes));

View file

@ -54,10 +54,14 @@ pub(crate) enum PendingOutboundPayment {
AwaitingInvoice {
timer_ticks_without_response: u8,
retry_strategy: Retry,
max_total_routing_fee_msat: Option<u64>,
},
InvoiceReceived {
payment_hash: PaymentHash,
retry_strategy: Retry,
// Note this field is currently just replicated from AwaitingInvoice but not actually
// used anywhere.
max_total_routing_fee_msat: Option<u64>,
},
Retryable {
retry_strategy: Option<Retry>,
@ -76,6 +80,7 @@ pub(crate) enum PendingOutboundPayment {
total_msat: u64,
/// Our best known block height at the time this payment was initiated.
starting_block_height: u32,
remaining_max_total_routing_fee_msat: Option<u64>,
},
/// When a pending payment is fulfilled, we continue tracking it until all pending HTLCs have
/// been resolved. This ensures we don't look up pending payments in ChannelMonitors on restart
@ -731,12 +736,15 @@ impl OutboundPayments {
SP: Fn(SendAlongPathArgs) -> Result<(), APIError>,
{
let payment_hash = invoice.payment_hash();
let mut max_total_routing_fee_msat = None;
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
hash_map::Entry::Occupied(entry) => match entry.get() {
PendingOutboundPayment::AwaitingInvoice { retry_strategy, .. } => {
PendingOutboundPayment::AwaitingInvoice { retry_strategy, max_total_routing_fee_msat: max_total_fee, .. } => {
max_total_routing_fee_msat = *max_total_fee;
*entry.into_mut() = PendingOutboundPayment::InvoiceReceived {
payment_hash,
retry_strategy: *retry_strategy,
max_total_routing_fee_msat,
};
},
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
@ -747,6 +755,7 @@ impl OutboundPayments {
let route_params = RouteParameters {
payment_params: PaymentParameters::from_bolt12_invoice(&invoice),
final_value_msat: invoice.amount_msats(),
max_total_routing_fee_msat,
};
self.find_route_and_send_payment(
@ -779,11 +788,12 @@ impl OutboundPayments {
let mut retry_id_route_params = None;
for (pmt_id, pmt) in outbounds.iter_mut() {
if pmt.is_auto_retryable_now() {
if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, payment_params: Some(params), payment_hash, .. } = pmt {
if let PendingOutboundPayment::Retryable { pending_amt_msat, total_msat, payment_params: Some(params), payment_hash, remaining_max_total_routing_fee_msat, .. } = pmt {
if pending_amt_msat < total_msat {
retry_id_route_params = Some((*payment_hash, *pmt_id, RouteParameters {
final_value_msat: *total_msat - *pending_amt_msat,
payment_params: params.clone(),
max_total_routing_fee_msat: *remaining_max_total_routing_fee_msat,
}));
break
}
@ -987,7 +997,7 @@ impl OutboundPayments {
log_error!(logger, "Payment not yet sent");
return
},
PendingOutboundPayment::InvoiceReceived { payment_hash, retry_strategy } => {
PendingOutboundPayment::InvoiceReceived { payment_hash, retry_strategy, .. } => {
let total_amount = route_params.final_value_msat;
let recipient_onion = RecipientOnionFields {
payment_secret: None,
@ -1207,6 +1217,8 @@ impl OutboundPayments {
custom_tlvs: recipient_onion.custom_tlvs,
starting_block_height: best_block_height,
total_msat: route.get_total_amount(),
remaining_max_total_routing_fee_msat:
route.route_params.as_ref().and_then(|p| p.max_total_routing_fee_msat),
};
for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) {
@ -1218,7 +1230,7 @@ impl OutboundPayments {
#[allow(unused)]
pub(super) fn add_new_awaiting_invoice(
&self, payment_id: PaymentId, retry_strategy: Retry
&self, payment_id: PaymentId, retry_strategy: Retry, max_total_routing_fee_msat: Option<u64>
) -> Result<(), ()> {
let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap();
match pending_outbounds.entry(payment_id) {
@ -1227,6 +1239,7 @@ impl OutboundPayments {
entry.insert(PendingOutboundPayment::AwaitingInvoice {
timer_ticks_without_response: 0,
retry_strategy,
max_total_routing_fee_msat,
});
Ok(())
@ -1328,8 +1341,9 @@ impl OutboundPayments {
failed_paths_retry: if pending_amt_unsent != 0 {
if let Some(payment_params) = route.route_params.as_ref().map(|p| p.payment_params.clone()) {
Some(RouteParameters {
payment_params: payment_params,
payment_params,
final_value_msat: pending_amt_unsent,
max_total_routing_fee_msat: None,
})
} else { None }
} else { None },
@ -1689,6 +1703,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
(8, pending_amt_msat, required),
(9, custom_tlvs, optional_vec),
(10, starting_block_height, required),
(11, remaining_max_total_routing_fee_msat, option),
(not_written, retry_strategy, (static_value, None)),
(not_written, attempts, (static_value, PaymentAttempts::new())),
},
@ -1700,10 +1715,12 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment,
(5, AwaitingInvoice) => {
(0, timer_ticks_without_response, required),
(2, retry_strategy, required),
(4, max_total_routing_fee_msat, option),
},
(7, InvoiceReceived) => {
(0, payment_hash, required),
(2, retry_strategy, required),
(4, max_total_routing_fee_msat, option),
},
);
@ -1926,7 +1943,9 @@ mod tests {
let payment_id = PaymentId([0; 32]);
assert!(!outbound_payments.has_pending_payments());
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None).is_ok()
);
assert!(outbound_payments.has_pending_payments());
for _ in 0..INVOICE_REQUEST_TIMEOUT_TICKS {
@ -1944,10 +1963,15 @@ mod tests {
);
assert!(pending_events.lock().unwrap().is_empty());
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None).is_ok()
);
assert!(outbound_payments.has_pending_payments());
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_err());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None)
.is_err()
);
}
#[test]
@ -1957,7 +1981,9 @@ mod tests {
let payment_id = PaymentId([0; 32]);
assert!(!outbound_payments.has_pending_payments());
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None).is_ok()
);
assert!(outbound_payments.has_pending_payments());
outbound_payments.abandon_payment(
@ -1985,7 +2011,9 @@ mod tests {
let outbound_payments = OutboundPayments::new();
let payment_id = PaymentId([0; 32]);
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None).is_ok()
);
assert!(outbound_payments.has_pending_payments());
let created_at = now() - DEFAULT_RELATIVE_EXPIRY;
@ -2031,7 +2059,9 @@ mod tests {
let outbound_payments = OutboundPayments::new();
let payment_id = PaymentId([0; 32]);
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None).is_ok()
);
assert!(outbound_payments.has_pending_payments());
let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
@ -2045,10 +2075,10 @@ mod tests {
.sign(recipient_sign).unwrap();
router.expect_find_route(
RouteParameters {
payment_params: PaymentParameters::from_bolt12_invoice(&invoice),
final_value_msat: invoice.amount_msats(),
},
RouteParameters::from_payment_params_and_value(
PaymentParameters::from_bolt12_invoice(&invoice),
invoice.amount_msats(),
),
Err(LightningError { err: String::new(), action: ErrorAction::IgnoreError }),
);
@ -2084,7 +2114,9 @@ mod tests {
let outbound_payments = OutboundPayments::new();
let payment_id = PaymentId([0; 32]);
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), None).is_ok()
);
assert!(outbound_payments.has_pending_payments());
let invoice = OfferBuilder::new("foo".into(), recipient_pubkey())
@ -2097,10 +2129,10 @@ mod tests {
.build().unwrap()
.sign(recipient_sign).unwrap();
let route_params = RouteParameters {
payment_params: PaymentParameters::from_bolt12_invoice(&invoice),
final_value_msat: invoice.amount_msats(),
};
let route_params = RouteParameters::from_payment_params_and_value(
PaymentParameters::from_bolt12_invoice(&invoice),
invoice.amount_msats(),
);
router.expect_find_route(
route_params.clone(), Ok(Route { paths: vec![], route_params: Some(route_params) })
);
@ -2150,6 +2182,7 @@ mod tests {
let route_params = RouteParameters {
payment_params: PaymentParameters::from_bolt12_invoice(&invoice),
final_value_msat: invoice.amount_msats(),
max_total_routing_fee_msat: Some(1234),
};
router.expect_find_route(
route_params.clone(),
@ -2185,7 +2218,9 @@ mod tests {
assert!(!outbound_payments.has_pending_payments());
assert!(pending_events.lock().unwrap().is_empty());
assert!(outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0)).is_ok());
assert!(
outbound_payments.add_new_awaiting_invoice(payment_id, Retry::Attempts(0), Some(1234)).is_ok()
);
assert!(outbound_payments.has_pending_payments());
assert_eq!(

View file

@ -1337,7 +1337,7 @@ fn preflight_probes_yield_event_and_skip() {
let mut payment_params = PaymentParameters::from_node_id(nodes[4].node.get_our_node_id(), TEST_FINAL_CLTV)
.with_bolt11_features(invoice_features).unwrap();
let route_params = RouteParameters { payment_params, final_value_msat: 80_000_000 };
let route_params = RouteParameters::from_payment_params_and_value(payment_params, 80_000_000);
let res = nodes[0].node.send_preflight_probes(route_params, None).unwrap();
// We check that only one probe was sent, the other one was skipped due to limited liquidity.

View file

@ -409,6 +409,7 @@ impl Writeable for Route {
(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(())
}
@ -436,6 +437,7 @@ impl Readable for Route {
(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 {
@ -448,7 +450,7 @@ impl Readable for Route {
// 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,
};
@ -467,12 +469,20 @@ pub struct RouteParameters {
/// 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 }
}
}
@ -480,6 +490,7 @@ impl Writeable for RouteParameters {
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.
@ -493,6 +504,7 @@ impl Readable for RouteParameters {
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),
});
@ -505,6 +517,7 @@ impl Readable for RouteParameters {
Ok(Self {
payment_params,
final_value_msat: final_value_msat.0.unwrap(),
max_total_routing_fee_msat,
})
}
}