use core::error;
use core::fmt;
use core::net::Ipv4Addr;
use core::net::SocketAddr;
use corepc_client::bitcoin::BlockHash;
use std::net::TcpListener;
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use tempfile::TempDir;
pub use bitcoind::BitcoinD;
pub use utreexod::UtreexoD;
pub mod bitcoind;
pub mod utreexod;
const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
pub const NODE_BUILDING_MAX_RETRIES: u8 = 5;
pub const POLL_INTERVAL: Duration = Duration::from_millis(100);
pub const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
pub trait Node {
fn get_name() -> &'static str;
fn get_chain_tip(&self) -> Result<u32, Error>;
fn get_block_hash(&self, height: u32) -> Result<BlockHash, Error>;
fn poll_interval() -> Duration {
POLL_INTERVAL
}
fn wait_timeout() -> Duration {
WAIT_TIMEOUT
}
}
#[rustfmt::skip]
impl Node for BitcoinD {
fn get_name() -> &'static str { "bitcoind" }
fn get_chain_tip(&self) -> Result<u32, Error> { self.get_chain_tip() }
fn get_block_hash(&self, height: u32) -> Result<BlockHash, Error> { self.get_block_hash(height) }
}
#[rustfmt::skip]
impl Node for UtreexoD {
fn get_name() -> &'static str { "utreexod" }
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, proof index not yet available".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 poll_interval() -> Duration { 2 * POLL_INTERVAL }
fn wait_timeout() -> Duration { 2 * WAIT_TIMEOUT }
}
pub fn wait_for_height<N: Node>(node: &N, height: u32) -> Result<(), Error> {
let start = Instant::now();
while start.elapsed() < N::wait_timeout() {
if node.get_chain_tip().unwrap() >= height {
return Ok(());
}
thread::sleep(N::poll_interval());
}
let curr_height = node.get_chain_tip().unwrap();
Err(Error::ChainSyncTimeOut((
height,
curr_height,
N::wait_timeout(),
)))
}
pub fn wait_for_height_with_timeout<N: Node>(
node: &N,
height: u32,
timeout: Duration,
) -> Result<(), Error> {
let start = Instant::now();
while start.elapsed() < timeout {
if node.get_chain_tip().unwrap() >= height {
return Ok(());
}
thread::sleep(N::poll_interval());
}
let curr_height = node.get_chain_tip().unwrap();
Err(Error::ChainSyncTimeOut((height, curr_height, timeout)))
}
#[inline]
pub fn get_available_port() -> u16 {
TcpListener::bind((LOCALHOST, 0))
.unwrap()
.local_addr()
.unwrap()
.port()
}
#[derive(Debug)]
pub enum DataDir {
Persistent(PathBuf),
Temporary(TempDir),
}
impl DataDir {
pub fn path(&self) -> PathBuf {
match self {
Self::Persistent(path) => path.to_owned(),
Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
}
}
}
#[derive(Debug)]
pub enum Error {
BinaryNotFound((String, PathBuf)),
FailedToSpawn(std::io::Error),
ExhaustedNodeBuildingRetries,
FailedToStop(corepc_client::client_sync::Error),
Io(std::io::Error),
JsonRpc(corepc_client::client_sync::Error),
PeerConnectionTimeout((SocketAddr, SocketAddr)),
BothDirsSpecified,
UnresponsiveBitcoinD(corepc_client::client_sync::Error),
UnresponsiveUtreexoD(corepc_client::client_sync::Error),
CookieFileTimeout(PathBuf),
RpcClientSetupTimeout,
UnexpectedResponse(String),
ChainSyncTimeOut((u32, u32, Duration)), }
#[rustfmt::skip]
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Error::*;
match self {
BinaryNotFound((bin_name, path)) => write!(f, "The `{}` binary was not found at the expected location={}", bin_name, path.display()),
FailedToSpawn(err) => write!(f, "Failed to spawn a process for the node: {err:?}"),
ExhaustedNodeBuildingRetries => write!(f, "Failed to instantiate the node after {} attempts", NODE_BUILDING_MAX_RETRIES),
FailedToStop(err) => write!(f, "Failed to stop the node over JSON-RPC: {err:?}"),
Io(err) => write!(f, "I/O Error: {err:?}"),
JsonRpc(err) => write!(f, "JSON-RPC Error: {err:?}"),
PeerConnectionTimeout((local_socket, remote_socket)) => write!(f, "Timed out whilst waiting for connection between local={local_socket} and remote={remote_socket}"),
BothDirsSpecified => write!(f, "Both `tempdir` and `workdir` were specified. You must choose one and only one"),
UnresponsiveBitcoinD(err) => write!(f, "`BitcoinD` is unresponsive to JSON-RPC calls: {err:?}"),
UnresponsiveUtreexoD(err) => write!(f, "`UtreexoD` is unresponsive to JSON-RPC calls: {err:?}"),
CookieFileTimeout(cookie_path) => write!(f, "Timed out whilst waiting for the cookie={} to be generated", cookie_path.display()),
RpcClientSetupTimeout => write!(f, "Timed out whilst waiting for the JSON-RPC client to be ready"),
UnexpectedResponse(err) => write!(f, "Received an unexpected response from the JSON-RPC server: {err:?}"),
ChainSyncTimeOut((target_height, current_height, t)) => write!(
f,
"Timed out after {} seconds whilst waiting for the node's chain to synchronize to height={} (current height={})",
target_height, current_height, t.as_secs()
),
}
}
}
impl error::Error for Error {}