diff --git a/integration-tests/lib/mod.rs b/integration-tests/lib/mod.rs index f2e08eb2c..62a5a038a 100644 --- a/integration-tests/lib/mod.rs +++ b/integration-tests/lib/mod.rs @@ -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)"; @@ -167,10 +189,23 @@ pub async fn start_pool( pub fn start_template_provider( sv2_interval: Option, 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, + 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) } diff --git a/integration-tests/lib/template_provider.rs b/integration-tests/lib/template_provider.rs index 87702346d..32582b260 100644 --- a/integration-tests/lib/template_provider.rs +++ b/integration-tests/lib/template_provider.rs @@ -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 { @@ -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 @@ -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![ @@ -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); @@ -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"); diff --git a/integration-tests/tests/template_provider_integration.rs b/integration-tests/tests/template_provider_integration.rs index 78615bd2e..a9e179b8f 100644 --- a/integration-tests/tests/template_provider_integration.rs +++ b/integration-tests/tests/template_provider_integration.rs @@ -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( + ¤t_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( + ¤t_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); +}