diff --git a/Cargo.lock b/Cargo.lock index 93bc3a65..9893b2d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -947,7 +947,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "ldk-sample" version = "0.1.0" -source = "git+https://github.com/lndk-org/ldk-sample?branch=offers#848e562f0ef57c95e015454cc2c00bca1e315e29" +source = "git+https://github.com/lndk-org/ldk-sample?branch=offer-handling#cdc1b006694eb323a8a982d523ae5b40f5f857ad" dependencies = [ "base64 0.13.1", "bech32 0.8.1", @@ -1095,6 +1095,7 @@ dependencies = [ "rand_core 0.6.4", "tempfile", "tokio", + "tokio-stream", "tonic 0.8.3", "tonic_lnd", ] diff --git a/Cargo.toml b/Cargo.toml index 101af1b6..5d286638 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ rand_core = "0.6.4" log = "0.4.17" log4rs = { version = "1.2.0", features = ["file_appender"] } tokio = { version = "1.25.0", features = ["rt", "rt-multi-thread"] } +tokio-stream = { version = "0.1.14" } tonic = "0.8.3" tonic_lnd = { git = "https://github.com/orbitalturtle/tonic_lnd", branch = "update-signer-client" } hex = "0.4.3" @@ -34,7 +35,7 @@ bytes = "1.4.0" bitcoincore-rpc = { package="core-rpc", version = "0.17.0" } bitcoind = { version = "0.30.0", features = [ "22_0" ] } chrono = { version = "0.4.26" } -ldk-sample = { git = "https://github.com/lndk-org/ldk-sample", branch = "offers" } +ldk-sample = { git = "https://github.com/lndk-org/ldk-sample", branch = "offer-handling" } mockall = "0.11.3" tempfile = "3.5.0" diff --git a/src/lndk_offers.rs b/src/lndk_offers.rs index ef48aa55..d5496890 100644 --- a/src/lndk_offers.rs +++ b/src/lndk_offers.rs @@ -10,7 +10,7 @@ use core::ops::Deref; use futures::executor::block_on; use lightning::blinded_path::BlindedPath; use lightning::ln::features::InitFeatures; -use lightning::ln::msgs::{Init, OnionMessageHandler}; +use lightning::ln::msgs::{Init, OnionMessage, OnionMessageHandler}; use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest}; use lightning::offers::merkle::SignError; use lightning::offers::offer::Offer; @@ -22,9 +22,12 @@ use lightning::onion_message::{ use lightning::sign::EntropySource; use lightning::sign::NodeSigner; use lightning::util::logger::Logger; +use lightning::util::ser::Readable; use std::error::Error; use std::fmt::Display; +use std::io::Cursor; use tokio::task; +use tokio_stream::StreamExt; use tonic_lnd::signrpc::{KeyLocator, SignMessageReq}; use tonic_lnd::tonic::Status; use tonic_lnd::Client; @@ -40,6 +43,8 @@ pub enum OfferError { DeriveKeyFailure(Status), /// Trouble sending an offers-related onion message along. SendError(SendError), + /// GetInvoiceError Indicates a failure to receive an invoice. + GetInvoiceError, } impl Display for OfferError { @@ -49,6 +54,7 @@ impl Display for OfferError { OfferError::SignError(e) => write!(f, "Error signing invoice request: {e:?}"), OfferError::DeriveKeyFailure(e) => write!(f, "Error signing invoice request: {e:?}"), OfferError::SendError(e) => write!(f, "Error sending onion message: {e:?}"), + OfferError::GetInvoiceError => write!(f, "Error retrieving invoice"), } } } @@ -198,6 +204,32 @@ pub async fn send_invoice_request( &mut custom_msg_client, ) .await; + + Ok(()) +} + +// After we've requested an invoice from the offer maker, wait for them to send us back an invoice. +pub async fn wait_for_invoice( + ln_client: &mut tonic_lnd::LightningClient, + onion_messenger: &OMH, +) -> Result<(), OfferError> { + let message_stream = ln_client + .subscribe_custom_messages(tonic_lnd::lnrpc::SubscribeCustomMessagesRequest {}) + .await + .expect("message subscription failed") + .into_inner(); + + let mut message_stream = message_stream.take(1); + if let Some(msg) = message_stream.next().await { + let message = msg.unwrap(); + if let Ok(onion_msg) = ::read(&mut Cursor::new(message.data)) { + let pubkey = PublicKey::from_slice(&message.peer).unwrap(); + onion_messenger.handle_onion_message(&pubkey, &onion_msg); + return Ok(()); + } else { + return Err(OfferError::GetInvoiceError); + } + } Ok(()) } diff --git a/tests/common/test_utils.rs b/tests/common/test_utils.rs index 0412b853..b3bf9e8f 100644 --- a/tests/common/test_utils.rs +++ b/tests/common/test_utils.rs @@ -19,11 +19,11 @@ where while retry_num < 3 { sleep(Duration::from_secs(3)).await; match f().await { - Err(_) => { + Err(e) => { println!("retrying {} call", func_name.clone()); retry_num += 1; if retry_num == 5 { - panic!("{} call failed after 3 retries", func_name); + panic!("{} call failed after 3 retries: {:?}", func_name, e); } continue; } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 56ff1e5e..e1af423d 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -3,21 +3,24 @@ use lndk; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::Network; +use bitcoincore_rpc::bitcoin::Network as RpcNetwork; +use bitcoincore_rpc::RpcApi; use ldk_sample::node_api::Node as LdkNode; use lightning::blinded_path::BlindedPath; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::offers::offer::Quantity; use lightning::onion_message::{DefaultMessageRouter, OnionMessenger}; use lndk::lnd::LndNodeSigner; -use lndk::lndk_offers::request_invoice; +use lndk::lndk_offers::{request_invoice, wait_for_invoice}; use lndk::onion_messenger::{CustomMessenger, InvoiceHandler, MessengerUtilities}; use std::collections::VecDeque; +use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; -use tokio::select; use tokio::time::{sleep, timeout, Duration as TokioDuration}; +use tokio::{join, select}; async fn wait_to_receive_onion_message( ldk1: LdkNode, @@ -185,3 +188,147 @@ async fn test_lndk_send_invoice_request() { } } } + +#[tokio::test(flavor = "multi_thread")] +// Here we test that when the offer creator responds with an invoice, lndk is able to receive it. +async fn test_lndk_wait_for_invoice() { + let test_name = "lndk_wait_for_invoice"; + let (bitcoind, mut lnd, ldk1, ldk2, _lndk_dir) = + common::setup_test_infrastructure(test_name).await; + + // Here we'll produce a little network of channels: + // + // ldk1 <- ldk2 <- lnd + // + // ldk1 will be the offer creator, which will build a blinded route from ldk2 to ldk1. + let (pubkey, addr) = ldk1.get_node_info(); + let (pubkey_2, addr_2) = ldk2.get_node_info(); + + ldk1.connect_to_peer(pubkey_2, addr_2).await.unwrap(); + lnd.connect_to_peer(pubkey_2, addr_2).await; + + let lnd_info = lnd.get_info().await; + let lnd_pubkey = PublicKey::from_str(&lnd_info.identity_pubkey).unwrap(); + + // ldk1 won't send back the invoice unless we have channels in our network. + let ldk2_fund_addr = ldk2.bitcoind_client.get_new_address().await; + let lnd_fund_addr = lnd.new_address().await.address; + + // We need to convert funding addresses to the form that the bitcoincore_rpc library recognizes. + let ldk2_addr_string = ldk2_fund_addr.to_string(); + let ldk2_addr = bitcoincore_rpc::bitcoin::Address::from_str(&ldk2_addr_string) + .unwrap() + .require_network(RpcNetwork::Regtest) + .unwrap(); + let lnd_addr = bitcoincore_rpc::bitcoin::Address::from_str(&lnd_fund_addr) + .unwrap() + .require_network(RpcNetwork::Regtest) + .unwrap(); + let lnd_network_addr = lnd + .address + .replace("localhost", "127.0.0.1") + .replace("https://", ""); + + // Fund both of these nodes so we can open channels. + bitcoind + .node + .client + .generate_to_address(1, &ldk2_addr) + .unwrap(); + bitcoind + .node + .client + .generate_to_address(1, &lnd_addr) + .unwrap(); + + // Make sure the above funds are confirmed. + bitcoind + .node + .client + .generate_to_address(100, &ldk2_addr) + .unwrap(); + + ldk2.open_channel(pubkey, addr, 200000, 0, true) + .await + .unwrap(); + ldk2.open_channel( + lnd_pubkey, + SocketAddr::from_str(&lnd_network_addr).unwrap(), + 200000, + 100000, + false, + ) + .await + .unwrap(); + + bitcoind + .node + .client + .generate_to_address(100, &ldk2_addr) + .unwrap(); + + // TODO: Restarting the lnd node here isn't ideal. But for some reason after we call LDK's open_channel + // API call, LND blocks and is unable to receive any API requests. This might be a bug in ldk-sample or + // our ldk-sample implementation. + lnd.handle.kill().expect("lnd couldn't be killed"); + lnd.start().await; + 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"); + + let mut signer_clone = lnd.client.clone().unwrap(); + let signer_client = signer_clone.signer(); + let node_signer = LndNodeSigner::new(lnd_pubkey.clone(), signer_client); + let messenger_utils = MessengerUtilities::new(); + let mut invoice_handler = InvoiceHandler { + invoices: Arc::new(Mutex::new(VecDeque::new())), + }; + let onion_messenger = OnionMessenger::new( + &messenger_utils, + &node_signer, + &messenger_utils, + &DefaultMessageRouter {}, + &mut invoice_handler, + IgnoringMessageHandler {}, + ); + + let client = lnd.client.clone().unwrap(); + let mut client_clone = lnd.client.clone().unwrap(); + let custom_msg_client = CustomMessenger { + client: client_clone.lightning().to_owned(), + }; + let blinded_path = offer.paths()[0].clone(); + let secp_ctx = Secp256k1::new(); + let reply_path = + BlindedPath::new_for_message(&[pubkey_2, lnd_pubkey], &messenger_utils, &secp_ctx).unwrap(); + + let (res1, res2) = join!( + wait_for_invoice(client_clone.lightning(), &onion_messenger), + request_invoice( + client.clone(), + &onion_messenger, + custom_msg_client, + offer, + pubkey_2, + reply_path, + blinded_path + ) + ); + res1.unwrap(); + res2.unwrap(); + + // LND should have received the Bolt12Invoice. + let invoices = invoice_handler.invoices.lock().unwrap(); + assert!(invoices.len() == 1); +}