Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion integration-tests/lib/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,28 @@ macro_rules! shutdown_all {
};
}

/// Polls `get_block_hash` until it returns a different hash than `current_block_hash`.
///
/// Panics if no new block is found within the timeout.
pub async fn wait_for_new_block(
current_block_hash: &str,
get_block_hash: impl Fn() -> String,
timeout_msg: &str,
) {
let timeout = tokio::time::Duration::from_secs(60);
let poll_interval = tokio::time::Duration::from_secs(2);
let start_time = tokio::time::Instant::now();
loop {
tokio::time::sleep(poll_interval).await;
if get_block_hash() != current_block_hash {
return;
}
if start_time.elapsed() > timeout {
panic!("{}", timeout_msg);
}
}
}

const SHARES_PER_MINUTE: f32 = 120.0;

const POOL_COINBASE_REWARD_DESCRIPTOR: &str = "addr(tb1qa0sm0hxzj0x25rh8gw5xlzwlsfvvyz8u96w3p8)";
Expand Down Expand Up @@ -167,10 +189,23 @@ pub async fn start_pool(
pub fn start_template_provider(
sv2_interval: Option<u32>,
difficulty_level: DifficultyLevel,
) -> (TemplateProvider, SocketAddr) {
start_template_provider_with_args(sv2_interval, difficulty_level, vec![])
}

pub fn start_template_provider_with_args(
sv2_interval: Option<u32>,
difficulty_level: DifficultyLevel,
extra_bitcoin_args: Vec<&str>,
) -> (TemplateProvider, SocketAddr) {
let address = get_available_address();
let sv2_interval = sv2_interval.unwrap_or(20);
let template_provider = TemplateProvider::start(address.port(), sv2_interval, difficulty_level);
let template_provider = TemplateProvider::start_with_args(
address.port(),
sv2_interval,
difficulty_level,
extra_bitcoin_args,
);
template_provider.generate_blocks(1);
(template_provider, address)
}
Expand Down
113 changes: 76 additions & 37 deletions integration-tests/lib/template_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use tracing::warn;

use crate::utils::{fs_utils, http, tarball};

const VERSION_SV2_TP: &str = "1.0.3";
const VERSION_SV2_TP: &str = "1.0.6";
const VERSION_BITCOIN_CORE: &str = "30.2";

fn get_sv2_tp_filename(os: &str, arch: &str) -> String {
Expand Down Expand Up @@ -69,6 +69,19 @@ pub struct BitcoinCore {
impl BitcoinCore {
/// Start a new [`BitcoinCore`] instance with IPC enabled.
pub fn start(port: u16, difficulty_level: DifficultyLevel) -> Self {
Self::start_with_args(port, difficulty_level, vec![])
}

/// Start a new [`BitcoinCore`] instance with IPC enabled and extra arguments.
///
/// When `extra_args` is non-empty, `BITCOIN_NODE_BIN` must be set to the custom
/// bitcoin-node binary path. Standard tests (empty extra_args) always use the
/// downloaded binary.
pub fn start_with_args(
port: u16,
difficulty_level: DifficultyLevel,
extra_args: Vec<&str>,
) -> Self {
let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir");
let bin_dir = current_dir.join("template-provider");
// Use temp dir for Bitcoin datadir to avoid long Unix socket paths in CI
Expand Down Expand Up @@ -110,46 +123,61 @@ impl BitcoinCore {
}
}

// Download and setup Bitcoin Core v30.2 with IPC support
let os = env::consts::OS;
let arch = env::consts::ARCH;
let bitcoin_filename = get_bitcoin_core_filename(os, arch);
let bitcoin_home = bin_dir.join(format!("bitcoin-{VERSION_BITCOIN_CORE}"));
let bitcoin_node_bin = bitcoin_home.join("libexec").join("bitcoin-node");
let bitcoin_cli_bin = bitcoin_home.join("bin").join("bitcoin-cli");

if !bitcoin_node_bin.exists() {
let tarball_bytes = match env::var("BITCOIN_CORE_TARBALL_FILE") {
Ok(path) => tarball::read_from_file(&path),
Err(_) => {
warn!("Downloading Bitcoin Core {} for the testing session. This could take a while...", VERSION_BITCOIN_CORE);
let download_endpoint = env::var("BITCOIN_CORE_DOWNLOAD_ENDPOINT")
.unwrap_or_else(|_| {
"https://bitcoincore.org/bin/bitcoin-core-30.2".to_owned()
});
let url = format!("{download_endpoint}/{bitcoin_filename}");
http::make_get_request(&url, 5)
}
};

if let Some(parent) = bitcoin_home.parent() {
create_dir_all(parent).unwrap();
// Use custom bitcoin-node binary if BITCOIN_NODE_BIN is set,
// otherwise download Bitcoin Core v30.2.
// Note: BITCOIN_NODE_BIN is only checked when extra_args are provided,
// so standard tests always use the downloaded binary.
let bitcoin_node_bin = if !extra_args.is_empty() {
match env::var("BITCOIN_NODE_BIN") {
Ok(custom_bin) => PathBuf::from(custom_bin),
Err(_) => panic!(
"BITCOIN_NODE_BIN must be set when extra bitcoin-node args are provided: {:?}",
extra_args
),
}
} else {
let os = env::consts::OS;
let arch = env::consts::ARCH;
let bitcoin_filename = get_bitcoin_core_filename(os, arch);
let bitcoin_home = bin_dir.join(format!("bitcoin-{VERSION_BITCOIN_CORE}"));
let bitcoin_node_bin = bitcoin_home.join("libexec").join("bitcoin-node");
let bitcoin_cli_bin = bitcoin_home.join("bin").join("bitcoin-cli");

if !bitcoin_node_bin.exists() {
let tarball_bytes = match env::var("BITCOIN_CORE_TARBALL_FILE") {
Ok(path) => tarball::read_from_file(&path),
Err(_) => {
warn!("Downloading Bitcoin Core {} for the testing session. This could take a while...", VERSION_BITCOIN_CORE);
let download_endpoint = env::var("BITCOIN_CORE_DOWNLOAD_ENDPOINT")
.unwrap_or_else(|_| {
"https://bitcoincore.org/bin/bitcoin-core-30.2".to_owned()
});
let url = format!("{download_endpoint}/{bitcoin_filename}");
http::make_get_request(&url, 5)
}
};

tarball::unpack(&tarball_bytes, &bin_dir);
if let Some(parent) = bitcoin_home.parent() {
create_dir_all(parent).unwrap();
}

// Sign the binaries on macOS
if os == "macos" {
for bin in &[&bitcoin_node_bin, &bitcoin_cli_bin] {
std::process::Command::new("codesign")
.arg("--sign")
.arg("-")
.arg(bin)
.output()
.expect("Failed to sign Bitcoin Core binary");
tarball::unpack(&tarball_bytes, &bin_dir);

// Sign the binaries on macOS
if os == "macos" {
for bin in &[&bitcoin_node_bin, &bitcoin_cli_bin] {
std::process::Command::new("codesign")
.arg("--sign")
.arg("-")
.arg(bin)
.output()
.expect("Failed to sign Bitcoin Core binary");
}
}
}
}

bitcoin_node_bin
};

// Add IPC and basic args
conf.args.extend(vec![
Expand All @@ -158,6 +186,7 @@ impl BitcoinCore {
"-debug=rpc",
"-logtimemicros=1",
]);
conf.args.extend(extra_args);

// Launch bitcoin-node using corepc-node (which will manage the process for us)
let timeout = std::time::Duration::from_secs(10);
Expand Down Expand Up @@ -278,7 +307,17 @@ pub struct TemplateProvider {
impl TemplateProvider {
/// Start a new [`TemplateProvider`] instance with Bitcoin Core v30.2+ and standalone sv2-tp.
pub fn start(port: u16, sv2_interval: u32, difficulty_level: DifficultyLevel) -> Self {
let bitcoin_core = BitcoinCore::start(port, difficulty_level);
Self::start_with_args(port, sv2_interval, difficulty_level, vec![])
}

/// Start with extra arguments passed to bitcoin-node.
pub fn start_with_args(
port: u16,
sv2_interval: u32,
difficulty_level: DifficultyLevel,
extra_bitcoin_args: Vec<&str>,
) -> Self {
let bitcoin_core = BitcoinCore::start_with_args(port, difficulty_level, extra_bitcoin_args);

let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir");
let bin_dir = current_dir.join("template-provider");
Expand Down
73 changes: 73 additions & 0 deletions integration-tests/tests/template_provider_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,76 @@ async fn tp_high_diff() {
assert_eq!(blockchain_info.difficulty, 77761.1123986095);
assert_eq!(blockchain_info.chain, "signet");
}

// This test verifies that coinbase outputs survive the full round-trip through the mining stack.
// A mempool transaction triggers a witness commitment output, which must be preserved through
// TP -> Pool -> Translator -> Minerd and back to Bitcoin Core via block submission.
#[tokio::test]
async fn tp_coinbase_outputs_round_trip() {
start_tracing();
let sv2_interval = Some(5);
let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low);
tp.fund_wallet().unwrap();
let current_block_hash = tp.get_best_block_hash().unwrap();

let (pool, pool_addr) = start_pool(sv2_tp_config(tp_addr), vec![], vec![]).await;

// Create a mempool transaction to trigger witness commitment output
tp.create_mempool_transaction().unwrap();

let (translator, tproxy_addr) =
start_sv2_translator(&[pool_addr], false, vec![], vec![], None).await;
let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await;

wait_for_new_block(
&current_block_hash,
|| tp.get_best_block_hash().unwrap(),
"Block should have been mined and accepted within 60 seconds, \
confirming coinbase outputs survived the round-trip",
)
.await;
shutdown_all!(pool, translator);
}

// This test verifies that multiple coinbase outputs survive the full round-trip.
// Requires a custom Bitcoin Core build with -testmulticoinbase support, which adds extra
// OP_RETURN outputs to the coinbase. Set BITCOIN_NODE_BIN to the custom binary path.
#[tokio::test]
async fn tp_multiple_coinbase_outputs_round_trip() {
start_tracing();

if std::env::var("BITCOIN_NODE_BIN").is_err() {
eprintln!(
"Skipping tp_multiple_coinbase_outputs_round_trip: \
BITCOIN_NODE_BIN not set (requires custom Bitcoin Core with -testmulticoinbase)"
);
return;
}

let sv2_interval = Some(5);
let (tp, tp_addr) = start_template_provider_with_args(
sv2_interval,
DifficultyLevel::Low,
vec!["-testmulticoinbase"],
);
tp.fund_wallet().unwrap();
let current_block_hash = tp.get_best_block_hash().unwrap();

let (pool, pool_addr) = start_pool(sv2_tp_config(tp_addr), vec![], vec![]).await;

// Create a mempool transaction to add a witness commitment OP_RETURN
tp.create_mempool_transaction().unwrap();

let (translator, tproxy_addr) =
start_sv2_translator(&[pool_addr], false, vec![], vec![], None).await;
let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await;

wait_for_new_block(
&current_block_hash,
|| tp.get_best_block_hash().unwrap(),
"Block should have been mined and accepted within 60 seconds, \
confirming all coinbase outputs survived the round-trip",
)
.await;
shutdown_all!(pool, translator);
}
Loading