corepc_node/
lib.rs

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