From 3e4cf01ceab76056005aaf38b70f26dde8626117 Mon Sep 17 00:00:00 2001 From: Orbital Date: Sun, 28 Jan 2024 22:49:10 -0600 Subject: [PATCH] offers: Build a reply path for invoice request --- src/lndk_offers.rs | 119 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 3 deletions(-) diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs index 2c67230b..b6e11f91 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -1,10 +1,10 @@ -use crate::lnd::{InvoicePayer, MessageSigner, PeerConnector}; +use crate::lnd::{features_support_onion_messages, InvoicePayer, MessageSigner, PeerConnector}; use crate::OfferHandler; use async_trait::async_trait; use bitcoin::hashes::sha256::Hash; use bitcoin::network::constants::Network; use bitcoin::secp256k1::schnorr::Signature; -use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey}; +use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey, Secp256k1}; use futures::executor::block_on; use lightning::blinded_path::BlindedPath; use lightning::ln::channelmanager::PaymentId; @@ -13,8 +13,10 @@ use lightning::offers::merkle::SignError; use lightning::offers::offer::{Amount, Offer}; use lightning::offers::parse::{Bolt12ParseError, Bolt12SemanticError}; use lightning::sign::EntropySource; +use log::error; use std::error::Error; use std::fmt::Display; +use std::str::FromStr; use tokio::task; use tonic_lnd::lnrpc::{ HtlcAttempt, LightningNode, ListPeersRequest, ListPeersResponse, QueryRoutesResponse, Route, @@ -44,6 +46,10 @@ pub enum OfferError { NodeAddressNotFound, /// Unable to find or send to payment route. RouteFailure(Status), + /// Cannot list peers. + ListPeersFailure(Status), + /// Failure to build a reply path. + BuildBlindedPathFailure, } impl Display for OfferError { @@ -63,6 +69,8 @@ impl Display for OfferError { OfferError::PeerConnectError(e) => write!(f, "Error connecting to peer: {e:?}"), OfferError::NodeAddressNotFound => write!(f, "Couldn't get node address"), OfferError::RouteFailure(e) => write!(f, "Error routing payment: {e:?}"), + OfferError::ListPeersFailure(e) => write!(f, "Error listing peers: {e:?}"), + OfferError::BuildBlindedPathFailure => write!(f, "Error building blinded path"), } } } @@ -208,6 +216,50 @@ impl OfferHandler { .await .unwrap() } + + // create_reply_path creates a blinded path to provide to the offer maker when requesting an + // invoice so they know where to send the invoice back to. + pub async fn create_reply_path( + &self, + mut connector: impl PeerConnector + std::marker::Send + 'static, + node_id: PublicKey, + ) -> Result> { + // Find an introduction node for our blinded path. + let current_peers = connector.list_peers().await.map_err(|e| { + error!("Could not lookup current peers: {e}."); + OfferError::ListPeersFailure(e) + })?; + + let mut intro_node = None; + for peer in current_peers.peers { + let pubkey = PublicKey::from_str(&peer.pub_key).unwrap(); + let onion_support = features_support_onion_messages(&peer.features); + if onion_support { + intro_node = Some(pubkey); + } + } + + let secp_ctx = Secp256k1::new(); + if intro_node.is_none() { + Ok( + BlindedPath::one_hop_for_message(node_id, &self.messenger_utils, &secp_ctx) + .map_err(|_| { + error!("Could not create blinded path."); + OfferError::BuildBlindedPathFailure + })?, + ) + } else { + Ok(BlindedPath::new_for_message( + &[intro_node.unwrap()], + &self.messenger_utils, + &secp_ctx, + ) + .map_err(|_| { + error!("Could not create blinded path."); + OfferError::BuildBlindedPathFailure + }))? + } + } } // pay_invoice tries to pay the provided invoice. @@ -388,11 +440,12 @@ impl InvoicePayer for Client { mod tests { use super::*; use crate::MessengerUtilities; - use bitcoin::secp256k1::{Error as Secp256k1Error, KeyPair, Secp256k1, SecretKey}; + use bitcoin::secp256k1::{Error as Secp256k1Error, KeyPair, SecretKey}; use core::convert::Infallible; use lightning::offers::merkle::SignError; use lightning::offers::offer::{OfferBuilder, Quantity}; use mockall::mock; + use std::collections::HashMap; use std::str::FromStr; use std::time::{Duration, SystemTime}; use tokio::sync::mpsc; @@ -749,4 +802,64 @@ mod tests { .is_err() ); } + + #[tokio::test] + async fn test_create_reply_path() { + let mut connector_mock = MockTestPeerConnector::new(); + + connector_mock.expect_list_peers().returning(|| { + let feature = tonic_lnd::lnrpc::Feature { + ..Default::default() + }; + let mut feature_entry = HashMap::new(); + feature_entry.insert(38, feature); + + let peer = tonic_lnd::lnrpc::Peer { + pub_key: get_pubkey(), + features: feature_entry, + ..Default::default() + }; + Ok(ListPeersResponse { peers: vec![peer] }) + }); + + let receiver_node_id = PublicKey::from_str(&get_pubkey()).unwrap(); + let handler = OfferHandler::new(); + assert!(handler + .create_reply_path(connector_mock, receiver_node_id) + .await + .is_ok()) + } + + #[tokio::test] + // Test that create_reply_path works fine when no suitable introduction node peer is found. + async fn test_create_reply_path_no_intro_node() { + let mut connector_mock = MockTestPeerConnector::new(); + + connector_mock + .expect_list_peers() + .returning(|| Ok(ListPeersResponse { peers: vec![] })); + + let receiver_node_id = PublicKey::from_str(&get_pubkey()).unwrap(); + let handler = OfferHandler::new(); + assert!(handler + .create_reply_path(connector_mock, receiver_node_id) + .await + .is_ok()) + } + + #[tokio::test] + async fn test_create_reply_path_list_peers_error() { + let mut connector_mock = MockTestPeerConnector::new(); + + connector_mock + .expect_list_peers() + .returning(|| Err(Status::unknown("unknown error"))); + + let receiver_node_id = PublicKey::from_str(&get_pubkey()).unwrap(); + let handler = OfferHandler::new(); + assert!(handler + .create_reply_path(connector_mock, receiver_node_id) + .await + .is_err()) + } }