Merge pull request #2209 from TheBlueMatt/2023-04-better-discon-err-msg
[rust-lightning] / lightning / src / routing / router.rs
index d4e7306c91c0c9ec2dd4a0c4360687901dcc6273..b26298391b2f9cb2fe3561b2b5786927a27ef6a2 100644 (file)
@@ -13,7 +13,7 @@ use bitcoin::secp256k1::PublicKey;
 use bitcoin::hashes::Hash;
 use bitcoin::hashes::sha256::Hash as Sha256;
 
-use crate::blinded_path::BlindedPath;
+use crate::blinded_path::{BlindedHop, BlindedPath};
 use crate::ln::PaymentHash;
 use crate::ln::channelmanager::{ChannelDetails, PaymentId};
 use crate::ln::features::{ChannelFeatures, InvoiceFeatures, NodeFeatures};
@@ -172,6 +172,12 @@ impl InFlightHtlcs {
        /// Takes in a path with payer's node id and adds the path's details to `InFlightHtlcs`.
        pub fn process_path(&mut self, path: &Path, payer_node_id: PublicKey) {
                if path.hops.is_empty() { return };
+
+               let mut cumulative_msat = 0;
+               if let Some(tail) = &path.blinded_tail {
+                       cumulative_msat += tail.final_value_msat;
+               }
+
                // total_inflight_map needs to be direction-sensitive when keeping track of the HTLC value
                // that is held up. However, the `hops` array, which is a path returned by `find_route` in
                // the router excludes the payer node. In the following lines, the payer's information is
@@ -180,7 +186,6 @@ impl InFlightHtlcs {
                let reversed_hops_with_payer = path.hops.iter().rev().skip(1)
                        .map(|hop| hop.pubkey)
                        .chain(core::iter::once(payer_node_id));
-               let mut cumulative_msat = 0;
 
                // Taking the reversed vector from above, we zip it with just the reversed hops list to
                // work "backwards" of the given path, since the last hop's `fee_msat` actually represents
@@ -227,10 +232,18 @@ pub struct RouteHop {
        /// to reach this node.
        pub channel_features: ChannelFeatures,
        /// The fee taken on this hop (for paying for the use of the *next* channel in the path).
-       /// For the last hop, this should be the full value of this path's part of the payment.
+       /// If this is the last hop in [`Path::hops`]:
+       /// * if we're sending to a [`BlindedPath`], this is the fee paid for use of the entire blinded path
+       /// * otherwise, this is the full value of this [`Path`]'s part of the payment
+       ///
+       /// [`BlindedPath`]: crate::blinded_path::BlindedPath
        pub fee_msat: u64,
-       /// The CLTV delta added for this hop. For the last hop, this is the CLTV delta expected at the
-       /// destination.
+       /// The CLTV delta added for this hop.
+       /// If this is the last hop in [`Path::hops`]:
+       /// * if we're sending to a [`BlindedPath`], this is the CLTV delta for the entire blinded path
+       /// * otherwise, this is the CLTV delta expected at the destination
+       ///
+       /// [`BlindedPath`]: crate::blinded_path::BlindedPath
        pub cltv_expiry_delta: u32,
 }
 
@@ -243,30 +256,71 @@ impl_writeable_tlv_based!(RouteHop, {
        (10, cltv_expiry_delta, required),
 });
 
-/// A path in a [`Route`] to the payment recipient.
+/// The blinded portion of a [`Path`], if we're routing to a recipient who provided blinded paths in
+/// their BOLT12 [`Invoice`].
+///
+/// [`Invoice`]: crate::offers::invoice::Invoice
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct BlindedTail {
+       /// The hops of the [`BlindedPath`] provided by the recipient.
+       ///
+       /// [`BlindedPath`]: crate::blinded_path::BlindedPath
+       pub hops: Vec<BlindedHop>,
+       /// The blinding point of the [`BlindedPath`] provided by the recipient.
+       ///
+       /// [`BlindedPath`]: crate::blinded_path::BlindedPath
+       pub blinding_point: PublicKey,
+       /// Excess CLTV delta added to the recipient's CLTV expiry to deter intermediate nodes from
+       /// inferring the destination. May be 0.
+       pub excess_final_cltv_expiry_delta: u32,
+       /// The total amount paid on this [`Path`], excluding the fees.
+       pub final_value_msat: u64,
+}
+
+impl_writeable_tlv_based!(BlindedTail, {
+       (0, hops, vec_type),
+       (2, blinding_point, required),
+       (4, excess_final_cltv_expiry_delta, required),
+       (6, final_value_msat, required),
+});
+
+/// A path in a [`Route`] to the payment recipient. Must always be at least length one.
+/// If no [`Path::blinded_tail`] is present, then [`Path::hops`] length may be up to 19.
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct Path {
-       /// The list of unblinded hops in this [`Path`].
+       /// The list of unblinded hops in this [`Path`]. Must be at least length one.
        pub hops: Vec<RouteHop>,
+       /// The blinded path at which this path terminates, if we're sending to one, and its metadata.
+       pub blinded_tail: Option<BlindedTail>,
 }
 
 impl Path {
        /// Gets the fees for a given path, excluding any excess paid to the recipient.
        pub fn fee_msat(&self) -> u64 {
-               // Do not count last hop of each path since that's the full value of the payment
-               self.hops.split_last().map(|(_, path_prefix)| path_prefix).unwrap_or(&[])
-                       .iter().map(|hop| &hop.fee_msat)
-                       .sum()
+               match &self.blinded_tail {
+                       Some(_) => self.hops.iter().map(|hop| hop.fee_msat).sum::<u64>(),
+                       None => {
+                               // Do not count last hop of each path since that's the full value of the payment
+                               self.hops.split_last().map_or(0,
+                                       |(_, path_prefix)| path_prefix.iter().map(|hop| hop.fee_msat).sum())
+                       }
+               }
        }
 
        /// Gets the total amount paid on this [`Path`], excluding the fees.
        pub fn final_value_msat(&self) -> u64 {
-               self.hops.last().map_or(0, |hop| hop.fee_msat)
+               match &self.blinded_tail {
+                       Some(blinded_tail) => blinded_tail.final_value_msat,
+                       None => self.hops.last().map_or(0, |hop| hop.fee_msat)
+               }
        }
 
        /// Gets the final hop's CLTV expiry delta.
-       pub fn final_cltv_expiry_delta(&self) -> u32 {
-               self.hops.last().map_or(0, |hop| hop.cltv_expiry_delta)
+       pub fn final_cltv_expiry_delta(&self) -> Option<u32> {
+               match &self.blinded_tail {
+                       Some(_) => None,
+                       None => self.hops.last().map(|hop| hop.cltv_expiry_delta)
+               }
        }
 }
 
@@ -274,11 +328,9 @@ impl Path {
 /// it can take multiple paths. Each path is composed of one or more hops through the network.
 #[derive(Clone, Hash, PartialEq, Eq)]
 pub struct Route {
-       /// The list of paths taken for a single (potentially-)multi-part payment. The pubkey of the
-       /// last [`RouteHop`] in each path must be the same. Each entry represents a list of hops, where
-       /// the last hop is the destination. Thus, this must always be at least length one. While the
-       /// maximum length of any given path is variable, keeping the length of any path less or equal to
-       /// 19 should currently ensure it is viable.
+       /// The list of [`Path`]s taken for a single (potentially-)multi-part payment. If no
+       /// [`BlindedTail`]s are present, then the pubkey of the last [`RouteHop`] in each path must be
+       /// the same.
        pub paths: Vec<Path>,
        /// The `payment_params` parameter passed to [`find_route`].
        /// This is used by `ChannelManager` to track information which may be required for retries,
@@ -311,14 +363,25 @@ impl Writeable for Route {
        fn write<W: crate::util::ser::Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
                write_ver_prefix!(writer, SERIALIZATION_VERSION, MIN_SERIALIZATION_VERSION);
                (self.paths.len() as u64).write(writer)?;
+               let mut blinded_tails = Vec::new();
                for path in self.paths.iter() {
                        (path.hops.len() as u8).write(writer)?;
-                       for hop in path.hops.iter() {
+                       for (idx, hop) in path.hops.iter().enumerate() {
                                hop.write(writer)?;
+                               if let Some(blinded_tail) = &path.blinded_tail {
+                                       if blinded_tails.is_empty() {
+                                               blinded_tails = Vec::with_capacity(path.hops.len());
+                                               for _ in 0..idx {
+                                                       blinded_tails.push(None);
+                                               }
+                                       }
+                                       blinded_tails.push(Some(blinded_tail));
+                               } else if !blinded_tails.is_empty() { blinded_tails.push(None); }
                        }
                }
                write_tlv_fields!(writer, {
                        (1, self.payment_params, option),
+                       (2, blinded_tails, optional_vec),
                });
                Ok(())
        }
@@ -340,12 +403,19 @@ impl Readable for Route {
                        if hops.is_empty() { return Err(DecodeError::InvalidValue); }
                        min_final_cltv_expiry_delta =
                                cmp::min(min_final_cltv_expiry_delta, hops.last().unwrap().cltv_expiry_delta);
-                       paths.push(Path { hops });
+                       paths.push(Path { hops, blinded_tail: None });
                }
-               let mut payment_params = None;
-               read_tlv_fields!(reader, {
+               _init_and_read_tlv_fields!(reader, {
                        (1, payment_params, (option: ReadableArgs, min_final_cltv_expiry_delta)),
+                       (2, blinded_tails, optional_vec),
                });
+               let blinded_tails = blinded_tails.unwrap_or(Vec::new());
+               if blinded_tails.len() != 0 {
+                       if blinded_tails.len() != paths.len() { return Err(DecodeError::InvalidValue) }
+                       for (mut path, blinded_tail_opt) in paths.iter_mut().zip(blinded_tails.into_iter()) {
+                               path.blinded_tail = blinded_tail_opt;
+                       }
+               }
                Ok(Route { paths, payment_params })
        }
 }
@@ -2021,7 +2091,7 @@ where L::Target: Logger {
        for results_vec in selected_paths {
                let mut hops = Vec::with_capacity(results_vec.len());
                for res in results_vec { hops.push(res?); }
-               paths.push(Path { hops });
+               paths.push(Path { hops, blinded_tail: None });
        }
        let route = Route {
                paths,
@@ -2110,6 +2180,10 @@ fn add_random_cltv_offset(route: &mut Route, payment_params: &PaymentParameters,
                shadow_ctlv_expiry_delta_offset = cmp::min(shadow_ctlv_expiry_delta_offset, max_path_offset);
 
                // Add 'shadow' CLTV offset to the final hop
+               if let Some(tail) = path.blinded_tail.as_mut() {
+                       tail.excess_final_cltv_expiry_delta = tail.excess_final_cltv_expiry_delta
+                               .checked_add(shadow_ctlv_expiry_delta_offset).unwrap_or(tail.excess_final_cltv_expiry_delta);
+               }
                if let Some(last_hop) = path.hops.last_mut() {
                        last_hop.cltv_expiry_delta = last_hop.cltv_expiry_delta
                                .checked_add(shadow_ctlv_expiry_delta_offset).unwrap_or(last_hop.cltv_expiry_delta);
@@ -2198,10 +2272,11 @@ fn build_route_from_hops_internal<L: Deref>(
 
 #[cfg(test)]
 mod tests {
+       use crate::blinded_path::{BlindedHop, BlindedPath};
        use crate::routing::gossip::{NetworkGraph, P2PGossipSync, NodeId, EffectiveCapacity};
        use crate::routing::utxo::UtxoResult;
        use crate::routing::router::{get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features,
-               Path, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RoutingFees,
+               BlindedTail, InFlightHtlcs, Path, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RoutingFees,
                DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE};
        use crate::routing::scoring::{ChannelUsage, FixedPenaltyScorer, Score, ProbabilisticScorer, ProbabilisticScoringParameters};
        use crate::routing::test_utils::{add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel};
@@ -2213,8 +2288,9 @@ mod tests {
        use crate::util::config::UserConfig;
        use crate::util::test_utils as ln_test_utils;
        use crate::util::chacha20::ChaCha20;
+       use crate::util::ser::{Readable, Writeable};
        #[cfg(c_bindings)]
-       use crate::util::ser::{Writeable, Writer};
+       use crate::util::ser::Writer;
 
        use bitcoin::hashes::Hash;
        use bitcoin::network::constants::Network;
@@ -2228,6 +2304,7 @@ mod tests {
        use bitcoin::secp256k1::{PublicKey,SecretKey};
        use bitcoin::secp256k1::Secp256k1;
 
+       use crate::io::Cursor;
        use crate::prelude::*;
        use crate::sync::Arc;
 
@@ -5272,7 +5349,7 @@ mod tests {
                                        channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
                                        short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0
                                },
-                       ]}],
+                       ], blinded_tail: None }],
                        payment_params: None,
                };
 
@@ -5294,7 +5371,7 @@ mod tests {
                                        channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
                                        short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
                                },
-                       ]}, Path { hops: vec![
+                       ], blinded_tail: None }, Path { hops: vec![
                                RouteHop {
                                        pubkey: PublicKey::from_slice(&hex::decode("02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619").unwrap()[..]).unwrap(),
                                        channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
@@ -5305,7 +5382,7 @@ mod tests {
                                        channel_features: ChannelFeatures::empty(), node_features: NodeFeatures::empty(),
                                        short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0
                                },
-                       ]}],
+                       ], blinded_tail: None }],
                        payment_params: None,
                };
 
@@ -5698,6 +5775,151 @@ mod tests {
                let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes);
                assert!(route.is_ok());
        }
+
+       #[test]
+       fn blinded_route_ser() {
+               let blinded_path_1 = BlindedPath {
+                       introduction_node_id: ln_test_utils::pubkey(42),
+                       blinding_point: ln_test_utils::pubkey(43),
+                       blinded_hops: vec![
+                               BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: Vec::new() },
+                               BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() }
+                       ],
+               };
+               let blinded_path_2 = BlindedPath {
+                       introduction_node_id: ln_test_utils::pubkey(46),
+                       blinding_point: ln_test_utils::pubkey(47),
+                       blinded_hops: vec![
+                               BlindedHop { blinded_node_id: ln_test_utils::pubkey(48), encrypted_payload: Vec::new() },
+                               BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }
+                       ],
+               };
+               // (De)serialize a Route with 1 blinded path out of two total paths.
+               let mut route = Route { paths: vec![Path {
+                       hops: vec![RouteHop {
+                               pubkey: ln_test_utils::pubkey(50),
+                               node_features: NodeFeatures::empty(),
+                               short_channel_id: 42,
+                               channel_features: ChannelFeatures::empty(),
+                               fee_msat: 100,
+                               cltv_expiry_delta: 0,
+                       }],
+                       blinded_tail: Some(BlindedTail {
+                               hops: blinded_path_1.blinded_hops,
+                               blinding_point: blinded_path_1.blinding_point,
+                               excess_final_cltv_expiry_delta: 40,
+                               final_value_msat: 100,
+                       })}, Path {
+                       hops: vec![RouteHop {
+                               pubkey: ln_test_utils::pubkey(51),
+                               node_features: NodeFeatures::empty(),
+                               short_channel_id: 43,
+                               channel_features: ChannelFeatures::empty(),
+                               fee_msat: 100,
+                               cltv_expiry_delta: 0,
+                       }], blinded_tail: None }],
+                       payment_params: None,
+               };
+               let encoded_route = route.encode();
+               let decoded_route: Route = Readable::read(&mut Cursor::new(&encoded_route[..])).unwrap();
+               assert_eq!(decoded_route.paths[0].blinded_tail, route.paths[0].blinded_tail);
+               assert_eq!(decoded_route.paths[1].blinded_tail, route.paths[1].blinded_tail);
+
+               // (De)serialize a Route with two paths, each containing a blinded tail.
+               route.paths[1].blinded_tail = Some(BlindedTail {
+                       hops: blinded_path_2.blinded_hops,
+                       blinding_point: blinded_path_2.blinding_point,
+                       excess_final_cltv_expiry_delta: 41,
+                       final_value_msat: 101,
+               });
+               let encoded_route = route.encode();
+               let decoded_route: Route = Readable::read(&mut Cursor::new(&encoded_route[..])).unwrap();
+               assert_eq!(decoded_route.paths[0].blinded_tail, route.paths[0].blinded_tail);
+               assert_eq!(decoded_route.paths[1].blinded_tail, route.paths[1].blinded_tail);
+       }
+
+       #[test]
+       fn blinded_path_inflight_processing() {
+               // Ensure we'll score the channel that's inbound to a blinded path's introduction node, and
+               // account for the blinded tail's final amount_msat.
+               let mut inflight_htlcs = InFlightHtlcs::new();
+               let blinded_path = BlindedPath {
+                       introduction_node_id: ln_test_utils::pubkey(43),
+                       blinding_point: ln_test_utils::pubkey(48),
+                       blinded_hops: vec![BlindedHop { blinded_node_id: ln_test_utils::pubkey(49), encrypted_payload: Vec::new() }],
+               };
+               let path = Path {
+                       hops: vec![RouteHop {
+                               pubkey: ln_test_utils::pubkey(42),
+                               node_features: NodeFeatures::empty(),
+                               short_channel_id: 42,
+                               channel_features: ChannelFeatures::empty(),
+                               fee_msat: 100,
+                               cltv_expiry_delta: 0,
+                       },
+                       RouteHop {
+                               pubkey: blinded_path.introduction_node_id,
+                               node_features: NodeFeatures::empty(),
+                               short_channel_id: 43,
+                               channel_features: ChannelFeatures::empty(),
+                               fee_msat: 1,
+                               cltv_expiry_delta: 0,
+                       }],
+                       blinded_tail: Some(BlindedTail {
+                               hops: blinded_path.blinded_hops,
+                               blinding_point: blinded_path.blinding_point,
+                               excess_final_cltv_expiry_delta: 0,
+                               final_value_msat: 200,
+                       }),
+               };
+               inflight_htlcs.process_path(&path, ln_test_utils::pubkey(44));
+               assert_eq!(*inflight_htlcs.0.get(&(42, true)).unwrap(), 301);
+               assert_eq!(*inflight_htlcs.0.get(&(43, false)).unwrap(), 201);
+       }
+
+       #[test]
+       fn blinded_path_cltv_shadow_offset() {
+               // Make sure we add a shadow offset when sending to blinded paths.
+               let blinded_path = BlindedPath {
+                       introduction_node_id: ln_test_utils::pubkey(43),
+                       blinding_point: ln_test_utils::pubkey(44),
+                       blinded_hops: vec![
+                               BlindedHop { blinded_node_id: ln_test_utils::pubkey(45), encrypted_payload: Vec::new() },
+                               BlindedHop { blinded_node_id: ln_test_utils::pubkey(46), encrypted_payload: Vec::new() }
+                       ],
+               };
+               let mut route = Route { paths: vec![Path {
+                       hops: vec![RouteHop {
+                               pubkey: ln_test_utils::pubkey(42),
+                               node_features: NodeFeatures::empty(),
+                               short_channel_id: 42,
+                               channel_features: ChannelFeatures::empty(),
+                               fee_msat: 100,
+                               cltv_expiry_delta: 0,
+                       },
+                       RouteHop {
+                               pubkey: blinded_path.introduction_node_id,
+                               node_features: NodeFeatures::empty(),
+                               short_channel_id: 43,
+                               channel_features: ChannelFeatures::empty(),
+                               fee_msat: 1,
+                               cltv_expiry_delta: 0,
+                       }
+                       ],
+                       blinded_tail: Some(BlindedTail {
+                               hops: blinded_path.blinded_hops,
+                               blinding_point: blinded_path.blinding_point,
+                               excess_final_cltv_expiry_delta: 0,
+                               final_value_msat: 200,
+                       }),
+               }], payment_params: None};
+
+               let payment_params = PaymentParameters::from_node_id(ln_test_utils::pubkey(47), 18);
+               let (_, network_graph, _, _, _) = build_line_graph();
+               add_random_cltv_offset(&mut route, &payment_params, &network_graph.read_only(), &[0; 32]);
+               assert_eq!(route.paths[0].blinded_tail.as_ref().unwrap().excess_final_cltv_expiry_delta, 40);
+               assert_eq!(route.paths[0].hops.last().unwrap().cltv_expiry_delta, 40);
+       }
 }
 
 #[cfg(all(test, not(feature = "no-std")))]