1use core::error;
31use core::fmt;
32use core::net::Ipv4Addr;
33use core::net::SocketAddr;
34use std::net::TcpListener;
35use std::path::PathBuf;
36use std::thread;
37use std::time::Duration;
38use std::time::Instant;
39use tempfile::TempDir;
40
41pub use bitcoind::BitcoinD;
42pub use utreexod::UtreexoD;
43
44pub mod bitcoind;
45pub mod utreexod;
46
47const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
49
50pub const NODE_BUILDING_MAX_RETRIES: u8 = 5;
52
53pub const POLL_INTERVAL: Duration = Duration::from_millis(100);
55
56pub const WAIT_TIMEOUT: Duration = Duration::from_secs(10);
58
59pub trait Node {
61 fn get_name() -> &'static str;
63
64 fn get_height(&self) -> Result<u32, Error>;
66
67 fn poll_interval() -> Duration {
73 POLL_INTERVAL
74 }
75
76 fn wait_timeout() -> Duration {
83 WAIT_TIMEOUT
84 }
85}
86
87#[rustfmt::skip]
88impl Node for BitcoinD {
89 fn get_name() -> &'static str { "bitcoind" }
90 fn get_height(&self) -> Result<u32, Error> { self.get_height() }
91}
92
93#[rustfmt::skip]
94impl Node for UtreexoD {
95 fn get_name() -> &'static str { "utreexod" }
96 fn get_height(&self) -> Result<u32, Error> { self.get_height() }
97 fn poll_interval() -> Duration { 2 * POLL_INTERVAL }
98 fn wait_timeout() -> Duration { 2 * WAIT_TIMEOUT }
99}
100
101pub fn wait_for_height<N: Node>(node: &N, height: u32) -> Result<(), Error> {
105 let start = Instant::now();
106 while start.elapsed() < N::wait_timeout() {
107 if node.get_height().unwrap() >= height {
108 return Ok(());
109 }
110 thread::sleep(N::poll_interval());
111 }
112
113 let curr_height = node.get_height().unwrap();
114 Err(Error::ChainSyncTimeOut((
115 height,
116 curr_height,
117 N::wait_timeout(),
118 )))
119}
120
121pub fn wait_for_height_with_timeout<N: Node>(
125 node: &N,
126 height: u32,
127 timeout: Duration,
128) -> Result<(), Error> {
129 let start = Instant::now();
130 while start.elapsed() < timeout {
131 if node.get_height().unwrap() >= height {
132 return Ok(());
133 }
134 thread::sleep(N::poll_interval());
135 }
136
137 let curr_height = node.get_height().unwrap();
138 Err(Error::ChainSyncTimeOut((height, curr_height, timeout)))
139}
140
141#[inline]
145pub fn get_available_port() -> u16 {
146 TcpListener::bind((LOCALHOST, 0))
147 .unwrap()
148 .local_addr()
149 .unwrap()
150 .port()
151}
152
153#[derive(Debug)]
160pub enum DataDir {
161 Persistent(PathBuf),
163 Temporary(TempDir),
165}
166
167impl DataDir {
168 pub fn path(&self) -> PathBuf {
170 match self {
171 Self::Persistent(path) => path.to_owned(),
172 Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
173 }
174 }
175}
176
177#[derive(Debug)]
178pub enum Error {
179 BinaryNotFound((String, PathBuf)),
181 FailedToSpawn(std::io::Error),
183 ExhaustedNodeBuildingRetries,
185 FailedToStop(corepc_client::client_sync::Error),
187 Io(std::io::Error),
189 JsonRpc(corepc_client::client_sync::Error),
191 PeerConnectionTimeout((SocketAddr, SocketAddr)),
193 BothDirsSpecified,
195 UnresponsiveBitcoinD(corepc_client::client_sync::Error),
197 UnresponsiveUtreexoD(corepc_client::client_sync::Error),
199 CookieFileTimeout(PathBuf),
201 RpcClientSetupTimeout,
203 UnexpectedResponse,
205 ChainSyncTimeOut((u32, u32, Duration)), }
208
209#[rustfmt::skip]
210impl fmt::Display for Error {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 use Error::*;
213 match self {
214 BinaryNotFound((bin_name, path)) => write!(f, "The `{}` binary was not found at the expected location={}", bin_name, path.display()),
215 FailedToSpawn(err) => write!(f, "Failed to spawn a process for the node: {err:?}"),
216 ExhaustedNodeBuildingRetries => write!(f, "Failed to instantiate the node after {} attempts", NODE_BUILDING_MAX_RETRIES),
217 FailedToStop(err) => write!(f, "Failed to stop the node over JSON-RPC: {err:?}"),
218 Io(err) => write!(f, "I/O Error: {err:?}"),
219 JsonRpc(err) => write!(f, "JSON-RPC Error: {err:?}"),
220 PeerConnectionTimeout((local_socket, remote_socket)) => write!(f, "Timed out whilst waiting for connection between local={local_socket} and remote={remote_socket}"),
221 BothDirsSpecified => write!(f, "Both `tempdir` and `workdir` were specified. You must choose one and only one"),
222 UnresponsiveBitcoinD(err) => write!(f, "`BitcoinD` is unresponsive to JSON-RPC calls: {err:?}"),
223 UnresponsiveUtreexoD(err) => write!(f, "`UtreexoD` is unresponsive to JSON-RPC calls: {err:?}"),
224 CookieFileTimeout(cookie_path) => write!(f, "Timed out whilst waiting for the cookie={} to be generated", cookie_path.display()),
225 RpcClientSetupTimeout => write!(f, "Timed out whilst waiting for the JSON-RPC client to be ready"),
226 UnexpectedResponse => write!(f, "Received an unexpected response from the JSON-RPC server"),
227 ChainSyncTimeOut((target_height, current_height, t)) => write!(
228 f,
229 "Timed out after {} seconds whilst waiting for the node's chain to synchronize to height={} (current height={})",
230 target_height, current_height, t.as_secs()
231 ),
232 }
233 }
234}
235
236impl error::Error for Error {}