mod error;
pub mod versions;
use anyhow::Context;
use bitcoind::bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoind::bitcoincore_rpc::RpcApi;
use bitcoind::tempfile::TempDir;
use bitcoind::{get_available_port, BitcoinD};
use log::{error, warn};
use std::env;
use std::ffi::OsStr;
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use tonic_lnd::Client;
pub use bitcoind;
pub use tonic_lnd;
pub use error::Error;
pub use which;
#[derive(Debug, PartialEq, Eq, Clone)]
#[non_exhaustive]
pub struct LndConf<'a> {
pub args: Vec<&'a str>,
pub view_stdout: bool,
pub view_stderr: bool,
pub network: &'a str,
pub tmpdir: Option<PathBuf>,
pub staticdir: Option<PathBuf>,
attempts: u8,
listen_port: u16,
pub minchansize: Option<u64>,
pub maxchansize: Option<u64>,
}
impl Default for LndConf<'_> {
fn default() -> Self {
let args = vec![];
LndConf {
args,
view_stderr: false,
view_stdout: false,
network: "regtest",
listen_port: 9735,
tmpdir: None,
staticdir: None,
attempts: 3,
minchansize: None,
maxchansize: None,
}
}
}
pub struct Lnd {
process: Child,
pub client: Client,
work_dir: DataDir,
pub grpc_url: String,
pub rest_url: String,
pub listen_url: Option<String>,
pub admin_macaroon: String,
pub tls_cert: String,
network: String,
}
pub enum DataDir {
Persistent(PathBuf),
Temporary(TempDir),
}
impl DataDir {
fn path(&self) -> PathBuf {
match self {
Self::Persistent(path) => path.to_owned(),
Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
}
}
}
impl Lnd {
pub async fn new<S: AsRef<OsStr>>(
exe: S,
bitcoind_cookie: String,
bitcoind_rpc_socket: String,
#[cfg(feature = "bitcoind")] bitcoind: &BitcoinD,
) -> anyhow::Result<Lnd> {
Lnd::with_conf(
exe,
&LndConf::default(),
bitcoind_cookie,
bitcoind_rpc_socket,
#[cfg(feature = "bitcoind")]
bitcoind,
)
.await
}
#[async_recursion::async_recursion(?Send)]
pub async fn with_conf<S: AsRef<OsStr>>(
exe: S,
conf: &LndConf<'_>,
bitcoind_cookie: String,
bitcoind_rpc_socket: String,
#[cfg(feature = "bitcoind")] bitcoind: &BitcoinD,
) -> anyhow::Result<Lnd> {
#[cfg(feature = "bitcoind")]
{
let response = bitcoind.client.call::<Value>("getblockchaininfo", &[])?;
if response
.get("initialblockdownload")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let node_address = bitcoind.client.call::<Value>("getnewaddress", &[])?;
bitcoind
.client
.call::<Value>("generatetoaddress", &[1.into(), node_address])
.unwrap();
}
}
let mut args = conf.args.clone();
let work_dir = match (&conf.tmpdir, &conf.staticdir) {
(Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()),
(Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?),
(None, Some(workdir)) => {
std::fs::create_dir_all(workdir)?;
DataDir::Persistent(workdir.to_owned())
}
(None, None) => match env::var("TEMPDIR_ROOT").map(PathBuf::from) {
Ok(path) => DataDir::Temporary(TempDir::new_in(path)?),
Err(_) => DataDir::Temporary(TempDir::new()?),
},
};
let db_dir = format!("--lnddir={}", work_dir.path().display());
args.push(&db_dir);
let tls_path = format!("--tlscertpath={}/tls.cert", work_dir.path().display());
args.push(&tls_path);
let network = format!("--bitcoin.{}", conf.network);
args.push(&network);
args.push("--bitcoin.active");
args.push("--bitcoin.node=bitcoind");
let cookie = format!("--bitcoind.rpccookie={}", bitcoind_cookie);
args.push(&cookie);
let host = format!("--bitcoind.rpchost={}", bitcoind_rpc_socket);
args.push(&host);
let listen_port = get_available_port()?;
let listen_url = format!("0.0.0.0:{}", listen_port);
let listen_arg = format!("--listen={}", listen_url);
args.push(&listen_arg);
let grpc_port = get_available_port()?;
let grpc_url = format!("0.0.0.0:{}", grpc_port);
let grpc_arg = format!("--rpclisten={}", grpc_url);
args.push(&grpc_arg);
let rest_port = get_available_port()?;
let rest_url = format!("0.0.0.0:{}", rest_port);
let rest_arg = format!("--restlisten={}", rest_url);
args.push(&rest_arg);
args.push("--bitcoind.rpcpolling");
args.push("--bitcoind.blockpollinginterval=1s");
args.push("--noseedbackup");
args.push("--accept-keysend");
args.push("--accept-amp");
args.push("--protocol.wumbo-channels");
args.push("--protocol.option-scid-alias");
args.push("--maxpendingchannels=10");
args.push("--historicalsyncinterval=1s");
args.push("--trickledelay=1000");
let view_stderr = if conf.view_stdout {
Stdio::inherit()
} else {
Stdio::null()
};
let view_stdout = if conf.view_stdout {
Stdio::inherit()
} else {
Stdio::null()
};
let mut process = Command::new(&exe)
.args(args)
.stderr(view_stderr)
.stdout(view_stdout)
.spawn()
.with_context(|| format!("Error while executing {:?}", exe.as_ref()))?;
let cert_file = work_dir.path().join("tls.cert");
let macaroon_file = work_dir.path().join(format!(
"data/chain/bitcoin/{}/admin.macaroon",
conf.network
));
let client = loop {
if let Some(status) = process.try_wait()? {
if conf.attempts > 0 {
warn!("early exit with: {:?}. Trying to launch again ({} attempts remaining), maybe some other process used our available port", status, conf.attempts);
let mut conf = conf.clone();
conf.attempts -= 1;
return Self::with_conf(
exe,
&conf,
bitcoind_cookie,
bitcoind_rpc_socket,
#[cfg(feature = "bitcoind")]
bitcoind,
)
.await
.with_context(|| format!("Remaining attempts {}", conf.attempts));
} else {
error!("early exit with: {:?}", status);
return Err(Error::EarlyExit(status).into());
}
}
match tonic_lnd::connect(
format!("https://localhost:{}", grpc_port.clone()),
&cert_file,
&macaroon_file,
)
.await
{
Ok(client) => break client,
Err(e) => {
error!("Error creating client: {}", e);
std::thread::sleep(Duration::from_millis(500));
}
};
};
let cert = std::fs::read(cert_file)?;
let tls_cert = hex::encode(&cert);
let mac = std::fs::read(macaroon_file)?;
let admin_macaroon = hex::encode(&mac);
tokio::time::sleep(Duration::from_secs(5)).await;
Ok(Lnd {
process,
client,
work_dir,
grpc_url: format!("https://127.0.0.1:{}", grpc_port),
rest_url: format!("https://127.0.0.1:{}", rest_port),
listen_url: Some(format!("127.0.0.1:{}", listen_port)),
admin_macaroon,
tls_cert,
network: conf.network.to_string(),
})
}
pub fn trigger(&self) -> anyhow::Result<()> {
Ok(nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGUSR1,
)?)
}
pub fn workdir(&self) -> PathBuf {
self.work_dir.path()
}
pub fn admin_macaroon_path(&self) -> PathBuf {
let mac_path = format!("data/chain/bitcoin/{}/admin.macaroon", self.network);
self.workdir().join(mac_path)
}
pub fn tls_cert_path(&self) -> PathBuf {
self.workdir().join("tls.cert")
}
pub fn kill(&mut self) -> anyhow::Result<()> {
match self.work_dir {
DataDir::Persistent(_) => {
nix::sys::signal::kill(
nix::unistd::Pid::from_raw(self.process.id() as i32),
nix::sys::signal::SIGINT,
)?;
match self.process.wait() {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
}
DataDir::Temporary(_) => Ok(self.process.kill()?),
}
}
}
impl Drop for Lnd {
fn drop(&mut self) {
let _ = self.kill();
}
}
pub fn downloaded_exe_path() -> Option<String> {
if versions::HAS_FEATURE {
Some(format!(
"{}/lnd/{}/lnd",
env!("OUT_DIR"),
versions::lnd_name(),
))
} else {
None
}
}
pub fn exe_path() -> anyhow::Result<String> {
if let (Ok(_), Ok(_)) = (std::env::var("LND_EXEC"), std::env::var("LND_EXE")) {
return Err(error::Error::BothEnvVars.into());
}
if let Ok(path) = std::env::var("LND_EXEC") {
return Ok(path);
}
if let Ok(path) = std::env::var("LND_EXE") {
return Ok(path);
}
if let Some(path) = downloaded_exe_path() {
return Ok(path);
}
which::which("lnd")
.map_err(|_| Error::NoLndExecutableFound.into())
.map(|p| p.display().to_string())
}
#[cfg(all(test, feature = "bitcoind"))]
mod test {
use crate::exe_path;
use crate::Lnd;
use bitcoind::bitcoincore_rpc::RpcApi;
use bitcoind::BitcoinD;
use log::debug;
use std::env;
use tonic_lnd::lnrpc::GetInfoRequest;
#[test]
fn test_both_env_vars() {
env::set_var("LND_EXEC", "placeholder");
env::set_var("LND_EXE", "placeholder");
assert!(exe_path().is_err());
env::remove_var("LND_EXEC");
env::remove_var("LND_EXE");
}
#[tokio::test]
async fn two_lnd_nodes() {
let (lnd_exe, _, bitcoind) = setup_nodes().await;
let cookie = bitcoind.params.cookie_file.to_str().unwrap();
let rpc_socket = bitcoind.params.rpc_socket.to_string();
let lnd = Lnd::new(&lnd_exe, cookie.to_string(), rpc_socket, &bitcoind).await;
assert!(lnd.is_ok());
}
#[tokio::test]
async fn test_with_gen_blocks() {
let (_, _, bitcoind) = setup_nodes().await;
let address = bitcoind
.client
.get_new_address(None, None)
.unwrap()
.assume_checked();
bitcoind
.client
.generate_to_address(100, &address)
.expect("Blocks not generated to address.");
}
#[tokio::test]
async fn test_kill() {
let (_, mut lnd, bitcoind) = setup_nodes().await;
bitcoind.client.ping().unwrap(); let info = lnd.client.lightning().get_info(GetInfoRequest {}).await;
assert!(info.is_ok());
lnd.kill().unwrap();
let info = lnd.client.lightning().get_info(GetInfoRequest {}).await;
assert!(info.is_err());
}
pub(crate) async fn setup_nodes() -> (String, Lnd, BitcoinD) {
let (bitcoind_exe, lnd_exe) = init();
debug!("bitcoind: {}", &bitcoind_exe);
debug!("lnd: {}", &lnd_exe);
let bitcoin_conf = bitcoind::Conf::default();
let bitcoind = BitcoinD::with_conf(bitcoind_exe, &bitcoin_conf).unwrap();
let lnd_conf = super::LndConf::default();
let cookie = bitcoind.params.cookie_file.to_str().unwrap();
let rpc_socket = bitcoind.params.rpc_socket.to_string();
let lnd = Lnd::with_conf(
&lnd_exe,
&lnd_conf,
cookie.to_string(),
rpc_socket,
&bitcoind,
)
.await
.unwrap();
(lnd_exe, lnd, bitcoind)
}
#[tokio::test]
async fn list_unspent() {
let (_, mut lnd, _) = setup_nodes().await;
let request = tonic_lnd::walletrpc::ListUnspentRequest::default();
let unspent = lnd.client.wallet().list_unspent(request).await;
println!("{:?}", unspent);
assert!(unspent.is_ok())
}
fn init() -> (String, String) {
let bitcoind_exe_path = bitcoind::exe_path().unwrap();
let lnd_exe_path = exe_path().unwrap();
(bitcoind_exe_path, lnd_exe_path)
}
}