use core::net::SocketAddr;
use core::net::SocketAddrV4;
use std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
use std::process::ExitStatus;
use std::process::Stdio;
use std::thread;
use std::thread::sleep;
use std::time::Duration;
use std::time::Instant;
use corepc_client::bitcoin::BlockHash;
use corepc_client::client_sync::Auth;
use corepc_client::client_sync::v17::AddNodeCommand;
use corepc_client::client_sync::v17::Client;
use crate::CONNECTION_INTERVAL;
use crate::CONNECTION_TIMEOUT;
use crate::DataDir;
use crate::Error;
use crate::IPV4_LOCALHOST;
use crate::NODE_BUILDING_INTERVAL;
use crate::NODE_BUILDING_MAX_RETRIES;
use crate::Node;
use crate::POLL_INTERVAL;
use crate::WAIT_TIMEOUT;
use crate::get_available_port;
mod versions;
const RPC_USER: &str = "halfin";
const RPC_PASS: &str = "halfin";
pub fn get_utreexod_path() -> Result<PathBuf, Error> {
let bin_name = UtreexoD::get_name().to_string();
#[allow(unused_mut)]
let mut bin_path = PathBuf::from(option_env!("HALFIN_UTREEXOD_PATH").unwrap_or(""));
#[cfg(target_os = "windows")]
if bin_path.extension().is_none() {
bin_path.set_extension("exe");
}
match bin_path.exists() {
true => Ok(bin_path),
false => Err(Error::BinaryNotFound((bin_name, bin_path))),
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct UtreexoDConf<'a> {
pub args: Vec<&'a str>,
pub tmpdir: Option<PathBuf>,
pub staticdir: Option<PathBuf>,
pub max_retries: u8,
}
impl Default for UtreexoDConf<'_> {
fn default() -> Self {
UtreexoDConf {
args: vec![
"--regtest",
"--notls",
"--cfilters",
"--noassumeutreexo",
"--miningaddr=bcrt1qusgerygumpd0ztn735s5pypq6wsv2zzhuc4yak",
],
tmpdir: None,
staticdir: None,
max_retries: NODE_BUILDING_MAX_RETRIES,
}
}
}
#[derive(Debug)]
pub struct UtreexoD {
process: Child,
rpc_client: Client,
working_directory: DataDir,
rpc_socket: SocketAddr,
p2p_socket: SocketAddr,
}
#[rustfmt::skip]
impl Node for UtreexoD {
fn get_name() -> &'static str { "utreexod_v_0_5_0" }
fn get_p2p_socket(&self) -> SocketAddr { self.get_p2p_socket() }
fn has_peer(&self, socket: SocketAddr) -> Result<bool, Error> { self.has_peer(socket) }
fn add_peer(&self, socket: SocketAddr) -> Result<(), Error> { self.add_peer(socket) }
fn get_peer_count(&self) -> Result<u32, Error> { self.get_peer_count() }
fn get_chain_tip(&self) -> Result<u32, Error> {
let height = self.get_chain_tip()?;
if height == 0 {
return Err(
Error::UnexpectedResponse("utreexod is at genesis, the proof index not ready yet".to_string())
);
}
self.get_block_uproof(height)?;
Ok(height)
}
fn get_block_hash(&self, height: u32) -> Result<BlockHash, Error> { self.get_block_hash(height) }
fn call(&self, method: &str, args: &[serde_json::Value]) -> Result<serde_json::Value, Error> {
self.rpc_client.call(method, args).map_err(Error::JsonRpc)
}
fn poll_interval() -> Duration { 2 * POLL_INTERVAL }
fn wait_timeout() -> Duration { 2 * WAIT_TIMEOUT }
}
impl UtreexoD {
pub fn new() -> Result<UtreexoD, Error> {
UtreexoD::from_bin(get_utreexod_path()?)
}
pub fn new_with_conf(conf: &UtreexoDConf) -> Result<UtreexoD, Error> {
UtreexoD::from_bin_with_conf(get_utreexod_path()?, conf)
}
pub fn from_bin<P: AsRef<Path>>(utreexod_bin: P) -> Result<UtreexoD, Error> {
UtreexoD::from_bin_with_conf(utreexod_bin, &UtreexoDConf::default())
}
pub fn from_bin_with_conf<P: AsRef<Path>>(
utreexod_bin: P,
conf: &UtreexoDConf,
) -> Result<UtreexoD, Error> {
for _attempt in 0..conf.max_retries {
let working_directory = Self::init_work_dir(conf)?;
let rpc_port = get_available_port();
let rpc_socket = SocketAddr::V4(SocketAddrV4::new(IPV4_LOCALHOST, rpc_port));
let rpc_url = format!("http://{}", rpc_socket);
let p2p_port = get_available_port();
let p2p_socket = SocketAddr::V4(SocketAddrV4::new(IPV4_LOCALHOST, p2p_port));
let datadir_arg = format!("--datadir={}", working_directory.path().display());
let rpclisten_arg = format!("--rpclisten=127.0.0.1:{}", rpc_port);
let rpcuser_arg = format!("--rpcuser={}", RPC_USER);
let rpcpass_arg = format!("--rpcpass={}", RPC_PASS);
let listen_arg = format!("--listen=127.0.0.1:{}", p2p_port);
let mut process = Command::new(utreexod_bin.as_ref())
.args(&conf.args)
.arg(&datadir_arg)
.arg(&rpclisten_arg)
.arg(&rpcuser_arg)
.arg(&rpcpass_arg)
.arg(&listen_arg)
.arg("--prune=0")
.arg("--flatutreexoproofindex")
.arg("--utreexoproofindexmaxmemory=512")
.arg("--v2transport")
.stdout(Stdio::null())
.spawn()
.map_err(Error::FailedToSpawn)?;
thread::sleep(NODE_BUILDING_INTERVAL);
match process.try_wait() {
Ok(Some(_)) | Err(_) => {
let _ = process.kill();
continue;
}
Ok(None) => {}
}
let auth = Auth::UserPass(RPC_USER.to_string(), RPC_PASS.to_string());
match Self::wait_for_client(&rpc_url, &auth, Duration::from_secs(10)) {
Ok(rpc_client) => {
sleep(Duration::from_millis(200));
return Ok(UtreexoD {
process,
rpc_client,
working_directory,
rpc_socket,
p2p_socket,
});
}
Err(_) => {
let _ = process.kill();
continue;
}
}
}
Err(Error::ExhaustedNodeBuildingRetries)
}
pub fn stop(&mut self) -> Result<ExitStatus, Error> {
let _ = self.rpc_client.stop().map_err(Error::FailedToStop)?;
let exit_status = self.process.wait().map_err(Error::Io)?;
Ok(exit_status)
}
pub fn get_pid(&self) -> u32 {
self.process.id()
}
pub fn get_working_directory(&self) -> PathBuf {
self.working_directory.path()
}
pub fn get_rpc_client(&self) -> &Client {
&self.rpc_client
}
pub fn get_p2p_socket(&self) -> SocketAddr {
self.p2p_socket
}
pub fn rpc_socket(&self) -> SocketAddr {
self.rpc_socket
}
pub fn get_chain_tip(&self) -> Result<u32, Error> {
let height = self
.rpc_client
.call::<serde_json::Value>("getblockchaininfo", &[])
.map_err(Error::JsonRpc)?["blocks"]
.as_u64()
.ok_or(Error::UnexpectedResponse(
"getblockchaininfo returned no `blocks` field".to_string(),
))? as u32;
Ok(height)
}
pub fn get_block_hash(&self, height: u32) -> Result<BlockHash, Error> {
let hash = self
.rpc_client
.call::<serde_json::Value>("getblockhash", &[height.into()])
.map_err(Error::JsonRpc)?
.as_str()
.ok_or(Error::UnexpectedResponse(
"getblockhash returned a non-string value".to_string(),
))?
.parse::<BlockHash>()
.map_err(|e| Error::UnexpectedResponse(e.to_string()))?;
Ok(hash)
}
pub fn get_block_uproof(&self, height: u32) -> Result<String, Error> {
let block_hash = self.get_block_hash(height)?;
let proof_hex = self
.rpc_client
.call::<serde_json::Value>("getutreexoproof", &[block_hash.to_string().into()])
.map_err(Error::JsonRpc)?
.as_str()
.ok_or(Error::UnexpectedResponse(
"getutreexoproof returned a non-string value".to_string(),
))?
.to_string();
Ok(proof_hex)
}
pub fn has_peer(&self, socket: SocketAddr) -> Result<bool, Error> {
let peers = self
.rpc_client
.call::<serde_json::Value>("getpeerinfo", &[])
.map_err(Error::JsonRpc)?;
let has_peer = peers
.as_array()
.map(|v| {
v.iter().any(|p| {
let inbound = p["inbound"].as_bool().unwrap_or(false);
if inbound {
p["addrlocal"]
.as_str()
.map(|a| a.contains(&self.p2p_socket.to_string()))
.unwrap_or(false)
} else {
p["addr"]
.as_str()
.map(|a| a.contains(&socket.to_string()))
.unwrap_or(false)
}
})
})
.unwrap_or(false);
Ok(has_peer)
}
pub fn add_peer(&self, socket: SocketAddr) -> Result<(), Error> {
self.rpc_client
.add_node(&socket.to_string(), AddNodeCommand::Add)
.map_err(Error::JsonRpc)?;
let mut delay = CONNECTION_INTERVAL;
let start = Instant::now();
while start.elapsed() < CONNECTION_TIMEOUT {
let peers = self
.rpc_client
.call::<serde_json::Value>("getpeerinfo", &[])
.map_err(Error::JsonRpc)?;
if peers
.as_array()
.map(|v| {
v.iter().any(|p| {
p["addr"]
.as_str()
.map(|a| a.contains(&socket.to_string()))
.unwrap_or(false)
})
})
.unwrap_or(false)
{
return Ok(());
}
thread::sleep(delay);
delay = (delay * 2).min(Duration::from_secs(1));
}
Err(Error::PeerConnectionTimeout((
self.get_p2p_socket(),
socket,
)))
}
pub fn get_peer_count(&self) -> Result<u32, Error> {
let peers = self
.rpc_client
.call::<serde_json::Value>("getpeerinfo", &[])
.map_err(Error::JsonRpc)?;
let peer_count = peers
.as_array()
.ok_or(Error::UnexpectedResponse(
"getpeerinfo returned a non-array value".to_string(),
))?
.len() as u32;
Ok(peer_count)
}
pub fn generate(&self, count: u32) -> Result<Vec<BlockHash>, Error> {
let hashes = self
.rpc_client
.call::<serde_json::Value>("generate", &[serde_json::to_value(count).unwrap()])
.map_err(Error::JsonRpc)?
.as_array()
.ok_or(Error::UnexpectedResponse(
"generate returned a non-array value".to_string(),
))?
.iter()
.map(|h| {
h.as_str()
.ok_or(Error::UnexpectedResponse(
"generate returned a non-string hash".to_string(),
))?
.parse::<BlockHash>()
.map_err(|e| Error::UnexpectedResponse(e.to_string()))
})
.collect::<Result<Vec<BlockHash>, Error>>()?;
Ok(hashes)
}
fn init_work_dir(conf: &UtreexoDConf) -> Result<DataDir, Error> {
let tmpdir = conf
.tmpdir
.clone()
.or_else(|| env::var("TEMPDIR_ROOT").map(PathBuf::from).ok());
let work_dir = match (&tmpdir, &conf.staticdir) {
(Some(_), Some(_)) => return Err(Error::BothDirsSpecified),
(None, Some(workdir)) => {
fs::create_dir_all(workdir).map_err(Error::Io)?;
DataDir::Persistent(workdir.to_owned())
}
(Some(tmpdir), None) => DataDir::Temporary(
tempfile::Builder::new()
.prefix("halfin-utreexod-")
.tempdir_in(tmpdir)
.map_err(Error::Io)?,
),
(None, None) => DataDir::Temporary(
tempfile::Builder::new()
.prefix("halfin-utreexod-")
.tempdir()
.map_err(Error::Io)?,
),
};
Ok(work_dir)
}
fn wait_for_client(rpc_url: &str, auth: &Auth, timeout: Duration) -> Result<Client, Error> {
let start = Instant::now();
while start.elapsed() < timeout {
if let Ok(client) = Client::new_with_auth(rpc_url, auth.clone()) {
if client
.call::<serde_json::Value>("getblockchaininfo", &[])
.is_ok()
{
return Ok(client);
}
}
thread::sleep(Duration::from_millis(200));
}
Err(Error::RpcClientSetupTimeout)
}
}
impl Drop for UtreexoD {
fn drop(&mut self) {
let _ = self.process.kill();
}
}