diff --git a/src/lib.rs b/src/lib.rs index bd68ea4c..336e56fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,12 +8,17 @@ mod rate_limit; use crate::lnd::{ features_support_onion_messages, get_lnd_client, string_to_network, LndCfg, LndNodeSigner, }; +use crate::lndk_offers::{connect_to_peer, validate_amount, OfferError}; use crate::onion_messenger::MessengerUtilities; -use bitcoin::secp256k1::PublicKey; +use bitcoin::network::constants::Network; +use bitcoin::secp256k1::{Error as Secp256k1Error, PublicKey}; use home::home_dir; +use lightning::blinded_path::BlindedPath; use lightning::ln::peer_handler::IgnoringMessageHandler; +use lightning::offers::offer::Offer; use lightning::onion_message::{ - DefaultMessageRouter, OffersMessage, OffersMessageHandler, OnionMessenger, PendingOnionMessage, + DefaultMessageRouter, Destination, OffersMessage, OffersMessageHandler, OnionMessenger, + PendingOnionMessage, }; use log::{error, info, LevelFilter}; use log4rs::append::console::ConsoleAppender; @@ -26,6 +31,7 @@ use std::str::FromStr; use std::sync::{Mutex, Once}; use tokio::sync::mpsc::{Receiver, Sender}; use tonic_lnd::lnrpc::GetInfoRequest; +use tonic_lnd::Client; use triggered::{Listener, Trigger}; static INIT: Once = Once::new(); @@ -107,6 +113,7 @@ impl LndkOnionMessenger { network_str = Some(chain.network.clone()) } } + if network_str.is_none() { error!("lnd node is not connected to bitcoin network as expected"); return Err(()); @@ -175,19 +182,73 @@ enum OfferState { } pub struct OfferHandler { - _active_offers: Mutex>, + active_offers: Mutex>, pending_messages: Mutex>>, // This channel will close once the onion messenger has successfully started up. - _started: RefCell>, + started: RefCell>, } impl OfferHandler { pub fn new(started: Receiver) -> Self { OfferHandler { - _active_offers: Mutex::new(HashMap::new()), + active_offers: Mutex::new(HashMap::new()), pending_messages: Mutex::new(Vec::new()), - _started: RefCell::new(started), + started: RefCell::new(started), + } + } + + /// Adds an offer to be paid with the amount specified. May only be called once for a single offer. + #[allow(clippy::await_holding_refcell_ref)] + pub async fn pay_offer( + &self, + offer: Offer, + amount: Option, + network: Network, + client: Client, + blinded_path: BlindedPath, + reply_path: Option, + ) -> Result<(), OfferError> { + // Wait for onion messenger to give us the signal that it's ready. Once the onion messenger drops + // the channel sender, recv will return None and we'll stop blocking here. + while (self.started.borrow_mut().recv().await).is_some() { + println!("Error: we shouldn't receive any messages on this channel"); + } + + let validated_amount = validate_amount(&offer, amount).await?; + + // For now we connect directly to the introduction node of the blinded path so we don't need any + // intermediate nodes here. In the future we'll query for a full path to the introduction node for + // better sender privacy. + connect_to_peer(client.clone(), blinded_path.introduction_node_id).await?; + + let offer_id = offer.clone().to_string(); + { + let mut active_offers = self.active_offers.lock().unwrap(); + if active_offers.contains_key(&offer_id.clone()) { + return Err(OfferError::AlreadyProcessing); + } + active_offers.insert(offer.to_string().clone(), OfferState::OfferAdded); } + + let invoice_request = self + .create_invoice_request(client.clone(), offer, vec![], network, validated_amount) + .await?; + + let contents = OffersMessage::InvoiceRequest(invoice_request); + let pending_message = PendingOnionMessage { + contents, + destination: Destination::BlindedPath(blinded_path.clone()), + reply_path, + }; + + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages.push(pending_message); + std::mem::drop(pending_messages); + + let mut active_offers = self.active_offers.lock().unwrap(); + active_offers.insert(offer_id, OfferState::InvoiceRequestSent); + + Ok(()) } } diff --git a/src/lnd.rs b/src/lnd.rs index 58a96d7d..1eb6161b 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -29,6 +29,7 @@ pub(crate) fn get_lnd_client(cfg: LndCfg) -> Result { } /// LndCfg specifies the configuration required to connect to LND's grpc client. +#[derive(Clone)] pub struct LndCfg { address: String, cert: PathBuf, @@ -186,7 +187,7 @@ pub(crate) fn string_to_network(network_str: &str) -> Result Result, Status>; async fn sign_message( &mut self, diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs index 1baadd03..5023fc2e 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -67,9 +67,8 @@ pub fn decode(offer_str: String) -> Result { } impl OfferHandler { - #[allow(dead_code)] // create_invoice_request builds and signs an invoice request, the first step in the BOLT 12 process of paying an offer. - pub(crate) async fn create_invoice_request( + pub async fn create_invoice_request( &self, mut signer: impl MessageSigner + std::marker::Send + 'static, offer: Offer, @@ -290,6 +289,8 @@ mod tests { use mockall::mock; use std::str::FromStr; use std::time::{Duration, SystemTime}; + use tokio::sync::mpsc; + use tokio::sync::mpsc::{Receiver, Sender}; use tonic_lnd::lnrpc::NodeAddress; fn get_offer() -> String { @@ -359,7 +360,8 @@ mod tests { }); let offer = decode(get_offer()).unwrap(); - let handler = OfferHandler::new(); + let (_, rx): (Sender, Receiver) = mpsc::channel(1); + let handler = OfferHandler::new(rx); assert!(handler .create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000) .await @@ -382,7 +384,8 @@ mod tests { }); let offer = decode(get_offer()).unwrap(); - let handler = OfferHandler::new(); + let (_, rx): (Sender, Receiver) = mpsc::channel(1); + let handler = OfferHandler::new(rx); assert!(handler .create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000) .await @@ -405,7 +408,8 @@ mod tests { .returning(|_, _, _| Err(Status::unknown("error testing"))); let offer = decode(get_offer()).unwrap(); - let handler = OfferHandler::new(); + let (_, rx): (Sender, Receiver) = mpsc::channel(1); + let handler = OfferHandler::new(rx); assert!(handler .create_invoice_request(signer_mock, offer, vec![], Network::Regtest, 10000) .await diff --git a/tests/common/mod.rs b/tests/common/mod.rs index bafdd82e..ba6b5437 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -151,7 +151,7 @@ pub struct LndNode { pub cert_path: String, pub macaroon_path: String, _handle: Child, - client: Option, + pub client: Option, } impl LndNode { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 02aa7b5b..c81e0482 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,12 +1,17 @@ mod common; use lndk; -use bitcoin::secp256k1::PublicKey; +use bitcoin::secp256k1::{PublicKey, Secp256k1}; +use bitcoin::Network; use chrono::Utc; use ldk_sample::node_api::Node as LdkNode; +use lightning::blinded_path::BlindedPath; +use lightning::offers::offer::Quantity; +use lndk::onion_messenger::MessengerUtilities; use lndk::Signals; use std::path::PathBuf; use std::str::FromStr; +use std::time::SystemTime; use tokio::select; use tokio::sync::mpsc; use tokio::sync::mpsc::{Receiver, Sender}; @@ -36,7 +41,6 @@ async fn wait_to_receive_onion_message( async fn check_for_message(ldk: LdkNode) -> LdkNode { loop { if ldk.onion_message_handler.messages.lock().unwrap().len() == 1 { - println!("MESSAGE: {:?}", ldk.onion_message_handler.messages); return ldk; } sleep(Duration::from_secs(2)).await; @@ -102,3 +106,87 @@ async fn test_lndk_forwards_onion_message() { } } } + +#[tokio::test(flavor = "multi_thread")] +// Here we test the beginning of the BOLT 12 offers flow. We show that lndk successfully builds an +// invoice_request and sends it. +async fn test_lndk_send_invoice_request() { + let test_name = "lndk_send_invoice_request"; + let (_bitcoind, mut lnd, ldk1, ldk2, lndk_dir) = + common::setup_test_infrastructure(test_name).await; + + // Here we'll produce a little network path: + // + // ldk1 <-> ldk2 <-> lnd + // + // ldk1 will be the offer creator, which will build a blinded route from ldk2 to ldk1. + let (pubkey, _) = ldk1.get_node_info(); + let (pubkey_2, addr_2) = ldk2.get_node_info(); + let lnd_info = lnd.get_info().await; + let lnd_pubkey = PublicKey::from_str(&lnd_info.identity_pubkey).unwrap(); + + ldk1.connect_to_peer(pubkey_2, addr_2).await.unwrap(); + lnd.connect_to_peer(pubkey_2, addr_2).await; + + let path_pubkeys = vec![pubkey_2, pubkey]; + let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); + let offer = ldk1 + .create_offer( + &path_pubkeys, + Network::Regtest, + 20_000, + Quantity::One, + expiration, + ) + .await + .expect("should create offer"); + + // Now we'll spin up lndk, which should forward the invoice request to ldk2. + let (shutdown, listener) = triggered::trigger(); + let lnd_cfg = lndk::lnd::LndCfg::new( + lnd.address.clone(), + PathBuf::from_str(&lnd.cert_path).unwrap(), + PathBuf::from_str(&lnd.macaroon_path).unwrap(), + ); + let (tx, rx): (Sender, Receiver) = mpsc::channel(1); + let signals = Signals { + shutdown: shutdown.clone(), + listener, + started: tx, + }; + + let lndk_cfg = lndk::Cfg { + lnd: lnd_cfg, + log_dir: Some( + lndk_dir + .join(format!("lndk-logs.txt")) + .to_str() + .unwrap() + .to_string(), + ), + signals, + }; + + let client = lnd.client.clone().unwrap(); + let blinded_path = offer.paths()[0].clone(); + let messenger_utils = MessengerUtilities::new(); + let secp_ctx = Secp256k1::new(); + let reply_path = + BlindedPath::new_for_message(&[pubkey_2, lnd_pubkey], &messenger_utils, &secp_ctx).unwrap(); + + // Make sure lndk successfully sends the invoice_request. + let handler = lndk::OfferHandler::new(rx); + let messenger = lndk::LndkOnionMessenger::new(handler); + select! { + val = messenger.run(lndk_cfg) => { + panic!("lndk should not have completed first {:?}", val); + }, + // We wait for ldk2 to receive the onion message. + res = messenger.offer_handler.pay_offer(offer.clone(), Some(20_000), Network::Regtest, client.clone(), blinded_path.clone(), Some(reply_path)) => { + assert!(res.is_ok()); + shutdown.trigger(); + ldk1.stop().await; + ldk2.stop().await; + } + } +}