simln_lib/
lnd.rs

1use std::collections::HashSet;
2use std::{collections::HashMap, str::FromStr};
3
4use crate::{
5    serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult,
6};
7use async_trait::async_trait;
8use bitcoin::hashes::{sha256, Hash};
9use bitcoin::secp256k1::PublicKey;
10use bitcoin::Network;
11use lightning::ln::features::NodeFeatures;
12use lightning::ln::{PaymentHash, PaymentPreimage};
13use serde::{Deserialize, Serialize};
14use tonic_lnd::lnrpc::{payment::PaymentStatus, GetInfoRequest, GetInfoResponse};
15use tonic_lnd::lnrpc::{ListChannelsRequest, NodeInfoRequest, PaymentFailureReason};
16use tonic_lnd::routerrpc::TrackPaymentRequest;
17use tonic_lnd::tonic::Code::Unavailable;
18use tonic_lnd::tonic::Status;
19use tonic_lnd::{routerrpc::SendPaymentRequest, Client};
20use triggered::Listener;
21
22const KEYSEND_KEY: u64 = 5482373484;
23const SEND_PAYMENT_TIMEOUT_SECS: i32 = 300;
24
25#[derive(Serialize, Deserialize, Debug, Clone)]
26pub struct LndConnection {
27    #[serde(with = "serializers::serde_node_id")]
28    pub id: NodeId,
29    pub address: String,
30    #[serde(deserialize_with = "serializers::deserialize_path")]
31    pub macaroon: String,
32    #[serde(deserialize_with = "serializers::deserialize_path")]
33    pub cert: String,
34}
35
36pub struct LndNode {
37    client: Client,
38    info: NodeInfo,
39}
40
41// TODO: We could even generalize this to parse any type of Features
42/// Parses the node features from the format returned by LND gRPC to LDK NodeFeatures
43fn parse_node_features(features: HashSet<u32>) -> NodeFeatures {
44    let mut flags = vec![0; 256];
45
46    for f in features.into_iter() {
47        let byte_offset = (f / 8) as usize;
48        let mask = 1 << (f - 8 * byte_offset as u32);
49        if flags.len() <= byte_offset {
50            flags.resize(byte_offset + 1, 0u8);
51        }
52
53        flags[byte_offset] |= mask
54    }
55
56    NodeFeatures::from_le_bytes(flags)
57}
58
59impl LndNode {
60    pub async fn new(connection: LndConnection) -> Result<Self, LightningError> {
61        let mut client =
62            tonic_lnd::connect(connection.address, connection.cert, connection.macaroon)
63                .await
64                .map_err(|err| LightningError::ConnectionError(err.to_string()))?;
65
66        let GetInfoResponse {
67            identity_pubkey,
68            features,
69            mut alias,
70            ..
71        } = client
72            .lightning()
73            .get_info(GetInfoRequest {})
74            .await
75            .map_err(|err| LightningError::GetInfoError(err.to_string()))?
76            .into_inner();
77
78        let pubkey = PublicKey::from_str(&identity_pubkey)
79            .map_err(|err| LightningError::GetInfoError(err.to_string()))?;
80        connection.id.validate(&pubkey, &mut alias)?;
81
82        Ok(Self {
83            client,
84            info: NodeInfo {
85                pubkey,
86                features: parse_node_features(features.keys().cloned().collect()),
87                alias,
88            },
89        })
90    }
91}
92
93#[async_trait]
94impl LightningNode for LndNode {
95    /// NOTE: This is cached now. We do call the node's RPC get_info method in the constructor and save the info there.
96    /// Currently, that info cannot be outdated, given we only store node_id, alias and features, but it may not be the case
97    /// if we end up storing some other info returned by the RPC call, such as the block height
98    fn get_info(&self) -> &NodeInfo {
99        &self.info
100    }
101
102    async fn get_network(&mut self) -> Result<Network, LightningError> {
103        let info = self
104            .client
105            .lightning()
106            .get_info(GetInfoRequest {})
107            .await
108            .map_err(|err| LightningError::GetInfoError(err.to_string()))?
109            .into_inner();
110
111        if info.chains.is_empty() {
112            return Err(LightningError::ValidationError(format!(
113                "{} is not connected any chain",
114                self.get_info()
115            )));
116        } else if info.chains.len() > 1 {
117            return Err(LightningError::ValidationError(format!(
118                "{} is connected to more than one chain: {:?}",
119                self.get_info(),
120                info.chains.iter().map(|c| c.chain.to_string())
121            )));
122        }
123
124        Ok(Network::from_str(match info.chains[0].network.as_str() {
125            "mainnet" => "bitcoin",
126            "simnet" => {
127                return Err(LightningError::ValidationError(
128                    "simnet is not supported".to_string(),
129                ))
130            },
131            x => x,
132        })
133        .map_err(|err| LightningError::ValidationError(err.to_string()))?)
134    }
135
136    async fn send_payment(
137        &mut self,
138        dest: PublicKey,
139        amount_msat: u64,
140    ) -> Result<PaymentHash, LightningError> {
141        let amt_msat: i64 = amount_msat
142            .try_into()
143            .map_err(|_| LightningError::SendPaymentError("Invalid send amount".to_string()))?;
144
145        let preimage = PaymentPreimage(rand::random());
146
147        let mut dest_custom_records = HashMap::new();
148        let payment_hash = sha256::Hash::hash(&preimage.0).to_byte_array().to_vec();
149        dest_custom_records.insert(KEYSEND_KEY, preimage.0.to_vec());
150
151        let response = self
152            .client
153            .router()
154            .send_payment_v2(SendPaymentRequest {
155                amt_msat,
156                dest: dest.serialize().to_vec(),
157                dest_custom_records,
158                payment_hash,
159                timeout_seconds: SEND_PAYMENT_TIMEOUT_SECS,
160                fee_limit_msat: i64::max_value(),
161                ..Default::default()
162            })
163            .await
164            .map_err(status_to_lightning_error)?;
165
166        let mut stream = response.into_inner();
167
168        let payment_hash = match stream.message().await.map_err(status_to_lightning_error)? {
169            Some(payment) => string_to_payment_hash(&payment.payment_hash)?,
170            None => return Err(LightningError::SendPaymentError("No payment".to_string())),
171        };
172
173        Ok(payment_hash)
174    }
175
176    async fn track_payment(
177        &mut self,
178        hash: &PaymentHash,
179        shutdown: Listener,
180    ) -> Result<PaymentResult, LightningError> {
181        let response = self
182            .client
183            .router()
184            .track_payment_v2(TrackPaymentRequest {
185                payment_hash: hash.0.to_vec(),
186                no_inflight_updates: true,
187            })
188            .await
189            .map_err(|err| LightningError::TrackPaymentError(err.to_string()))?;
190
191        let mut stream = response.into_inner();
192
193        tokio::select! {
194            biased;
195            _ = shutdown => { Err(LightningError::TrackPaymentError("Shutdown before tracking results".to_string())) },
196            stream = stream.message() => {
197                let payment = stream.map_err(|err| LightningError::TrackPaymentError(err.to_string()))?;
198                match payment {
199                    Some(payment) => {
200                        let payment_status: PaymentStatus = payment.status.try_into()
201                            .map_err(|_| LightningError::TrackPaymentError("Invalid payment status".to_string()))?;
202                        let failure_reason: PaymentFailureReason = payment.failure_reason.try_into()
203                            .map_err(|_| LightningError::TrackPaymentError("Invalid failure reason".to_string()))?;
204
205                        let payment_outcome = match payment_status {
206                            PaymentStatus::Succeeded => PaymentOutcome::Success,
207                            PaymentStatus::Failed => match failure_reason {
208                                PaymentFailureReason::FailureReasonTimeout => PaymentOutcome::PaymentExpired,
209                                PaymentFailureReason::FailureReasonNoRoute => PaymentOutcome::RouteNotFound,
210                                PaymentFailureReason::FailureReasonError => PaymentOutcome::UnexpectedError,
211                                PaymentFailureReason::FailureReasonIncorrectPaymentDetails => PaymentOutcome::IncorrectPaymentDetails,
212                                PaymentFailureReason::FailureReasonInsufficientBalance => PaymentOutcome::InsufficientBalance,
213                                // Payment status is Failed, but failure reason is None or unknown
214                                _ => return Err(LightningError::TrackPaymentError("Unexpected failure reason".to_string())),
215                            },
216                            // PaymentStatus::InFlight or PaymentStatus::Unknown
217                            _ => PaymentOutcome::Unknown,
218                        };
219                        return Ok(PaymentResult {
220                            htlc_count: payment.htlcs.len(),
221                            payment_outcome
222                        });
223                    },
224                    None => {
225                        return Err(LightningError::TrackPaymentError(
226                            "No payment".to_string(),
227                        ));
228                    },
229                }
230            },
231        }
232    }
233
234    async fn get_node_info(&mut self, node_id: &PublicKey) -> Result<NodeInfo, LightningError> {
235        let node_info = self
236            .client
237            .lightning()
238            .get_node_info(NodeInfoRequest {
239                pub_key: node_id.to_string(),
240                include_channels: false,
241            })
242            .await
243            .map_err(|err| LightningError::GetNodeInfoError(err.to_string()))?
244            .into_inner();
245
246        if let Some(node_info) = node_info.node {
247            Ok(NodeInfo {
248                pubkey: *node_id,
249                alias: node_info.alias,
250                features: parse_node_features(node_info.features.keys().cloned().collect()),
251            })
252        } else {
253            Err(LightningError::GetNodeInfoError(
254                "Node not found".to_string(),
255            ))
256        }
257    }
258
259    async fn list_channels(&mut self) -> Result<Vec<u64>, LightningError> {
260        let channels = self
261            .client
262            .lightning()
263            .list_channels(ListChannelsRequest {
264                ..Default::default()
265            })
266            .await
267            .map_err(|err| LightningError::ListChannelsError(err.to_string()))?
268            .into_inner();
269
270        // Capacity is returned in satoshis, so we convert to msat.
271        Ok(channels
272            .channels
273            .iter()
274            .map(|channel| 1000 * channel.capacity as u64)
275            .collect())
276    }
277}
278
279fn string_to_payment_hash(hash: &str) -> Result<PaymentHash, LightningError> {
280    let bytes = hex::decode(hash).map_err(|_| LightningError::InvalidPaymentHash)?;
281    let slice: [u8; 32] = bytes
282        .as_slice()
283        .try_into()
284        .map_err(|_| LightningError::InvalidPaymentHash)?;
285    Ok(PaymentHash(slice))
286}
287
288fn status_to_lightning_error(s: Status) -> LightningError {
289    let code = s.code();
290    let message = s.message();
291    match code {
292        Unavailable => LightningError::SendPaymentError(format!("Node unavailable: {message}")),
293        _ => LightningError::PermanentError(message.to_string()),
294    }
295}