From d64e53414d76be0bd7237b6daf0d1e3ce1185d69 Mon Sep 17 00:00:00 2001 From: Orbital Date: Mon, 1 Jan 2024 18:40:40 -0600 Subject: [PATCH] offers: send invoice request --- src/lib.rs | 84 +++++++++++++++++++++++++++++++--- src/lnd.rs | 3 +- src/lndk_offers.rs | 86 ++++++++++++++++++++++++++++++++++- tests/common/mod.rs | 2 +- tests/integration_tests.rs | 92 +++++++++++++++++++++++++++++++++++++- 5 files changed, 255 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 535f3a56..91d1309d 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,21 +182,86 @@ enum OfferState { } pub struct OfferHandler { - _active_offers: Mutex>, + active_offers: Mutex>, pending_messages: Mutex>>, messenger_utils: MessengerUtilities, // 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()), messenger_utils: MessengerUtilities::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, + mut client: Client, + blinded_path: BlindedPath, + mut 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?; + + if reply_path.is_none() { + let info = client + .lightning() + .get_info(GetInfoRequest {}) + .await + .expect("failed to get info") + .into_inner(); + + let pubkey = PublicKey::from_str(&info.identity_pubkey).unwrap(); + reply_path = Some(self.create_reply_path(client.clone(), pubkey).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 d43ede6a..7d4a2a7e 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -76,9 +76,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, @@ -348,6 +347,10 @@ mod tests { use tokio::sync::mpsc::{Receiver, Sender}; use tonic_lnd::lnrpc::NodeAddress; + fn get_offer() -> String { + "lno1qgsqvgnwgcg35z6ee2h3yczraddm72xrfua9uve2rlrm9deu7xyfzrcgqgn3qzsyvfkx26qkyypvr5hfx60h9w9k934lt8s2n6zc0wwtgqlulw7dythr83dqx8tzumg".to_string() + } + fn build_custom_offer(amount_msats: u64) -> Offer { let secp_ctx = Secp256k1::new(); let keys = KeyPair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); @@ -367,6 +370,10 @@ mod tests { "0313ba7ccbd754c117962b9afab6c2870eb3ef43f364a9f6c43d0fabb4553776ba".to_string() } + fn get_signature() -> String { + "28b937976a29c15827433086440b36c2bec6ca5bd977557972dca8641cd59ffba50daafb8ee99a19c950976b46f47d9e7aa716652e5657dfc555b82eff467f18".to_string() + } + mock! { TestBolt12Signer{} @@ -388,6 +395,81 @@ mod tests { } } + #[tokio::test] + async fn test_request_invoice() { + let mut signer_mock = MockTestBolt12Signer::new(); + + signer_mock.expect_derive_key().returning(|_| { + Ok(PublicKey::from_str(&get_pubkey()) + .unwrap() + .serialize() + .to_vec()) + }); + + signer_mock.expect_sign_message().returning(|_, _, _| { + Ok(Signature::from_str(&get_signature()) + .unwrap() + .as_ref() + .to_vec()) + }); + + let offer = decode(get_offer()).unwrap(); + 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 + .is_ok()) + } + + #[tokio::test] + async fn test_request_invoice_derive_key_error() { + let mut signer_mock = MockTestBolt12Signer::new(); + + signer_mock + .expect_derive_key() + .returning(|_| Err(Status::unknown("error testing"))); + + signer_mock.expect_sign_message().returning(|_, _, _| { + Ok(Signature::from_str(&get_signature()) + .unwrap() + .as_ref() + .to_vec()) + }); + + let offer = decode(get_offer()).unwrap(); + 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 + .is_err()) + } + + #[tokio::test] + async fn test_request_invoice_signer_error() { + let mut signer_mock = MockTestBolt12Signer::new(); + + signer_mock.expect_derive_key().returning(|_| { + Ok(PublicKey::from_str(&get_pubkey()) + .unwrap() + .serialize() + .to_vec()) + }); + + signer_mock + .expect_sign_message() + .returning(|_, _, _| Err(Status::unknown("error testing"))); + + let offer = decode(get_offer()).unwrap(); + 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 + .is_err()) + } + #[tokio::test] async fn test_validate_amount() { // If the amount the user provided is greater than the offer-provided amount, then 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; + } + } +}