pub extern crate corepc_client as client;
mod client_versions;
mod versions;
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::time::Duration;
use std::time::Instant;
use corepc_client::bitcoin::Network;
use corepc_client::client_sync::Auth;
use corepc_client::client_sync::v30::AddNodeCommand;
use corepc_client::client_sync::v30::Client;
use tempfile::TempDir;
use crate::DataDir;
use crate::Error;
use crate::LOCALHOST;
use crate::NODE_BUILDING_MAX_RETRIES;
use crate::get_available_port;
const BITCOIND_WALLET: &str = "wallet";
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct BitcoinDConf<'a> {
pub args: Vec<&'a str>,
pub tmpdir: Option<PathBuf>,
pub staticdir: Option<PathBuf>,
pub max_retries: u8,
}
impl Default for BitcoinDConf<'_> {
fn default() -> Self {
BitcoinDConf {
args: vec!["-regtest", "-fallbackfee=0.0001"],
tmpdir: None,
staticdir: None,
max_retries: NODE_BUILDING_MAX_RETRIES,
}
}
}
#[derive(Debug)]
pub struct BitcoinD {
process: Child,
rpc_client: Client,
working_directory: DataDir,
cookie_file: PathBuf,
rpc_socket: SocketAddr,
p2p_socket: SocketAddr,
}
impl Drop for BitcoinD {
fn drop(&mut self) {
if let DataDir::Persistent(_) = self.working_directory {
let _ = self.stop();
}
let _ = self.process.kill();
}
}
impl BitcoinD {
pub fn download_new() -> Result<BitcoinD, Error> {
BitcoinD::from_bin(get_bitcoind_path()?)
}
pub fn download_new_with_conf(conf: &BitcoinDConf) -> Result<BitcoinD, Error> {
BitcoinD::from_bin_with_conf(get_bitcoind_path()?, conf)
}
pub fn from_bin<P: AsRef<Path>>(bitcoind_bin: P) -> Result<BitcoinD, Error> {
BitcoinD::from_bin_with_conf(bitcoind_bin, &BitcoinDConf::default())
}
pub fn from_bin_with_conf<P: AsRef<Path>>(
bitcoind_bin: P,
conf: &BitcoinDConf,
) -> Result<BitcoinD, Error> {
for _ in 0..=conf.max_retries {
let working_directory = Self::init_work_dir(conf)?;
let cookie_file = working_directory
.path()
.join(Network::Regtest.to_string())
.join(".cookie");
let rpc_port = get_available_port();
let rpc_socket = SocketAddr::V4(SocketAddrV4::new(LOCALHOST, rpc_port));
let rpc_url = format!("http://{}", rpc_socket);
let p2p_port = get_available_port();
let p2p_socket = SocketAddr::V4(SocketAddrV4::new(LOCALHOST, p2p_port));
let datadir_arg = format!("-datadir={}", working_directory.path().display());
let rpc_arg = format!("-rpcport={}", rpc_port);
let p2p_arg = format!("-bind={}", p2p_socket);
let mut process = Command::new(bitcoind_bin.as_ref())
.args(&conf.args)
.arg(&datadir_arg)
.arg(&rpc_arg)
.arg(&p2p_arg)
.stdout(Stdio::null())
.spawn()
.map_err(Error::FailedToSpawn)?;
thread::sleep(Duration::from_millis(100));
match process.try_wait() {
Ok(Some(_)) | Err(_) => {
let _ = process.kill();
continue;
}
Ok(None) => {}
}
if Self::wait_for_cookie_file(&cookie_file, Duration::from_secs(5)).is_err() {
let _ = process.kill();
continue;
}
let wallet_url = format!("{}/wallet/{}", rpc_url, BITCOIND_WALLET);
let auth = Auth::CookieFile(cookie_file.clone());
let client_base = Self::create_base_rpc_client(&rpc_url, &auth)?;
let deadline = Instant::now() + Duration::from_secs(5);
let rpc_client = loop {
if Instant::now() > deadline {
let _ = process.kill();
continue;
}
if client_base.create_wallet(BITCOIND_WALLET).is_ok()
|| client_base.load_wallet(BITCOIND_WALLET).is_ok()
{
if let Ok(client) = Client::new_with_auth(&wallet_url, auth.clone()) {
break client;
}
}
thread::sleep(Duration::from_millis(200));
};
if Self::wait_for_client(&rpc_client, Duration::from_secs(5)).is_err() {
let _ = process.kill();
continue;
}
return Ok(BitcoinD {
process,
rpc_client,
working_directory,
cookie_file,
rpc_socket,
p2p_socket,
});
}
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_p2p_socket(&self) -> SocketAddr {
self.p2p_socket
}
pub fn get_rpc_client(&self) -> &Client {
&self.rpc_client
}
pub fn rpc_socket(&self) -> SocketAddr {
self.rpc_socket
}
pub fn cookie_file(&self) -> &Path {
&self.cookie_file
}
pub fn get_height(&self) -> Result<u32, Error> {
let response = self
.rpc_client
.get_blockchain_info()
.map_err(Error::JsonRpc)?;
let height = response.blocks as u32;
Ok(height)
}
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 = Duration::from_millis(100);
let timeout = Duration::from_secs(5);
let start = Instant::now();
while start.elapsed() < timeout {
let peers = self.rpc_client.get_peer_info().map_err(Error::JsonRpc)?;
if peers
.0
.iter()
.any(|p| p.address.contains(&socket.to_string()))
{
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.get_peer_info().map_err(Error::JsonRpc)?.0;
let peer_count = peers.len() as u32;
Ok(peer_count)
}
pub fn generate(&self, count: u32) -> Result<Vec<String>, Error> {
let address = self.rpc_client.new_address().map_err(Error::JsonRpc)?;
let hashes = self
.rpc_client
.generate_to_address(count as usize, &address)
.map_err(Error::JsonRpc)?
.0;
Ok(hashes)
}
fn init_work_dir(conf: &BitcoinDConf) -> 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(TempDir::new_in(tmpdir).map_err(Error::Io)?),
(None, None) => DataDir::Temporary(TempDir::new().map_err(Error::Io)?),
};
Ok(work_dir)
}
fn create_base_rpc_client(rpc_url: &str, auth: &Auth) -> Result<Client, Error> {
for _ in 0..10 {
if let Ok(client) = Client::new_with_auth(rpc_url, auth.clone()) {
return Ok(client);
}
thread::sleep(Duration::from_millis(200));
}
let client =
Client::new_with_auth(rpc_url, auth.clone()).map_err(Error::UnresponsiveBitcoinD)?;
Ok(client)
}
fn wait_for_cookie_file(cookie_file: &Path, timeout: Duration) -> Result<(), Error> {
let start = Instant::now();
while start.elapsed() < timeout {
if cookie_file.exists() {
return Ok(());
}
thread::sleep(Duration::from_millis(200));
}
Err(Error::CookieFileTimeout(cookie_file.into()))
}
fn wait_for_client(rpc_client: &Client, timeout: Duration) -> Result<(), Error> {
let start = Instant::now();
while start.elapsed() < timeout {
if rpc_client.get_blockchain_info().is_ok() {
return Ok(());
}
thread::sleep(Duration::from_millis(200));
}
Err(Error::RpcClientSetupTimeout)
}
}
pub fn get_bitcoind_path() -> Result<PathBuf, Error> {
use versions::BITCOIND_VERSION;
let mut bin_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("bin");
bin_path.push(format!("bitcoin-{}", BITCOIND_VERSION));
bin_path.push("bitcoind");
match bin_path.exists() {
true => Ok(bin_path),
false => Err(Error::BinaryNotFound(bin_path)),
}
}
#[cfg(test)]
mod test {
use crate::wait_for_height;
use super::*;
#[test]
fn test_bitcoind_starts() {
let bin = get_bitcoind_path().unwrap();
let bitcoind = BitcoinD::from_bin(bin).unwrap();
println!("PID: {}", bitcoind.get_pid());
println!("Working Directory: {:?}", bitcoind.get_working_directory());
println!("P2P Socket: {}", bitcoind.get_p2p_socket());
}
#[test]
fn test_bitcoind_generate() {
let bitcoind = BitcoinD::download_new().unwrap();
let height = bitcoind.get_height().unwrap();
assert_eq!(height, 0);
bitcoind.generate(10).unwrap();
let height = bitcoind.get_height().unwrap();
assert_eq!(height, 10);
}
#[test]
fn test_bitcoind_addnode() {
let bitcoind_alpha = BitcoinD::download_new().unwrap();
let bitcoind_beta = BitcoinD::download_new().unwrap();
assert_eq!(bitcoind_alpha.get_peer_count().unwrap(), 0);
assert_eq!(bitcoind_beta.get_peer_count().unwrap(), 0);
bitcoind_beta
.add_peer(bitcoind_alpha.get_p2p_socket())
.unwrap();
assert_eq!(bitcoind_alpha.get_peer_count().unwrap(), 1);
assert_eq!(bitcoind_beta.get_peer_count().unwrap(), 1);
}
#[test]
fn test_bitcoind_blocks_propagate() {
let bitcoind_alpha = BitcoinD::download_new().unwrap();
let bitcoind_beta = BitcoinD::download_new().unwrap();
bitcoind_alpha.generate(21).unwrap();
assert_eq!(bitcoind_alpha.get_height().unwrap(), 21);
assert_eq!(bitcoind_beta.get_height().unwrap(), 0);
bitcoind_alpha
.add_peer(bitcoind_beta.get_p2p_socket())
.unwrap();
wait_for_height(&bitcoind_beta, 21).unwrap();
assert_eq!(bitcoind_beta.get_height().unwrap(), 21);
bitcoind_beta.generate(21).unwrap();
wait_for_height(&bitcoind_alpha, 42).unwrap();
assert_eq!(bitcoind_alpha.get_height().unwrap(), 42);
}
}