diff --git a/lightning-invoice/src/payment.rs b/lightning-invoice/src/payment.rs index e672b89c919..997bc3efbf8 100644 --- a/lightning-invoice/src/payment.rs +++ b/lightning-invoice/src/payment.rs @@ -74,6 +74,12 @@ //! # &self, route: &Route, payment_id: PaymentId //! # ) -> Result<(), PaymentSendFailure> { unimplemented!() } //! # fn abandon_payment(&self, payment_id: PaymentId) { unimplemented!() } +//! # fn send_probe_payment( +//! # &self, route: &Route +//! # ) -> Result { unimplemented!() } +//! # fn payment_is_probe( +//! # &self, payment_hash: PaymentHash, payment_id: PaymentId +//! # ) -> bool { unimplemented!() } //! # } //! # //! # struct FakeRouter {} @@ -82,6 +88,10 @@ //! # &self, payer: &PublicKey, params: &RouteParameters, payment_hash: &PaymentHash, //! # first_hops: Option<&[&ChannelDetails]>, scorer: &S //! # ) -> Result { unimplemented!() } +//! # +//! # fn build_route_from_hops( +//! # &self, _payer: &PublicKey, _hops: &[PublicKey], route_params: &RouteParameters +//! # ) -> Result { unimplemented!() } //! # } //! # //! # struct FakeScorer {} @@ -247,6 +257,12 @@ pub trait Payer { /// Signals that no further retries for the given payment will occur. fn abandon_payment(&self, payment_id: PaymentId); + + /// Send a payment probe over the given [`Route`]. + fn send_probe_payment(&self, route: &Route) -> Result; + + /// Returns whether payment with the given [`PaymentId`] and [`PaymentHash`] is a probe. + fn payment_is_probe(&self, payment_hash: PaymentHash, payment_id: PaymentId) -> bool; } /// A trait defining behavior for routing an [`Invoice`] payment. @@ -256,6 +272,11 @@ pub trait Router { &self, payer: &PublicKey, route_params: &RouteParameters, payment_hash: &PaymentHash, first_hops: Option<&[&ChannelDetails]>, scorer: &S ) -> Result; + + /// Builds a [`Route`] from `payer` along the given path. + fn build_route_from_hops( + &self, payer: &PublicKey, hops: &[PublicKey], params: &RouteParameters + ) -> Result; } /// Strategies available to retry payment path failures for an [`Invoice`]. @@ -411,6 +432,23 @@ where .map_err(|e| { self.payment_cache.lock().unwrap().remove(&payment_hash); e }) } + /// Sends a probe payment along the given path. The resulting payment will not be cached and + /// resulting failures will be handled differently from regular payments. + pub fn send_probe_along_path( + &self, pubkey: PublicKey, hops: &[PublicKey], amount_msats: u64, final_cltv_expiry_delta: u32 + ) -> Result { + let route_params = RouteParameters { + payment_params: PaymentParameters::for_keysend(pubkey), + final_value_msat: amount_msats, + final_cltv_expiry_delta, + }; + let payer = self.payer.node_id(); + let route = self.router.build_route_from_hops(&payer, hops, &route_params) + .map_err(|e| PaymentError::Routing(e))?; + + self.payer.send_probe_payment(&route).map_err(|e| PaymentError::Sending(e)) + } + fn pay_internal Result + Copy>( &self, params: &RouteParameters, payment_hash: PaymentHash, send_payment: F, ) -> Result { @@ -550,6 +588,17 @@ where Event::PaymentPathFailed { payment_id, payment_hash, rejected_by_dest, path, short_channel_id, retry, .. } => { + if let Some(payment_id) = payment_id { + // When the failed payment was a probe, we make sure to not penalize the last + // hop and then drop the event instead of handing it up to the user's event + // handler. + if self.payer.payment_is_probe(*payment_hash, *payment_id) { + let path = path.iter().collect::>(); + self.scorer.lock().payment_path_failed(&path, u64::max_value()); + return; + } + } + if let Some(short_channel_id) = short_channel_id { let path = path.iter().collect::>(); self.scorer.lock().payment_path_failed(&path, *short_channel_id); @@ -1402,6 +1451,14 @@ mod tests { payment_params: Some(route_params.payment_params.clone()), ..Self::route_for_value(route_params.final_value_msat) }) } + + fn build_route_from_hops( + &self, _payer: &PublicKey, _hops: &[PublicKey], route_params: &RouteParameters + ) -> Result { + Ok(Route { + payment_params: Some(route_params.payment_params.clone()), ..Self::route_for_value(route_params.final_value_msat) + }) + } } struct FailingRouter; @@ -1413,6 +1470,12 @@ mod tests { ) -> Result { Err(LightningError { err: String::new(), action: ErrorAction::IgnoreError }) } + + fn build_route_from_hops( + &self, _payer: &PublicKey, _hops: &[PublicKey], _route_params: &RouteParameters + ) -> Result { + Err(LightningError { err: String::new(), action: ErrorAction::IgnoreError }) + } } struct TestScorer { @@ -1604,6 +1667,17 @@ mod tests { } fn abandon_payment(&self, _payment_id: PaymentId) { } + + fn send_probe_payment(&self, route: &Route) -> Result { + // TODO: for now copied from spontaneous, figure out what to do here. + self.check_value_msats(Amount::Spontaneous(route.get_total_amount())); + self.check_attempts() + } + + fn payment_is_probe(&self, _payment_hash: PaymentHash, _payment_id: PaymentId) -> bool { + // TODO: figure out what to do here. + false + } } // *** Full Featured Functional Tests with a Real ChannelManager *** @@ -1616,6 +1690,12 @@ mod tests { ) -> Result { self.0.borrow_mut().pop_front().unwrap() } + + fn build_route_from_hops( + &self, _payer: &PublicKey, _hops: &[PublicKey], _route_params: &RouteParameters + ) -> Result { + self.0.borrow_mut().pop_front().unwrap() + } } impl ManualRouter { fn expect_find_route(&self, result: Result) { diff --git a/lightning-invoice/src/utils.rs b/lightning-invoice/src/utils.rs index e09fd836224..7b627fc84c0 100644 --- a/lightning-invoice/src/utils.rs +++ b/lightning-invoice/src/utils.rs @@ -16,7 +16,7 @@ use lightning::ln::channelmanager::{PhantomRouteHints, MIN_CLTV_EXPIRY_DELTA}; use lightning::ln::inbound_payment::{create, create_from_hash, ExpandedKey}; use lightning::ln::msgs::LightningError; use lightning::routing::gossip::{NetworkGraph, RoutingFees}; -use lightning::routing::router::{Route, RouteHint, RouteHintHop, RouteParameters, find_route}; +use lightning::routing::router::{Route, RouteHint, RouteHintHop, RouteParameters, find_route, build_route_from_hops}; use lightning::routing::scoring::Score; use lightning::util::logger::Logger; use secp256k1::PublicKey; @@ -469,6 +469,18 @@ where L::Target: Logger { }; find_route(payer, params, &network_graph, first_hops, &*self.logger, scorer, &random_seed_bytes) } + + fn build_route_from_hops( + &self, payer: &PublicKey, hops: &[PublicKey], params: &RouteParameters + ) -> Result { + let network_graph = self.network_graph.read_only(); + let random_seed_bytes = { + let mut locked_random_seed_bytes = self.random_seed_bytes.lock().unwrap(); + *locked_random_seed_bytes = sha256::Hash::hash(&*locked_random_seed_bytes).into_inner(); + *locked_random_seed_bytes + }; + build_route_from_hops(payer, hops, params, &network_graph, &*self.logger, &random_seed_bytes) + } } impl Payer for ChannelManager @@ -509,6 +521,14 @@ where fn abandon_payment(&self, payment_id: PaymentId) { self.abandon_payment(payment_id) } + + fn send_probe_payment(&self, route: &Route) -> Result { + self.send_probe_payment(route).map(|(_, payment_id)| payment_id) + } + + fn payment_is_probe(&self, payment_hash: PaymentHash, payment_id: PaymentId) -> bool { + self.payment_is_probe(payment_hash, payment_id) + } } #[cfg(test)]