corepc_node/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2#![cfg_attr(docsrs, cfg_attr(all(), doc = include_str!("../README.md")))]
3
4pub extern crate corepc_client as client;
5
6#[rustfmt::skip]
7mod client_versions;
8mod versions;
9
10use std::ffi::OsStr;
11use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
12use std::path::{Path, PathBuf};
13use std::process::{Child, Command, ExitStatus, Stdio};
14use std::time::Duration;
15use std::{env, fmt, fs, thread};
16
17use anyhow::Context;
18use corepc_client::client_sync::{self, Auth};
19use tempfile::TempDir;
20pub use {anyhow, serde_json, tempfile, which};
21
22#[rustfmt::skip]                // Keep pubic re-exports separate.
23#[doc(inline)]
24pub use self::{
25    // Re-export `vtype` (the version specific types) and client defined types.
26    client_versions::*,
27    // Re-export the version string e.g., "28.0".
28    versions::VERSION,
29    // Re-export the model types as `mtype` to differentiate it from `vtype`.
30    client::types::model as mtype, // `types` is the `corepc-types` crate.
31};
32
33#[derive(Debug)]
34/// Struct representing the bitcoind process with related information.
35pub struct Node {
36    /// Process child handle, used to terminate the process when this struct is dropped.
37    process: Child,
38    /// Rpc client linked to this bitcoind process.
39    pub client: Client,
40    /// Work directory, where the node store blocks and other stuff.
41    work_dir: DataDir,
42
43    /// Contains information to connect to this node.
44    pub params: ConnectParams,
45}
46
47#[derive(Debug)]
48/// The DataDir struct defining the kind of data directory the node
49/// will contain. Data directory can be either persistent, or temporary.
50pub enum DataDir {
51    /// Persistent Data Directory.
52    Persistent(PathBuf),
53    /// Temporary Data Directory.
54    Temporary(TempDir),
55}
56
57impl DataDir {
58    /// Return the data directory path.
59    fn path(&self) -> PathBuf {
60        match self {
61            Self::Persistent(path) => path.to_owned(),
62            Self::Temporary(tmp_dir) => tmp_dir.path().to_path_buf(),
63        }
64    }
65}
66
67#[derive(Debug, Clone)]
68/// Contains all the information to connect to this node.
69pub struct ConnectParams {
70    /// Path to the node cookie file, useful for other client to connect to the node.
71    pub cookie_file: PathBuf,
72    /// Url of the rpc of the node, useful for other client to connect to the node.
73    pub rpc_socket: SocketAddrV4,
74    /// p2p connection url, is some if the node started with p2p enabled.
75    pub p2p_socket: Option<SocketAddrV4>,
76    /// zmq pub raw block connection url.
77    pub zmq_pub_raw_block_socket: Option<SocketAddrV4>,
78    /// zmq pub raw tx connection Url.
79    pub zmq_pub_raw_tx_socket: Option<SocketAddrV4>,
80}
81
82pub struct CookieValues {
83    pub user: String,
84    pub password: String,
85}
86
87impl ConnectParams {
88    /// Parses the cookie file content.
89    fn parse_cookie(content: String) -> Option<CookieValues> {
90        let values: Vec<_> = content.splitn(2, ':').collect();
91        let user = values.first()?.to_string();
92        let password = values.get(1)?.to_string();
93        Some(CookieValues { user, password })
94    }
95
96    /// Return the user and password values from cookie file.
97    pub fn get_cookie_values(&self) -> Result<Option<CookieValues>, std::io::Error> {
98        let cookie = std::fs::read_to_string(&self.cookie_file)?;
99        Ok(self::ConnectParams::parse_cookie(cookie))
100    }
101}
102
103/// Enum to specify p2p settings.
104#[derive(Debug, PartialEq, Eq, Clone)]
105pub enum P2P {
106    /// the node doesn't open a p2p port and work in standalone mode.
107    No,
108    /// the node open a p2p port.
109    Yes,
110    /// The node open a p2p port and also connects to the url given as parameter, it's handy to
111    /// initialize this with [Node::p2p_connect] of another node. The `bool` parameter indicates
112    /// if the node can accept connection too.
113    Connect(SocketAddrV4, bool),
114}
115
116/// All the possible error in this crate.
117pub enum Error {
118    /// Wrapper of io Error.
119    Io(std::io::Error),
120    /// Wrapper of bitcoincore_rpc Error.
121    Rpc(client_sync::Error),
122    /// Returned when calling methods requiring a feature to be activated, but it's not.
123    NoFeature,
124    /// Returned when calling methods requiring a env var to exist, but it's not.
125    NoEnvVar,
126    /// Returned when calling methods requiring the bitcoind executable but none is found
127    /// (no feature, no `BITCOIND_EXE`, no `bitcoind` in `PATH` ).
128    NoBitcoindExecutableFound,
129    /// Wrapper of early exit status.
130    EarlyExit(ExitStatus),
131    /// Returned when both tmpdir and staticdir is specified in `Conf` options.
132    BothDirsSpecified,
133    /// Returned when -rpcuser and/or -rpcpassword is used in `Conf` args.
134    /// It will soon be deprecated, please use -rpcauth instead.
135    RpcUserAndPasswordUsed,
136    /// Returned when expecting an auto-downloaded executable but `BITCOIND_SKIP_DOWNLOAD` env var is set.
137    SkipDownload,
138    /// Returned when bitcoind could not be reached after multiple attempts.
139    /// The attached string, if present, contains the error encountered when trying to connect.
140    NoBitcoindInstance(String),
141}
142
143impl fmt::Debug for Error {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        use Error::*;
146
147        match self {
148            Io(_) => write!(f, "io::Error"), // FIXME: Use bitcoin-internals.
149            Rpc(_) => write!(f, "bitcoin_rpc::Error"),
150            NoFeature => write!(f, "Called a method requiring a feature to be set, but it's not"),
151            NoEnvVar => write!(f, "Called a method requiring env var `BITCOIND_EXE` to be set, but it's not"),
152            NoBitcoindExecutableFound =>  write!(f, "`bitcoind` executable is required, provide it with one of the following: set env var `BITCOIND_EXE` or use a feature like \"22_1\" or have `bitcoind` executable in the `PATH`"),
153            EarlyExit(e) => write!(f, "The bitcoind process terminated early with exit code {}", e),
154            BothDirsSpecified => write!(f, "tempdir and staticdir cannot be enabled at same time in configuration options"),
155            RpcUserAndPasswordUsed => write!(f, "`-rpcuser` and `-rpcpassword` cannot be used, it will be deprecated soon and it's recommended to use `-rpcauth` instead which works alongside with the default cookie authentication"),
156            SkipDownload => write!(f, "expecting an auto-downloaded executable but `BITCOIND_SKIP_DOWNLOAD` env var is set"),
157            NoBitcoindInstance(msg) => write!(f, "it appears that bitcoind is not reachable: {}", msg),
158        }
159    }
160}
161
162impl std::fmt::Display for Error {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) }
164}
165
166impl std::error::Error for Error {
167    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
168        use Error::*;
169
170        match *self {
171            Error::Io(ref e) => Some(e),
172            Error::Rpc(ref e) => Some(e),
173            NoFeature
174            | NoEnvVar
175            | NoBitcoindExecutableFound
176            | EarlyExit(_)
177            | BothDirsSpecified
178            | RpcUserAndPasswordUsed
179            | SkipDownload
180            | NoBitcoindInstance(_) => None,
181        }
182    }
183}
184
185const LOCAL_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
186
187const INVALID_ARGS: [&str; 2] = ["-rpcuser", "-rpcpassword"];
188
189/// The node configuration parameters, implements a convenient [Default] for most common use.
190///
191/// `#[non_exhaustive]` allows adding new parameters without breaking downstream users.
192/// Users cannot instantiate the struct directly, they need to create it via the `default()` method
193/// and mutate fields according to their preference.
194///
195/// Default values:
196/// ```
197/// use corepc_node as bitcoind;
198/// let mut conf = bitcoind::Conf::default();
199/// conf.args = vec!["-regtest", "-fallbackfee=0.0001"];
200/// conf.view_stdout = false;
201/// conf.p2p = bitcoind::P2P::No;
202/// conf.network = "regtest";
203/// conf.tmpdir = None;
204/// conf.staticdir = None;
205/// conf.attempts = 5;
206/// assert_eq!(conf, bitcoind::Conf::default());
207/// ```
208///
209#[non_exhaustive]
210#[derive(Debug, PartialEq, Eq, Clone)]
211pub struct Conf<'a> {
212    /// Bitcoind command line arguments containing no spaces like `vec!["-dbcache=300", "-regtest"]`
213    /// note that `port`, `rpcport`, `connect`, `datadir`, `listen`
214    /// cannot be used because they are automatically initialized.
215    pub args: Vec<&'a str>,
216
217    /// if `true` bitcoind log output will not be suppressed.
218    pub view_stdout: bool,
219
220    /// Allows to specify options to open p2p port or connect to the another node.
221    pub p2p: P2P,
222
223    /// Must match what specified in args without dashes, needed to locate the cookie file
224    /// directory with different/esoteric networks.
225    pub network: &'a str,
226
227    /// Temporary directory path.
228    ///
229    /// Optionally specify a temporary or persistent working directory for the node.
230    /// The following two parameters can be configured to simulate desired working directory configuration.
231    ///
232    /// tmpdir is Some() && staticdir is Some() : Error. Cannot be enabled at same time.
233    /// tmpdir is Some(temp_path) && staticdir is None : Create temporary directory at `tmpdir` path.
234    /// tmpdir is None && staticdir is Some(work_path) : Create persistent directory at `staticdir` path.
235    /// tmpdir is None && staticdir is None: Creates a temporary directory in OS default temporary directory (eg /tmp) or `TEMPDIR_ROOT` env variable path.
236    ///
237    /// It may be useful for example to set to a ramdisk via `TEMPDIR_ROOT` env option so that
238    /// bitcoin nodes spawn very fast because their datadirs are in RAM. Should not be enabled with persistent
239    /// mode, as it cause memory overflows.
240    pub tmpdir: Option<PathBuf>,
241
242    /// Persistent directory path.
243    pub staticdir: Option<PathBuf>,
244
245    /// Try to spawn the process `attempt` time.
246    ///
247    /// The OS is giving available ports to use, however, they aren't booked, so it could rarely
248    /// happen they are used at the time the process is spawn. When retrying other available ports
249    /// are returned reducing the probability of conflicts to negligible.
250    pub attempts: u8,
251
252    /// Enable the ZMQ interface to be accessible.
253    pub enable_zmq: bool,
254
255    /// Load `wallet` after initialization.
256    pub wallet: Option<String>,
257}
258
259impl Default for Conf<'_> {
260    fn default() -> Self {
261        Conf {
262            args: vec!["-regtest", "-fallbackfee=0.0001"],
263            view_stdout: false,
264            p2p: P2P::No,
265            network: "regtest",
266            tmpdir: None,
267            staticdir: None,
268            attempts: 5,
269            enable_zmq: false,
270            wallet: Some("default".to_string()),
271        }
272    }
273}
274
275impl Node {
276    /// Launch the bitcoind process from the given `exe` executable with default args.
277    ///
278    /// Waits for the node to be ready to accept connections before returning.
279    pub fn new<S: AsRef<OsStr>>(exe: S) -> anyhow::Result<Node> {
280        Node::with_conf(exe, &Conf::default())
281    }
282
283    /// Launch the bitcoind process from the given `exe` executable with given [Conf] param and
284    /// create/load the "default" wallet.
285    ///
286    /// Waits for the node to be ready to accept connections before returning.
287    ///
288    /// # Parameters
289    ///
290    /// * `exe` - The path to the bitcoind executable.
291    /// * `conf` - The configuration parameters for the node.
292    ///
293    /// # Returns
294    ///
295    /// A [`Node`] instance if the node is successfully started and ready to accept connections.
296    ///
297    /// # Errors
298    ///
299    /// If the node fails to start after the specified number of attempts.
300    pub fn with_conf<S: AsRef<OsStr>>(exe: S, conf: &Conf) -> anyhow::Result<Node> {
301        for attempt in 0..conf.attempts {
302            let work_dir = Self::init_work_dir(conf)?;
303            let cookie_file = work_dir.path().join(conf.network).join(".cookie");
304
305            let rpc_port = get_available_port()?;
306            let rpc_socket = SocketAddrV4::new(LOCAL_IP, rpc_port);
307            let rpc_url = format!("http://{}", rpc_socket);
308
309            let (p2p_args, p2p_socket) = Self::p2p_args(&conf.p2p)?;
310            let (zmq_args, zmq_pub_raw_tx_socket, zmq_pub_raw_block_socket) =
311                Self::zmq_args(conf.enable_zmq)?;
312
313            let stdout = if conf.view_stdout { Stdio::inherit() } else { Stdio::null() };
314
315            let datadir_arg = format!("-datadir={}", work_dir.path().display());
316            let rpc_arg = format!("-rpcport={}", rpc_port);
317            let default_args = [&datadir_arg, &rpc_arg];
318            let conf_args = validate_args(conf.args.clone())?;
319
320            let mut process = Command::new(exe.as_ref())
321                .args(default_args)
322                .args(&p2p_args)
323                .args(&conf_args)
324                .args(&zmq_args)
325                .stdout(stdout)
326                .spawn()
327                .with_context(|| format!("Error while executing {:?}", exe.as_ref()))?;
328            match process.try_wait() {
329                Ok(Some(_)) | Err(_) => {
330                    // Process has exited or an error occurred, kill and retry
331                    let _ = process.kill();
332                    continue;
333                }
334                Ok(None) => {
335                    // Process is still running, proceed
336                }
337            }
338
339            if Self::wait_for_cookie_file(cookie_file.as_path(), Duration::from_secs(5)).is_err() {
340                // If the cookie file is not accessible a new work_dir is needed and therefore a new
341                // process. Kill the process and retry.
342                let _ = process.kill();
343                continue;
344            }
345            let auth = Auth::CookieFile(cookie_file.clone());
346
347            let client_base = Self::create_client_base(&rpc_url, &auth)?;
348            let client = match &conf.wallet {
349                Some(wallet) =>
350                    match Self::create_client_wallet(&client_base, &rpc_url, &auth, wallet) {
351                        Ok(client) => client,
352                        Err(e) =>
353                            if attempt == conf.attempts - 1 {
354                                return Err(e);
355                            } else {
356                                // If the wallet cannot be created or loaded, there might be an issue
357                                // with the work_dir or process. Kill the process and retry.
358                                let _ = process.kill();
359                                continue;
360                            },
361                    },
362                None => client_base,
363            };
364            if Self::wait_for_client(&client, Duration::from_secs(5)).is_err() {
365                // If the client times out there might be an issue with the work_dir or process. Kill
366                // the process and retry.
367                let _ = process.kill();
368                continue;
369            }
370
371            return Ok(Node {
372                process,
373                client,
374                work_dir,
375                params: ConnectParams {
376                    cookie_file,
377                    rpc_socket,
378                    p2p_socket,
379                    zmq_pub_raw_block_socket,
380                    zmq_pub_raw_tx_socket,
381                },
382            });
383        }
384        Err(anyhow::anyhow!("Failed to start the node after {} attempts", conf.attempts))
385    }
386
387    /// Initialize the work directory based on the provided configuration in [`Conf`].
388    ///
389    /// # Parameters
390    ///
391    /// * `conf` - Contains the paths for temporary (`tmpdir`) and static (`staticdir`)
392    ///   directories. If neither is specified, a temporary directory will be created in the
393    ///   system's default temporary directory.
394    fn init_work_dir(conf: &Conf) -> anyhow::Result<DataDir> {
395        let tmpdir =
396            conf.tmpdir.clone().or_else(|| env::var("TEMPDIR_ROOT").map(PathBuf::from).ok());
397        let work_dir = match (&tmpdir, &conf.staticdir) {
398            (Some(_), Some(_)) => return Err(Error::BothDirsSpecified.into()),
399            (Some(tmpdir), None) => DataDir::Temporary(TempDir::new_in(tmpdir)?),
400            (None, Some(workdir)) => {
401                fs::create_dir_all(workdir)?;
402                DataDir::Persistent(workdir.to_owned())
403            }
404            (None, None) => DataDir::Temporary(TempDir::new()?),
405        };
406        Ok(work_dir)
407    }
408
409    /// Returns the p2p args and the p2p socket address if any.
410    fn p2p_args(p2p: &P2P) -> anyhow::Result<(Vec<String>, Option<SocketAddrV4>)> {
411        match p2p {
412            P2P::No => Ok((vec!["-listen=0".to_string()], None)),
413            P2P::Yes => {
414                let p2p_port = get_available_port()?;
415                let p2p_socket = SocketAddrV4::new(LOCAL_IP, p2p_port);
416                let bind_arg = format!("-bind={}", p2p_socket);
417                let args = vec![bind_arg];
418                Ok((args, Some(p2p_socket)))
419            }
420            P2P::Connect(other_node_url, listen) => {
421                let p2p_port = get_available_port()?;
422                let p2p_socket = SocketAddrV4::new(LOCAL_IP, p2p_port);
423                let bind_arg = format!("-bind={}", p2p_socket);
424                let connect = format!("-connect={}", other_node_url);
425                let mut args = vec![bind_arg, connect];
426                if *listen {
427                    args.push("-listen=1".to_string())
428                }
429                Ok((args, Some(p2p_socket)))
430            }
431        }
432    }
433
434    /// Returns the zmq args and the zmq socket addresses if any.
435    ///
436    /// # Parameters
437    /// * `enable_zmq` - If `true`, creates two ZMQ sockets:
438    ///     - `zmq_pub_raw_tx_socket`: for raw transaction publishing.
439    ///     - `zmq_pub_raw_block_socket`: for raw block publishing.
440    fn zmq_args(
441        enable_zmq: bool,
442    ) -> anyhow::Result<(Vec<String>, Option<SocketAddrV4>, Option<SocketAddrV4>)> {
443        if enable_zmq {
444            let zmq_pub_raw_tx_port = get_available_port()?;
445            let zmq_pub_raw_tx_socket = SocketAddrV4::new(LOCAL_IP, zmq_pub_raw_tx_port);
446            let zmq_pub_raw_block_port = get_available_port()?;
447            let zmq_pub_raw_block_socket = SocketAddrV4::new(LOCAL_IP, zmq_pub_raw_block_port);
448            let zmqpubrawblock_arg =
449                format!("-zmqpubrawblock=tcp://0.0.0.0:{}", zmq_pub_raw_block_port);
450            let zmqpubrawtx_arg = format!("-zmqpubrawtx=tcp://0.0.0.0:{}", zmq_pub_raw_tx_port);
451            Ok((
452                vec![zmqpubrawtx_arg, zmqpubrawblock_arg],
453                Some(zmq_pub_raw_tx_socket),
454                Some(zmq_pub_raw_block_socket),
455            ))
456        } else {
457            Ok((vec![], None, None))
458        }
459    }
460
461    /// Returns `Ok` once the cookie file is accessible, or an error if it times out.
462    fn wait_for_cookie_file(cookie_file: &Path, timeout: Duration) -> anyhow::Result<()> {
463        let start = std::time::Instant::now();
464        while start.elapsed() < timeout {
465            if cookie_file.exists() {
466                return Ok(());
467            }
468            thread::sleep(Duration::from_millis(200));
469        }
470        Err(anyhow::anyhow!("timeout waiting for cookie file: {}", cookie_file.display()))
471    }
472
473    /// Returns `Ok` once the client can successfully call, or an error if it times out.
474    fn wait_for_client(client: &Client, timeout: Duration) -> anyhow::Result<()> {
475        let start = std::time::Instant::now();
476        while start.elapsed() < timeout {
477            // Test calling GetBlockchainInfo. Use serde value to be resilient to upstream changes.
478            if client.call::<serde_json::Value>("getblockchaininfo", &[]).is_ok() {
479                return Ok(());
480            }
481            thread::sleep(Duration::from_millis(200));
482        }
483        Err(anyhow::anyhow!("timeout waiting for client to be ready"))
484    }
485
486    /// Create a new RPC client connected to the given `rpc_url` with the provided `auth`.
487    ///
488    /// The client may not be immediately available, so retry up to 10 times.
489    fn create_client_base(rpc_url: &str, auth: &Auth) -> anyhow::Result<Client> {
490        for _ in 0..10 {
491            if let Ok(client) = Client::new_with_auth(rpc_url, auth.clone()) {
492                return Ok(client);
493            }
494            thread::sleep(Duration::from_millis(200));
495        }
496        Client::new_with_auth(rpc_url, auth.clone())
497            .map_err(|e| Error::NoBitcoindInstance(e.to_string()).into())
498    }
499
500    /// Create a new RPC client connected to the given `wallet`.
501    ///
502    /// If the wallet with the given name does not exist, it will create it.
503    /// If the wallet already exists, it will load it.
504    ///
505    /// The client or wallet may not be immediately available, so retry up to 10 times.
506    fn create_client_wallet(
507        client_base: &Client,
508        rpc_url: &str,
509        auth: &Auth,
510        wallet: &str,
511    ) -> anyhow::Result<Client> {
512        for _ in 0..10 {
513            // Try to create the wallet, or if that fails it might already exist so try to load it.
514            if client_base.create_wallet(wallet).is_ok() || client_base.load_wallet(wallet).is_ok()
515            {
516                let url = format!("{}/wallet/{}", rpc_url, wallet);
517                return Client::new_with_auth(&url, auth.clone())
518                    .map_err(|e| Error::NoBitcoindInstance(e.to_string()).into());
519            }
520            thread::sleep(Duration::from_millis(200));
521        }
522        Err(Error::NoBitcoindInstance("Could not create or load wallet".to_string()).into())
523    }
524
525    /// Returns the rpc URL including the schema eg. http://127.0.0.1:44842.
526    pub fn rpc_url(&self) -> String { format!("http://{}", self.params.rpc_socket) }
527
528    /// Returns the rpc URL including the schema and the given `wallet_name`.
529    /// eg. http://127.0.0.1:44842/wallet/my_wallet.
530    pub fn rpc_url_with_wallet<T: AsRef<str>>(&self, wallet_name: T) -> String {
531        format!("http://{}/wallet/{}", self.params.rpc_socket, wallet_name.as_ref())
532    }
533
534    /// Return the current workdir path of the running node.
535    pub fn workdir(&self) -> PathBuf { self.work_dir.path() }
536
537    /// Returns the [P2P] enum to connect to this node p2p port.
538    pub fn p2p_connect(&self, listen: bool) -> Option<P2P> {
539        self.params.p2p_socket.map(|s| P2P::Connect(s, listen))
540    }
541
542    /// Stop the node, waiting correct process termination.
543    pub fn stop(&mut self) -> anyhow::Result<ExitStatus> {
544        self.client.stop()?;
545        Ok(self.process.wait()?)
546    }
547
548    /// Create a new wallet in the running node, and return an RPC client connected to the just
549    /// created wallet.
550    pub fn create_wallet<T: AsRef<str>>(&self, wallet: T) -> anyhow::Result<Client> {
551        let _ = self.client.create_wallet(wallet.as_ref())?;
552        Ok(Client::new_with_auth(
553            &self.rpc_url_with_wallet(wallet),
554            Auth::CookieFile(self.params.cookie_file.clone()),
555        )?)
556    }
557}
558
559#[cfg(feature = "download")]
560impl Node {
561    /// create Node struct with the downloaded executable.
562    pub fn from_downloaded() -> anyhow::Result<Node> { Node::new(downloaded_exe_path()?) }
563
564    /// create Node struct with the downloaded executable and given Conf.
565    pub fn from_downloaded_with_conf(conf: &Conf) -> anyhow::Result<Node> {
566        Node::with_conf(downloaded_exe_path()?, conf)
567    }
568}
569
570impl Drop for Node {
571    fn drop(&mut self) {
572        if let DataDir::Persistent(_) = self.work_dir {
573            let _ = self.stop();
574        }
575        let _ = self.process.kill();
576    }
577}
578
579/// Returns a non-used local port if available.
580///
581/// Note there is a race condition during the time the method check availability and the caller.
582pub fn get_available_port() -> anyhow::Result<u16> {
583    // using 0 as port let the system assign a port available
584    let t = TcpListener::bind(("127.0.0.1", 0))?; // 0 means the OS choose a free port
585    Ok(t.local_addr().map(|s| s.port())?)
586}
587
588impl From<std::io::Error> for Error {
589    fn from(e: std::io::Error) -> Self { Error::Io(e) }
590}
591
592impl From<client_sync::Error> for Error {
593    fn from(e: client_sync::Error) -> Self { Error::Rpc(e) }
594}
595
596/// Provide the bitcoind executable path if a version feature has been specified.
597#[cfg(not(feature = "download"))]
598pub fn downloaded_exe_path() -> anyhow::Result<String> { Err(Error::NoFeature.into()) }
599
600/// Provide the bitcoind executable path if a version feature has been specified.
601#[cfg(feature = "download")]
602pub fn downloaded_exe_path() -> anyhow::Result<String> {
603    if std::env::var_os("BITCOIND_SKIP_DOWNLOAD").is_some() {
604        return Err(Error::SkipDownload.into());
605    }
606
607    let mut path: PathBuf = env!("OUT_DIR").into();
608    path.push("bitcoin");
609    path.push(format!("bitcoin-{}", VERSION));
610    path.push("bin");
611
612    if cfg!(target_os = "windows") {
613        path.push("bitcoind.exe");
614    } else {
615        path.push("bitcoind");
616    }
617
618    let path = format!("{}", path.display());
619    Ok(path)
620}
621
622/// Returns the daemon `bitcoind` executable with the following precedence:
623///
624/// 1) If it's specified in the `BITCOIND_EXE` env var.
625/// 2) If there is no env var but the auto-download feature is enabled, returns the
626///    path of the downloaded executable.
627/// 3) If neither of the precedent are available, the `bitcoind` executable is searched in the `PATH`.
628pub fn exe_path() -> anyhow::Result<String> {
629    if let Ok(path) = std::env::var("BITCOIND_EXE") {
630        return Ok(path);
631    }
632    if let Ok(path) = downloaded_exe_path() {
633        return Ok(path);
634    }
635    which::which("bitcoind")
636        .map_err(|_| Error::NoBitcoindExecutableFound.into())
637        .map(|p| p.display().to_string())
638}
639
640/// Validate the specified arg if there is any unavailable or deprecated one.
641pub fn validate_args(args: Vec<&str>) -> anyhow::Result<Vec<&str>> {
642    args.iter().try_for_each(|arg| {
643        // other kind of invalid arguments can be added into the list if needed
644        if INVALID_ARGS.iter().any(|x| arg.starts_with(x)) {
645            return Err(Error::RpcUserAndPasswordUsed);
646        }
647        Ok(())
648    })?;
649
650    Ok(args)
651}
652
653#[cfg(test)]
654mod test {
655    use std::net::SocketAddrV4;
656
657    use tempfile::TempDir;
658
659    use super::*;
660    use crate::{exe_path, get_available_port, Conf, Node, LOCAL_IP, P2P};
661
662    #[test]
663    fn test_local_ip() {
664        assert_eq!("127.0.0.1", format!("{}", LOCAL_IP));
665        let port = get_available_port().unwrap();
666        let socket = SocketAddrV4::new(LOCAL_IP, port);
667        assert_eq!(format!("127.0.0.1:{}", port), format!("{}", socket));
668    }
669
670    #[test]
671    fn test_node_get_blockchain_info() {
672        let exe = init();
673        let node = Node::new(exe).unwrap();
674        let info = node.client.get_blockchain_info().unwrap();
675        assert_eq!(0, info.blocks);
676    }
677
678    #[test]
679    fn test_node() {
680        let exe = init();
681        let node = Node::new(exe).unwrap();
682        let info = node.client.get_blockchain_info().unwrap();
683
684        assert_eq!(0, info.blocks);
685        let address = node.client.new_address().unwrap();
686        let _ = node.client.generate_to_address(1, &address).unwrap();
687        let info = node.client.get_blockchain_info().unwrap();
688        assert_eq!(1, info.blocks);
689    }
690
691    #[test]
692    #[cfg(feature = "0_21_2")]
693    fn test_getindexinfo() {
694        let exe = init();
695        let mut conf = Conf::default();
696        conf.args.push("-txindex");
697        let node = Node::with_conf(&exe, &conf).unwrap();
698        assert!(
699            node.client.server_version().unwrap() >= 210_000,
700            "getindexinfo requires bitcoin >0.21"
701        );
702        let info: std::collections::HashMap<String, serde_json::Value> =
703            node.client.call("getindexinfo", &[]).unwrap();
704        assert!(info.contains_key("txindex"));
705        assert!(node.client.server_version().unwrap() >= 210_000);
706    }
707
708    #[test]
709    fn test_p2p() {
710        let exe = init();
711
712        let conf = Conf::<'_> { p2p: P2P::Yes, ..Default::default() };
713        let node = Node::with_conf(&exe, &conf).unwrap();
714        assert_eq!(peers_connected(&node.client), 0);
715
716        let other_conf = Conf::<'_> { p2p: node.p2p_connect(false).unwrap(), ..Default::default() };
717        let other_node = Node::with_conf(&exe, &other_conf).unwrap();
718
719        assert_eq!(peers_connected(&node.client), 1);
720        assert_eq!(peers_connected(&other_node.client), 1);
721    }
722
723    #[cfg(not(target_os = "windows"))] // TODO: investigate why it doesn't work in windows
724    #[test]
725    fn test_data_persistence() {
726        // Create a Conf with staticdir type
727        let mut conf = Conf::default();
728        let datadir = TempDir::new().unwrap();
729        conf.staticdir = Some(datadir.path().to_path_buf());
730
731        // Start Node with persistent db config
732        // Generate 101 blocks
733        // Wallet balance should be 50
734        let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
735        let core_addrs = node.client.new_address().unwrap();
736        node.client.generate_to_address(101, &core_addrs).unwrap();
737        let wallet_balance_1 = node.client.get_balance().unwrap();
738        let best_block_1 = node.client.get_best_block_hash().unwrap();
739
740        drop(node);
741
742        // Start a new Node with the same datadir
743        let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
744
745        let wallet_balance_2 = node.client.get_balance().unwrap();
746        let best_block_2 = node.client.get_best_block_hash().unwrap();
747
748        // Check node chain data persists
749        assert_eq!(best_block_1, best_block_2);
750
751        // Check the node wallet balance persists
752        assert_eq!(wallet_balance_1, wallet_balance_2);
753    }
754
755    #[test]
756    fn test_multi_p2p() {
757        let exe = init();
758
759        let conf_node1 = Conf::<'_> { p2p: P2P::Yes, ..Default::default() };
760        let node1 = Node::with_conf(&exe, &conf_node1).unwrap();
761        assert_eq!(peers_connected(&node1.client), 0);
762
763        // Create Node 2 connected Node 1
764        let conf_node2 = Conf::<'_> { p2p: node1.p2p_connect(true).unwrap(), ..Default::default() };
765        let node2 = Node::with_conf(&exe, &conf_node2).unwrap();
766
767        // Create Node 3 Connected To Node
768        let conf_node3 =
769            Conf::<'_> { p2p: node2.p2p_connect(false).unwrap(), ..Default::default() };
770        let node3 = Node::with_conf(exe_path().unwrap(), &conf_node3).unwrap();
771
772        // Get each nodes Peers
773        let node1_peers = peers_connected(&node1.client);
774        let node2_peers = peers_connected(&node2.client);
775        let node3_peers = peers_connected(&node3.client);
776
777        // Peers found
778        assert!(node1_peers >= 1);
779        assert!(node2_peers >= 1);
780        assert_eq!(node3_peers, 1, "listen false but more than 1 peer");
781    }
782
783    #[cfg(feature = "0_19_1")]
784    #[test]
785    fn test_multi_wallet() {
786        use corepc_client::bitcoin::Amount;
787
788        let exe = init();
789        let node = Node::new(exe).unwrap();
790        let alice = node.create_wallet("alice").unwrap();
791        let alice_address = alice.new_address().unwrap();
792        let bob = node.create_wallet("bob").unwrap();
793        let bob_address = bob.new_address().unwrap();
794        node.client.generate_to_address(1, &alice_address).unwrap();
795        node.client.generate_to_address(101, &bob_address).unwrap();
796
797        let balances = alice.get_balances().unwrap();
798        let alice_balances: vtype::GetBalances = balances;
799
800        let balances = bob.get_balances().unwrap();
801        let bob_balances: vtype::GetBalances = balances;
802
803        assert_eq!(
804            Amount::from_btc(50.0).unwrap(),
805            Amount::from_btc(alice_balances.mine.trusted).unwrap()
806        );
807        assert_eq!(
808            Amount::from_btc(50.0).unwrap(),
809            Amount::from_btc(bob_balances.mine.trusted).unwrap()
810        );
811        assert_eq!(
812            Amount::from_btc(5000.0).unwrap(),
813            Amount::from_btc(bob_balances.mine.immature).unwrap()
814        );
815        let _txid = alice.send_to_address(&bob_address, Amount::from_btc(1.0).unwrap()).unwrap();
816
817        let balances = alice.get_balances().unwrap();
818        let alice_balances: vtype::GetBalances = balances;
819
820        assert!(
821            Amount::from_btc(alice_balances.mine.trusted).unwrap()
822                < Amount::from_btc(49.0).unwrap()
823                && Amount::from_btc(alice_balances.mine.trusted).unwrap()
824                    > Amount::from_btc(48.9).unwrap()
825        );
826
827        // bob wallet may not be immediately updated
828        for _ in 0..30 {
829            let balances = bob.get_balances().unwrap();
830            let bob_balances: vtype::GetBalances = balances;
831
832            if Amount::from_btc(bob_balances.mine.untrusted_pending).unwrap().to_sat() > 0 {
833                break;
834            }
835            std::thread::sleep(std::time::Duration::from_millis(100));
836        }
837        let balances = bob.get_balances().unwrap();
838        let bob_balances: vtype::GetBalances = balances;
839
840        assert_eq!(
841            Amount::from_btc(1.0).unwrap(),
842            Amount::from_btc(bob_balances.mine.untrusted_pending).unwrap()
843        );
844        assert!(node.create_wallet("bob").is_err(), "wallet already exist");
845    }
846
847    #[test]
848    fn test_node_rpcuser_and_rpcpassword() {
849        let exe = init();
850
851        let mut conf = Conf::default();
852        conf.args.push("-rpcuser=bitcoind");
853        conf.args.push("-rpcpassword=bitcoind");
854
855        let node = Node::with_conf(exe, &conf);
856
857        assert!(node.is_err());
858    }
859
860    #[test]
861    fn test_node_rpcauth() {
862        let exe = init();
863
864        let mut conf = Conf::default();
865        // rpcauth generated with [rpcauth.py](https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py)
866        // this could be also added to node, example: [RpcAuth](https://github.com/testcontainers/testcontainers-rs/blob/dev/testcontainers/src/images/coblox_bitcoincore.rs#L39-L91)
867        conf.args.push("-rpcauth=bitcoind:cccd5d7fd36e55c1b8576b8077dc1b83$60b5676a09f8518dcb4574838fb86f37700cd690d99bd2fdc2ea2bf2ab80ead6");
868
869        let node = Node::with_conf(exe, &conf).unwrap();
870
871        let auth = Auth::UserPass("bitcoind".to_string(), "bitcoind".to_string());
872        let client = Client::new_with_auth(
873            format!("{}/wallet/default", node.rpc_url().as_str()).as_str(),
874            auth,
875        )
876        .unwrap();
877        let info = client.get_blockchain_info().unwrap();
878        assert_eq!(0, info.blocks);
879
880        let address = client.new_address().unwrap();
881        let _ = client.generate_to_address(1, &address).unwrap();
882        let info = node.client.get_blockchain_info().unwrap();
883        assert_eq!(1, info.blocks);
884    }
885
886    #[test]
887    fn test_get_cookie_user_and_pass() {
888        let exe = init();
889        let node = Node::new(exe).unwrap();
890
891        let user: &str = "bitcoind_user";
892        let password: &str = "bitcoind_password";
893
894        std::fs::write(&node.params.cookie_file, format!("{}:{}", user, password)).unwrap();
895
896        let result_values = node.params.get_cookie_values().unwrap().unwrap();
897
898        assert_eq!(user, result_values.user);
899        assert_eq!(password, result_values.password);
900    }
901
902    #[test]
903    fn zmq_interface_enabled() {
904        let conf = Conf::<'_> { enable_zmq: true, ..Default::default() };
905        let node = Node::with_conf(exe_path().unwrap(), &conf).unwrap();
906
907        assert!(node.params.zmq_pub_raw_tx_socket.is_some());
908        assert!(node.params.zmq_pub_raw_block_socket.is_some());
909    }
910
911    #[test]
912    fn zmq_interface_disabled() {
913        let exe = init();
914        let node = Node::new(exe).unwrap();
915
916        assert!(node.params.zmq_pub_raw_tx_socket.is_none());
917        assert!(node.params.zmq_pub_raw_block_socket.is_none());
918    }
919
920    fn peers_connected(client: &Client) -> usize {
921        let json = client.get_peer_info().expect("get_peer_info");
922        json.0.len()
923    }
924
925    fn init() -> String {
926        let _ = env_logger::try_init();
927        exe_path().unwrap()
928    }
929}