Skip to content

refactor: BitcoinCoreController rcp rollout + housekeeping #6385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Aug 22, 2025
Merged
1 change: 0 additions & 1 deletion .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ jobs:
exclude:
# The following tests are excluded from CI runs. Some of these may be
# worth investigating adding back into the CI
- test-name: tests::bitcoin_regtest::bitcoind_integration_test_segwit
- test-name: tests::nakamoto_integrations::consensus_hash_event_dispatcher
- test-name: tests::neon_integrations::atlas_integration_test
- test-name: tests::neon_integrations::atlas_stress_integration_test
Expand Down
190 changes: 190 additions & 0 deletions stacks-node/src/burnchains/bitcoin/core_controller.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright (C) 2025 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

//! Bitcoin Core module
//!
//! This module provides convenient APIs for managing a `bitcoind` process,
//! including utilities to quickly start and stop instances for testing or
//! development purposes.

use std::io::{BufRead, BufReader};
use std::process::{Child, Command, Stdio};

use crate::burnchains::rpc::bitcoin_rpc_client::BitcoinRpcClient;
use crate::Config;

// Value usable as `BurnchainConfig::peer_port` to avoid bitcoind peer port binding
pub const BURNCHAIN_CONFIG_PEER_PORT_DISABLED: u16 = 0;

/// Errors that can occur when managing a `bitcoind` process.
#[derive(Debug, thiserror::Error)]
pub enum BitcoinCoreError {
/// Returned when the `bitcoind` process fails to start.
#[error("bitcoind spawn failed: {0}")]
SpawnFailed(String),
/// Returned when an attempt to stop the `bitcoind` process fails.
#[error("bitcoind stop failed: {0}")]
StopFailed(String),
/// Returned when an attempt to forcibly kill the `bitcoind` process fails.
#[error("bitcoind kill failed: {0}")]
KillFailed(String),
}

type BitcoinResult<T> = Result<T, BitcoinCoreError>;

/// Represents a managed `bitcoind` process instance.
pub struct BitcoinCoreController {
/// Handle to the spawned `bitcoind` process.
bitcoind_process: Option<Child>,
/// Command-line arguments used to launch the process.
args: Vec<String>,
/// Path to the data directory used by `bitcoind`.
data_path: String,
/// RPC client for communicating with the `bitcoind` instance.
rpc_client: BitcoinRpcClient,
}

impl BitcoinCoreController {
/// Create a [`BitcoinCoreController`] from Stacks Configuration
pub fn from_stx_config(config: &Config) -> Self {
let client =
BitcoinRpcClient::from_stx_config(config).expect("rpc client creation failed!");
Self::from_stx_config_and_client(config, client)
}

/// Create a [`BitcoinCoreController`] from Stacks Configuration (mainly using [`stacks::config::BurnchainConfig`])
/// and an rpc client [`BitcoinRpcClient`]
pub fn from_stx_config_and_client(config: &Config, client: BitcoinRpcClient) -> Self {
let mut result = BitcoinCoreController {
bitcoind_process: None,
args: vec![],
data_path: config.get_burnchain_path_str(),
rpc_client: client,
};

result.add_arg("-regtest");
result.add_arg("-nodebug");
result.add_arg("-nodebuglogfile");
result.add_arg("-rest");
result.add_arg("-persistmempool=1");
result.add_arg("-dbcache=100");
result.add_arg("-txindex=1");
result.add_arg("-server=1");
result.add_arg("-listenonion=0");
result.add_arg("-rpcbind=127.0.0.1");
result.add_arg(format!("-datadir={}", result.data_path));

let peer_port = config.burnchain.peer_port;
if peer_port == BURNCHAIN_CONFIG_PEER_PORT_DISABLED {
info!("Peer Port is disabled. So `-listen=0` flag will be used");
result.add_arg("-listen=0");
} else {
result.add_arg(format!("-port={peer_port}"));
}

result.add_arg(format!("-rpcport={}", config.burnchain.rpc_port));

if let (Some(username), Some(password)) =
(&config.burnchain.username, &config.burnchain.password)
{
result.add_arg(format!("-rpcuser={username}"));
result.add_arg(format!("-rpcpassword={password}"));
}

result
}

/// Add argument (like "-name=value") to be used to run bitcoind process
pub fn add_arg(&mut self, arg: impl Into<String>) -> &mut Self {
self.args.push(arg.into());
self
}

/// Start Bitcoind process
pub fn start_bitcoind(&mut self) -> BitcoinResult<()> {
std::fs::create_dir_all(&self.data_path).unwrap();

let mut command = Command::new("bitcoind");
command.stdout(Stdio::piped());

command.args(self.args.clone());

info!("bitcoind spawn: {command:?}");

let mut process = match command.spawn() {
Ok(child) => child,
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{e:?}"))),
};

let mut out_reader = BufReader::new(process.stdout.take().unwrap());

let mut line = String::new();
while let Ok(bytes_read) = out_reader.read_line(&mut line) {
if bytes_read == 0 {
return Err(BitcoinCoreError::SpawnFailed(
"Bitcoind closed before spawning network".into(),
));
}
if line.contains("Done loading") {
break;
}
}

info!("bitcoind startup finished");

self.bitcoind_process = Some(process);

Ok(())
}

/// Gracefully stop bitcoind process
pub fn stop_bitcoind(&mut self) -> BitcoinResult<()> {
if let Some(mut bitcoind_process) = self.bitcoind_process.take() {
let res = self
.rpc_client
.stop()
.map_err(|e| BitcoinCoreError::StopFailed(format!("{e:?}")))?;
info!("bitcoind stop started with message: '{res}'");
bitcoind_process
.wait()
.map_err(|e| BitcoinCoreError::StopFailed(format!("{e:?}")))?;
info!("bitcoind stop finished");
}
Ok(())
}

/// Kill bitcoind process
pub fn kill_bitcoind(&mut self) -> BitcoinResult<()> {
if let Some(mut bitcoind_process) = self.bitcoind_process.take() {
info!("bitcoind kill started");
bitcoind_process
.kill()
.map_err(|e| BitcoinCoreError::KillFailed(format!("{e:?}")))?;
info!("bitcoind kill finished");
}
Ok(())
}

/// Check if bitcoind process is running
pub fn is_running(&self) -> bool {
self.bitcoind_process.is_some()
}
}

impl Drop for BitcoinCoreController {
fn drop(&mut self) {
self.kill_bitcoind().unwrap();
}
}
21 changes: 21 additions & 0 deletions stacks-node/src/burnchains/bitcoin/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (C) 2025 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

//! Bitcoin Module
//!
//! Entry point for all bitcoin related modules

#[cfg(test)]
pub mod core_controller;
Loading
Loading