Skip to main content

lexe_common/ln/
route.rs

1use std::{fmt, ops::Deref};
2
3use lightning::{
4    routing::{
5        gossip::{NetworkGraph, NodeId},
6        router::{BlindedTail, Path, Route, RouteHop},
7    },
8    util::logger::Logger,
9};
10#[cfg(any(test, feature = "test-utils"))]
11use proptest_derive::Arbitrary;
12use serde::{Deserialize, Serialize};
13
14use super::{amount::Amount, node_alias::LxNodeAlias};
15use crate::api::user::{NodePk, Scid};
16
17/// Newtype for [`Route`].
18#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
19#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
20pub struct LxRoute {
21    /// The [`LxPath`]s taken for a single (possibly multi-path) payment.
22    ///
23    /// If no [`LxBlindedTail`]s are present, then the pubkey of the last
24    /// [`LxRouteHop`] in each path must be the same.
25    pub paths: Vec<LxPath>,
26}
27
28impl LxRoute {
29    pub fn from_ldk<L: Deref<Target: Logger>>(
30        route: Route,
31        network_graph: &NetworkGraph<L>,
32    ) -> Self {
33        let mut paths = route
34            .paths
35            .into_iter()
36            .map(LxPath::from)
37            .collect::<Vec<_>>();
38
39        // Annotate all hops with node aliases known from our network graph.
40        {
41            let locked_graph = network_graph.read_only();
42
43            let lookup_alias = |node_pk: &NodePk| -> Option<LxNodeAlias> {
44                let node_id = NodeId::from_pubkey(&node_pk.0);
45                locked_graph
46                    .node(&node_id)
47                    .and_then(|node_info| node_info.announcement_info.as_ref())
48                    .map(|ann_info| LxNodeAlias::from(*ann_info.alias()))
49            };
50
51            for path in paths.iter_mut() {
52                for hop in path.hops.iter_mut() {
53                    hop.node_alias = lookup_alias(&hop.node_pk);
54                }
55            }
56        }
57
58        Self { paths }
59    }
60
61    /// Return the total amount paid on this [`LxRoute`], excluding the fees.
62    pub fn amount(&self) -> Amount {
63        self.paths.iter().map(LxPath::amount).sum()
64    }
65
66    /// Return the total fees on this [`LxRoute`].
67    pub fn fees(&self) -> Amount {
68        self.paths.iter().map(LxPath::fees).sum()
69    }
70}
71
72impl fmt::Display for LxRoute {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        let num_paths = self.paths.len();
75        for (i, path) in self.paths.iter().enumerate() {
76            write!(f, "{path}")?;
77            if i != num_paths - 1 {
78                write!(f, " | ")?;
79            }
80        }
81        Ok(())
82    }
83}
84
85/// Newtype for [`Path`].
86#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
87#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
88pub struct LxPath {
89    /// The unblinded hops in this [`Path`]. Must be at least length one.
90    pub hops: Vec<LxRouteHop>,
91    /// The blinded path at which this path terminates, if present.
92    pub blinded_tail: Option<LxBlindedTail>,
93}
94
95impl From<Path> for LxPath {
96    fn from(path: Path) -> Self {
97        LxPath {
98            hops: path.hops.into_iter().map(LxRouteHop::from).collect(),
99            blinded_tail: path.blinded_tail.map(LxBlindedTail::from),
100        }
101    }
102}
103
104impl fmt::Display for LxPath {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        let amount = self.amount();
107        let fees = self.fees();
108        write!(f, "[{amount} sat, {fees} fees: ")?;
109        let num_hops = self.hops.len();
110        for (i, hop) in self.hops.iter().enumerate() {
111            let hop_node_pk = hop.node_pk;
112            match hop.node_alias {
113                Some(alias) => write!(f, "({alias}) {hop_node_pk}")?,
114                None => write!(f, "{hop_node_pk}")?,
115            }
116            if i != num_hops - 1 {
117                write!(f, " -> ")?;
118            }
119        }
120        if let Some(tail) = &self.blinded_tail {
121            let num_hops = tail.num_hops;
122            write!(f, " -> blinded tail with {num_hops} hops")?;
123        }
124        write!(f, "]")?;
125        Ok(())
126    }
127}
128
129impl LxPath {
130    /// Return the amount paid on this [`LxPath`], excluding the fees.
131    pub fn amount(&self) -> Amount {
132        match self.blinded_tail.as_ref() {
133            Some(tail) => tail.final_value,
134            None => self
135                .hops
136                .last()
137                .map_or(Amount::ZERO, |hop| hop.fee_or_amount),
138        }
139    }
140
141    /// Gets the fees on this [`Path`], excluding any excess fees paid to the
142    /// recipient.
143    pub fn fees(&self) -> Amount {
144        match &self.blinded_tail {
145            // There is a blinded tail:
146            // - Non-last hops are fees
147            // - Last hop is the fee for the entire blinded path.
148            // => Sum `fee_or_amount` over all hops.
149            Some(_) => self
150                .hops
151                .iter()
152                .map(|hop| hop.fee_or_amount)
153                .sum::<Amount>(),
154            // There is no blinded tail:
155            // - Non-last hops are fees
156            // - Last hop is the amount paid, so it should be ignored
157            None => match self.hops.split_last() {
158                Some((_last, non_last)) =>
159                    non_last.iter().map(|hop| hop.fee_or_amount).sum::<Amount>(),
160                None => Amount::ZERO,
161            },
162        }
163    }
164}
165
166/// Newtype for [`RouteHop`].
167#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
168#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
169pub struct LxRouteHop {
170    /// The node_id of the node at this hop.
171    pub node_pk: NodePk,
172    /// The alias of the node at this hop, if known.
173    pub node_alias: Option<LxNodeAlias>,
174    /// The channel used from the previous hop to reach this node.
175    pub scid: Scid,
176    /// If this is NOT the last hop in [`LxPath::hops`], this is the fee taken
177    /// on this hop (for paying for the use of the *next* channel in the path).
178    ///
179    /// If this IS the last hop in [`LxPath::hops`]:
180    /// - If we're sending to a blinded payment path, this is the fee paid for
181    ///   use of the entire blinded path.
182    /// - Otherwise, this is the amount of this [`LxPath`]'s part of the
183    ///   payment.
184    pub fee_or_amount: Amount,
185    /// Whether we believe this channel is announced in the public graph.
186    pub announced: bool,
187}
188
189impl From<RouteHop> for LxRouteHop {
190    fn from(hop: RouteHop) -> Self {
191        Self {
192            node_pk: NodePk(hop.pubkey),
193            node_alias: None,
194            scid: Scid(hop.short_channel_id),
195            fee_or_amount: Amount::from_msat(hop.fee_msat),
196            announced: hop.maybe_announced_channel,
197        }
198    }
199}
200
201/// Newtype for [`BlindedTail`].
202#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
203#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
204pub struct LxBlindedTail {
205    pub num_hops: usize,
206    /// The total amount paid on this [`LxPath`], excluding the fees.
207    pub final_value: Amount,
208}
209
210impl From<BlindedTail> for LxBlindedTail {
211    fn from(tail: BlindedTail) -> Self {
212        Self {
213            num_hops: tail.hops.len(),
214            final_value: Amount::from_msat(tail.final_value_msat),
215        }
216    }
217}
218
219#[cfg(test)]
220mod test {
221    use lexe_crypto::rng::{FastRng, SysRng};
222    use proptest::prelude::any;
223
224    use super::*;
225    use crate::test_utils::arbitrary;
226
227    /// Check the [`fmt::Display`] implementation of [`LxRoute`].
228    ///
229    /// $ cargo test -p lexe-common display_route -- --ignored --nocapture
230    #[ignore]
231    #[test]
232    fn display_route() {
233        let mut rng = FastRng::from_sysrng(&mut SysRng::new());
234        let mut route = arbitrary::gen_value(&mut rng, any::<LxRoute>());
235        route.paths.truncate(3);
236        for path in route.paths.iter_mut() {
237            path.hops.truncate(3);
238        }
239        println!("Route: {route}");
240    }
241}