diff --git a/Makefile b/Makefile index aa669da..04c3ffa 100644 --- a/Makefile +++ b/Makefile @@ -2,17 +2,24 @@ GO_BUILD := go build CARGO_TEST := cargo test +CARGO_BUILD := cargo build LND_PKG := github.com/lightningnetwork/lnd TMP_DIR := "/tmp" +BIN_DIR := $(TMP_DIR)/lndk-tests/bin + UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) TMP_DIR=${TMPDIR} endif itest: - @$(call print, "Building lnd for itests.") + @echo Building lnd for itests. git submodule update --init --recursive - cd lnd/cmd/lnd; $(GO_BUILD) -tags="peersrpc signrpc walletrpc dev" -o $(TMP_DIR)/lndk-tests/bin/lnd-itest$(EXEC_SUFFIX) - $(CARGO_TEST) --test '*' -- --test-threads=1 --nocapture + cd lnd/cmd/lnd; $(GO_BUILD) -tags="peersrpc signrpc walletrpc dev" -o $(BIN_DIR)/lnd-itest$(EXEC_SUFFIX) + @echo Building lndk-cli for itests. + # This outputs the lndk-cli binary into $(BINDIR)/debug/ + $(CARGO_BUILD) --bin=lndk-cli --target-dir=$(BIN_DIR) + + $(CARGO_TEST) --test '*' -- --test-threads=1 --nocapture diff --git a/src/cli.rs b/src/cli.rs index 3913f13..8ba2605 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,9 +3,9 @@ use lightning::offers::invoice::Bolt12Invoice; use lndk::lndk_offers::decode; use lndk::lndkrpc::offers_client::OffersClient; use lndk::lndkrpc::{GetInvoiceRequest, PayInvoiceRequest, PayOfferRequest}; +use lndk::server::{DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT}; use lndk::{ - Bolt12InvoiceString, DEFAULT_DATA_DIR, DEFAULT_RESPONSE_INVOICE_TIMEOUT, DEFAULT_SERVER_HOST, - DEFAULT_SERVER_PORT, TLS_CERT_FILENAME, + Bolt12InvoiceString, DEFAULT_DATA_DIR, DEFAULT_RESPONSE_INVOICE_TIMEOUT, TLS_CERT_FILENAME, }; use std::fs::File; use std::io::BufReader; diff --git a/src/lib.rs b/src/lib.rs index 3885992..e3965b6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,6 @@ pub fn init_logger(config: LogConfig) { }); } -pub const DEFAULT_SERVER_HOST: &str = "127.0.0.1"; -pub const DEFAULT_SERVER_PORT: u16 = 7000; pub const LDK_LOGGER_NAME: &str = "ldk"; pub const DEFAULT_DATA_DIR: &str = ".lndk"; @@ -213,8 +211,8 @@ impl LndkOnionMessenger { } // Create an onion messenger that depends on LND's signer client and consume related events. - let mut node_client = client.signer().clone(); - let node_signer = LndNodeSigner::new(pubkey, &mut node_client); + let node_client = client.clone().signer_read_only(); + let node_signer = LndNodeSigner::new(pubkey, node_client); let messenger_utils = MessengerUtilities::new(); let network_graph = &NetworkGraph::new(network, &messenger_utils); let message_router = &DefaultMessageRouter::new(network_graph, &messenger_utils); @@ -229,10 +227,10 @@ impl LndkOnionMessenger { IgnoringMessageHandler {}, ); - let mut peers_client = client.lightning().clone(); + let peers_client = client.lightning().clone(); self.run_onion_messenger( peer_support, - &mut peers_client, + &peers_client, onion_messenger, network, args.signals, diff --git a/src/lnd.rs b/src/lnd.rs index 4770ff1..22315ca 100644 --- a/src/lnd.rs +++ b/src/lnd.rs @@ -13,7 +13,6 @@ use lightning::offers::invoice::UnsignedBolt12Invoice; use lightning::offers::invoice_request::{InvoiceRequest, UnsignedInvoiceRequest}; use lightning::sign::{KeyMaterial, NodeSigner, Recipient}; use log::error; -use std::cell::RefCell; use std::collections::HashMap; use std::error::Error; use std::fmt::Display; @@ -48,7 +47,7 @@ pub fn get_lnd_client(cfg: LndCfg) -> Result { } /// LndCfg specifies the configuration required to connect to LND's grpc client. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LndCfg { pub address: String, pub creds: Creds, @@ -233,23 +232,23 @@ pub fn has_build_tags(version: &Version, requirement: Option { +pub(crate) struct LndNodeSigner { pubkey: PublicKey, secp_ctx: Secp256k1, - signer: RefCell<&'a mut tonic_lnd::SignerClient>, + signer: tonic_lnd::SignerClient, } -impl<'a> LndNodeSigner<'a> { - pub(crate) fn new(pubkey: PublicKey, signer: &'a mut tonic_lnd::SignerClient) -> Self { +impl LndNodeSigner { + pub(crate) fn new(pubkey: PublicKey, signer: tonic_lnd::SignerClient) -> Self { LndNodeSigner { pubkey, secp_ctx: Secp256k1::new(), - signer: RefCell::new(signer), + signer, } } } -impl<'a> NodeSigner for LndNodeSigner<'a> { +impl NodeSigner for LndNodeSigner { /// Get node id based on the provided [`Recipient`]. /// /// This method must return the same value each time it is called with a given [`Recipient`] @@ -287,7 +286,7 @@ impl<'a> NodeSigner for LndNodeSigner<'a> { *other_key }; - let shared_secret = match block_on(self.signer.borrow_mut().derive_shared_key( + let shared_secret = match block_on(self.signer.clone().derive_shared_key( tonic_lnd::signrpc::SharedKeyRequest { ephemeral_pubkey: tweaked_key.serialize().into_iter().collect::>(), key_desc: None, diff --git a/src/main.rs b/src/main.rs index dfb48cb..81e5890 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,13 +11,11 @@ mod internal { use home::home_dir; use internal::*; -use lndk::lnd::{get_lnd_client, validate_lnd_creds, LndCfg}; -use lndk::server::{generate_tls_creds, read_tls, LNDKServer}; +use lndk::lnd::{validate_lnd_creds, LndCfg}; +use lndk::server::setup_server; use lndk::{ - lndkrpc, setup_logger, Cfg, LifecycleSignals, LndkOnionMessenger, OfferHandler, - DEFAULT_DATA_DIR, DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT, + setup_logger, Cfg, LifecycleSignals, LndkOnionMessenger, OfferHandler, DEFAULT_DATA_DIR, }; -use lndkrpc::offers_server::OffersServer; use log::{error, info}; use std::fs::create_dir_all; use std::path::PathBuf; @@ -25,8 +23,6 @@ use std::process::exit; use std::sync::Arc; use tokio::select; use tokio::signal::unix::SignalKind; -use tonic::transport::{Server, ServerTlsConfig}; -use tonic_lnd::lnrpc::GetInfoRequest; #[macro_use] extern crate configure_me; @@ -93,51 +89,17 @@ async fn main() -> Result<(), ()> { let handler = Arc::new(OfferHandler::new(config.response_invoice_timeout)); let messenger = LndkOnionMessenger::new(); - let mut client = get_lnd_client(args.lnd.clone()).expect("failed to connect to lnd"); - let info = client - .lightning() - .get_info(GetInfoRequest {}) - .await - .expect("failed to get info") - .into_inner(); - - let grpc_host = match config.grpc_host { - Some(host) => host, - None => DEFAULT_SERVER_HOST.to_string(), - }; - let grpc_port = match config.grpc_port { - Some(port) => port, - None => DEFAULT_SERVER_PORT, - }; - let addr = format!("{grpc_host}:{grpc_port}").parse().map_err(|e| { - error!("Error parsing API address: {e}"); - })?; - let lnd_tls_str = creds.get_certificate_string()?; - - // The user passed in a TLS cert to help us establish a secure connection to LND. But now we - // need to generate a TLS credentials for connecting securely to the LNDK server. - generate_tls_creds(data_dir.clone(), config.tls_ip).map_err(|e| { - error!("Error generating tls credentials: {e}"); - })?; - let identity = read_tls(data_dir).map_err(|e| { - error!("Error reading tls credentials: {e}"); - })?; - - let server = LNDKServer::new( + let server_fut = setup_server( + args.lnd.clone(), + config.grpc_host, + config.grpc_port, + data_dir, + config.tls_ip, Arc::clone(&handler), - &info.identity_pubkey, - lnd_tls_str, address, ) - .await; - - let server_fut = Server::builder() - .tls_config(ServerTlsConfig::new().identity(identity)) - .expect("couldn't configure tls") - .add_service(OffersServer::new(server)) - .serve_with_shutdown(addr, listener); - - info!("Starting lndk's grpc server at address {grpc_host}:{grpc_port}"); + .await + .map_err(|e| error!("Error setting up server: {:?}", e))?; select! { _ = messenger.run(args, Arc::clone(&handler)) => { diff --git a/src/onion_messenger.rs b/src/onion_messenger.rs index fa02151..3ff6367 100644 --- a/src/onion_messenger.rs +++ b/src/onion_messenger.rs @@ -163,7 +163,7 @@ impl LndkOnionMessenger { >( &self, current_peers: HashMap, - ln_client: &mut tonic_lnd::LightningClient, + ln_client: &tonic_lnd::LightningClient, onion_messenger: OnionMessenger, network: Network, signals: LifecycleSignals, @@ -266,10 +266,8 @@ impl LndkOnionMessenger { } }); - // Consume events is our main controlling loop, so we run it inline here. We use a RefCell - // in onion_messenger to allow interior mutability (see LndNodeSigner) so this - // function can't safely be passed off to another thread. This function is expected - // to finish if any producing thread exits (because we're no longer receiving the + // Consume events is our main controlling loop, so we run it inline here. This function is + // expected to finish if any producing thread exits (because we're no longer receiving the // events we need). let rate_limiter = &mut TokenLimiter::new( current_peers.keys().copied(), diff --git a/src/server.rs b/src/server.rs index 98f511b..9e99cb8 100644 --- a/src/server.rs +++ b/src/server.rs @@ -12,31 +12,37 @@ use lightning::offers::invoice::{BlindedPayInfo, Bolt12Invoice}; use lightning::offers::offer::Offer; use lightning::sign::EntropySource; use lightning::util::ser::Writeable; -use lndkrpc::offers_server::Offers; +use lndkrpc::offers_server::{Offers, OffersServer}; use lndkrpc::{ Bolt12InvoiceContents, DecodeInvoiceRequest, FeatureBit, GetInvoiceRequest, GetInvoiceResponse, PayInvoiceRequest, PayInvoiceResponse, PayOfferRequest, PayOfferResponse, PaymentHash, PaymentPaths, }; +use log::{error, info}; use rcgen::{generate_simple_self_signed, CertifiedKey, Error as RcgenError}; use std::error::Error; use std::fmt::Display; use std::fs::{metadata, set_permissions, File}; +use std::future::Future; use std::io::Write; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use tonic::metadata::MetadataMap; -use tonic::transport::Identity; +use tonic::transport::{Identity, Server, ServerTlsConfig}; use tonic::{Request, Response, Status}; use tonic_lnd::lnrpc::GetInfoRequest; + +pub const DEFAULT_SERVER_HOST: &str = "127.0.0.1"; +pub const DEFAULT_SERVER_PORT: u16 = 7000; + pub struct LNDKServer { offer_handler: Arc, node_id: PublicKey, // The LND tls cert we need to establish a connection with LND. lnd_cert: String, - address: String, + lnd_address: String, } impl LNDKServer { @@ -44,13 +50,13 @@ impl LNDKServer { offer_handler: Arc, node_id: &str, lnd_cert: String, - address: String, + lnd_address: String, ) -> Self { Self { offer_handler, node_id: PublicKey::from_str(node_id).unwrap(), lnd_cert, - address, + lnd_address, } } } @@ -69,7 +75,7 @@ impl Offers for LNDKServer { cert: self.lnd_cert.clone(), macaroon, }; - let lnd_cfg = LndCfg::new(self.address.clone(), creds); + let lnd_cfg = LndCfg::new(self.lnd_address.clone(), creds); let mut client = get_lnd_client(lnd_cfg) .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; @@ -165,7 +171,7 @@ impl Offers for LNDKServer { cert: self.lnd_cert.clone(), macaroon, }; - let lnd_cfg = LndCfg::new(self.address.clone(), creds); + let lnd_cfg = LndCfg::new(self.lnd_address.clone(), creds); let mut client = get_lnd_client(lnd_cfg) .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; @@ -252,7 +258,7 @@ impl Offers for LNDKServer { cert: self.lnd_cert.clone(), macaroon, }; - let lnd_cfg = LndCfg::new(self.address.clone(), creds); + let lnd_cfg = LndCfg::new(self.lnd_address.clone(), creds); let client = get_lnd_client(lnd_cfg) .map_err(|e| Status::unavailable(format!("Couldn't connect to lnd: {e}")))?; @@ -310,6 +316,64 @@ fn check_auth_metadata(metadata: &MetadataMap) -> Result { Ok(macaroon) } +pub async fn setup_server( + lnd_cfg: LndCfg, + grpc_host: Option, + grpc_port: Option, + data_dir: PathBuf, + tls_ip: Option, + handler: Arc, + lnd_address: String, +) -> Result>, ()> { + let mut client = get_lnd_client(lnd_cfg.clone()).expect("failed to connect to lnd"); + let info = client + .lightning() + .get_info(GetInfoRequest {}) + .await + .expect("failed to get info") + .into_inner(); + + let grpc_host = match grpc_host { + Some(host) => host, + None => DEFAULT_SERVER_HOST.to_string(), + }; + let grpc_port = match grpc_port { + Some(port) => port, + None => DEFAULT_SERVER_PORT, + }; + let addr = format!("{grpc_host}:{grpc_port}").parse().map_err(|e| { + error!("Error parsing API address: {e}"); + })?; + let lnd_tls_str = lnd_cfg.creds.get_certificate_string()?; + + // The user passed in a TLS cert to help us establish a secure connection to LND. But now we + // need to generate a TLS credentials for connecting securely to the LNDK server. + generate_tls_creds(data_dir.clone(), tls_ip).map_err(|e| { + error!("Error generating tls credentials: {e}"); + })?; + let identity = read_tls(data_dir).map_err(|e| { + error!("Error reading tls credentials: {e}"); + })?; + + let server = LNDKServer::new( + Arc::clone(&handler), + &info.identity_pubkey, + lnd_tls_str, + lnd_address, + ) + .await; + + let server_fut = Server::builder() + .tls_config(ServerTlsConfig::new().identity(identity)) + .expect("couldn't configure tls") + .add_service(OffersServer::new(server)) + .serve(addr); + + info!("Starting lndk's grpc server at address {grpc_host}:{grpc_port}"); + + Ok(server_fut) +} + /// An error that occurs when generating TLS credentials. #[derive(Debug)] pub enum CertificateGenFailure { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1847990..c4cfeb3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -12,6 +12,7 @@ use lndk::lnd::validate_lnd_creds; use lndk::{setup_logger, LifecycleSignals, LndkOnionMessenger, OfferHandler}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; +use std::process::ExitStatus; use std::process::{Child, Command, Stdio}; use std::str::FromStr; use std::sync::Arc; @@ -23,6 +24,8 @@ use tonic_lnd::lnrpc::{AddressType, GetInfoRequest}; use tonic_lnd::Client; const LNDK_TESTS_FOLDER: &str = "lndk-tests"; +const BIN_DIR: &str = "bin"; +const DEBUG_DIR: &str = "debug"; pub async fn setup_test_infrastructure( test_name: &str, @@ -295,7 +298,7 @@ impl LndNode { zmq_tx_port: u16, lnd_data_dir: PathBuf, ) -> LndNode { - let lnd_exe_dir = env::temp_dir().join(LNDK_TESTS_FOLDER).join("bin"); + let lnd_exe_dir = env::temp_dir().join(LNDK_TESTS_FOLDER).join(BIN_DIR); env::set_current_dir(lnd_exe_dir).expect("couldn't set current directory"); let lnd_dir_binding = Builder::new() @@ -554,3 +557,40 @@ impl LndNode { resp } } + +pub(crate) struct LndkCli { + macaroon_path: String, + cert_path: String, + server_port: u16, +} + +impl LndkCli { + pub(crate) fn new(macaroon_path: String, cert_path: String, server_port: u16) -> Self { + Self { + macaroon_path, + cert_path, + server_port, + } + } + + pub(crate) async fn pay_offer(&self, offer: String, amount: u64) -> ExitStatus { + let lndk_exe_dir = env::temp_dir() + .join(LNDK_TESTS_FOLDER) + .join(BIN_DIR) + .join(DEBUG_DIR); + env::set_current_dir(lndk_exe_dir).expect("couldn't set current directory"); + + Command::new("./lndk-cli") + .arg(format!("--macaroon-path={}", self.macaroon_path)) + .arg(format!("--cert-path={}", self.cert_path)) + .arg(format!("--grpc-port={}", self.server_port)) + .arg("pay-offer") + .arg(offer) + .arg(amount.to_string()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .expect("failed to execute cargo run process") + .status + } +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d9bdd8c..7b7af70 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -6,12 +6,15 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::Network; use bitcoincore_rpc::bitcoin::Network as RpcNetwork; use bitcoincore_rpc::RpcApi; +use bitcoind::get_available_port; +use common::LndkCli; use ldk_sample::node_api::Node as LdkNode; use lightning::blinded_path::{BlindedPath, IntroductionNode}; use lightning::offers::offer::Quantity; use lightning::onion_message::messenger::Destination; use lndk::lnd::validate_lnd_creds; use lndk::onion_messenger::MessengerUtilities; +use lndk::server::setup_server; use lndk::{setup_logger, LifecycleSignals, OfferHandler, PayOfferParams}; use std::path::PathBuf; use std::str::FromStr; @@ -577,3 +580,96 @@ async fn test_reply_path_announced_peers() { ldk1.stop().await; ldk2.stop().await; } + +#[tokio::test(flavor = "multi_thread")] +// Tests the interaction between the CLI and the server and ensures that we can make a payment +// with the CLI. +async fn test_server_pay_offer() { + let test_name = "lndk_server_pay_offer"; + let (bitcoind, mut lnd, ldk1, ldk2, lndk_dir) = + common::setup_test_infrastructure(test_name).await; + + let (ldk1_pubkey, ldk2_pubkey, _) = + common::connect_network(&ldk1, &ldk2, true, &mut lnd, &bitcoind).await; + + let path_pubkeys = vec![ldk2_pubkey, ldk1_pubkey]; + let expiration = SystemTime::now() + Duration::from_secs(24 * 60 * 60); + let offer_amount = 20_000; + let offer = ldk1 + .create_offer( + &path_pubkeys, + Network::Regtest, + offer_amount, + Quantity::One, + expiration, + ) + .await + .expect("should create offer"); + + let log_dir = Some( + lndk_dir + .join(format!("lndk-logs.txt")) + .to_str() + .unwrap() + .to_string(), + ); + setup_logger(None, log_dir).unwrap(); + + let (lndk_cfg, handler, messenger, shutdown) = common::setup_lndk( + &lnd.cert_path, + &lnd.macaroon_path, + lnd.address.clone(), + lndk_dir.clone(), + ) + .await; + + let creds = validate_lnd_creds( + Some(PathBuf::from_str(&lnd.cert_path).unwrap()), + None, + Some(PathBuf::from_str(&lnd.macaroon_path).unwrap()), + None, + ) + .unwrap(); + let lnd_cfg = lndk::lnd::LndCfg::new(lnd.address.clone(), creds.clone()); + + let server_port = get_available_port().unwrap(); + let server_fut = setup_server( + lnd_cfg, + None, + Some(server_port), + lndk_dir.clone(), + None, + Arc::clone(&handler), + lnd.address, + ) + .await + .unwrap(); + + // Spin up the onion messenger and server in another thread before we try to pay the offer + // via the CLI. + tokio::spawn(async move { + select! { + val = messenger.run(lndk_cfg, Arc::clone(&handler)) => { + panic!("lndk should not have completed first {:?}", val); + }, + _ = server_fut => { + panic!("server should not have completed first"); + }, + } + }); + + // Let's try running the pay-offer CLI command. + let lndk_tls_cert_path = lndk_dir.join("tls-cert.pem"); + let cli = LndkCli::new( + lnd.macaroon_path, + lndk_tls_cert_path.to_str().unwrap().to_owned(), + server_port, + ); + + let resp = cli.pay_offer(offer.to_string(), offer_amount).await; + assert!(resp.success()); + + shutdown.trigger(); + ldk1.stop().await; + ldk2.stop().await; +}