Skip to content

Commit 1706f1c

Browse files
committed
test multiple coinbase outputs round-trip
Closes #58
1 parent eb2fbf5 commit 1706f1c

3 files changed

Lines changed: 175 additions & 37 deletions

File tree

integration-tests/lib/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,23 @@ pub async fn start_pool(
167167
pub fn start_template_provider(
168168
sv2_interval: Option<u32>,
169169
difficulty_level: DifficultyLevel,
170+
) -> (TemplateProvider, SocketAddr) {
171+
start_template_provider_with_args(sv2_interval, difficulty_level, vec![])
172+
}
173+
174+
pub fn start_template_provider_with_args(
175+
sv2_interval: Option<u32>,
176+
difficulty_level: DifficultyLevel,
177+
extra_bitcoin_args: Vec<&str>,
170178
) -> (TemplateProvider, SocketAddr) {
171179
let address = get_available_address();
172180
let sv2_interval = sv2_interval.unwrap_or(20);
173-
let template_provider = TemplateProvider::start(address.port(), sv2_interval, difficulty_level);
181+
let template_provider = TemplateProvider::start_with_args(
182+
address.port(),
183+
sv2_interval,
184+
difficulty_level,
185+
extra_bitcoin_args,
186+
);
174187
template_provider.generate_blocks(1);
175188
(template_provider, address)
176189
}

integration-tests/lib/template_provider.rs

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,18 @@ pub struct BitcoinCore {
6969
impl BitcoinCore {
7070
/// Start a new [`BitcoinCore`] instance with IPC enabled.
7171
pub fn start(port: u16, difficulty_level: DifficultyLevel) -> Self {
72+
Self::start_with_args(port, difficulty_level, vec![])
73+
}
74+
75+
/// Start a new [`BitcoinCore`] instance with IPC enabled and extra arguments.
76+
///
77+
/// Set `BITCOIN_NODE_BIN` to override the bitcoin-node binary path
78+
/// (e.g. a custom build with `-testmulticoinbase` support).
79+
pub fn start_with_args(
80+
port: u16,
81+
difficulty_level: DifficultyLevel,
82+
extra_args: Vec<&str>,
83+
) -> Self {
7284
let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir");
7385
let bin_dir = current_dir.join("template-provider");
7486
// Use temp dir for Bitcoin datadir to avoid long Unix socket paths in CI
@@ -110,46 +122,53 @@ impl BitcoinCore {
110122
}
111123
}
112124

113-
// Download and setup Bitcoin Core v30.2 with IPC support
114-
let os = env::consts::OS;
115-
let arch = env::consts::ARCH;
116-
let bitcoin_filename = get_bitcoin_core_filename(os, arch);
117-
let bitcoin_home = bin_dir.join(format!("bitcoin-{VERSION_BITCOIN_CORE}"));
118-
let bitcoin_node_bin = bitcoin_home.join("libexec").join("bitcoin-node");
119-
let bitcoin_cli_bin = bitcoin_home.join("bin").join("bitcoin-cli");
125+
// Use custom bitcoin-node binary if BITCOIN_NODE_BIN is set,
126+
// otherwise download Bitcoin Core v30.2.
127+
let bitcoin_node_bin = if let Ok(custom_bin) = env::var("BITCOIN_NODE_BIN") {
128+
PathBuf::from(custom_bin)
129+
} else {
130+
let os = env::consts::OS;
131+
let arch = env::consts::ARCH;
132+
let bitcoin_filename = get_bitcoin_core_filename(os, arch);
133+
let bitcoin_home = bin_dir.join(format!("bitcoin-{VERSION_BITCOIN_CORE}"));
134+
let bitcoin_node_bin = bitcoin_home.join("libexec").join("bitcoin-node");
135+
let bitcoin_cli_bin = bitcoin_home.join("bin").join("bitcoin-cli");
136+
137+
if !bitcoin_node_bin.exists() {
138+
let tarball_bytes = match env::var("BITCOIN_CORE_TARBALL_FILE") {
139+
Ok(path) => tarball::read_from_file(&path),
140+
Err(_) => {
141+
warn!("Downloading Bitcoin Core {} for the testing session. This could take a while...", VERSION_BITCOIN_CORE);
142+
let download_endpoint = env::var("BITCOIN_CORE_DOWNLOAD_ENDPOINT")
143+
.unwrap_or_else(|_| {
144+
"https://bitcoincore.org/bin/bitcoin-core-30.2".to_owned()
145+
});
146+
let url = format!("{download_endpoint}/{bitcoin_filename}");
147+
http::make_get_request(&url, 5)
148+
}
149+
};
120150

121-
if !bitcoin_node_bin.exists() {
122-
let tarball_bytes = match env::var("BITCOIN_CORE_TARBALL_FILE") {
123-
Ok(path) => tarball::read_from_file(&path),
124-
Err(_) => {
125-
warn!("Downloading Bitcoin Core {} for the testing session. This could take a while...", VERSION_BITCOIN_CORE);
126-
let download_endpoint = env::var("BITCOIN_CORE_DOWNLOAD_ENDPOINT")
127-
.unwrap_or_else(|_| {
128-
"https://bitcoincore.org/bin/bitcoin-core-30.2".to_owned()
129-
});
130-
let url = format!("{download_endpoint}/{bitcoin_filename}");
131-
http::make_get_request(&url, 5)
151+
if let Some(parent) = bitcoin_home.parent() {
152+
create_dir_all(parent).unwrap();
132153
}
133-
};
134154

135-
if let Some(parent) = bitcoin_home.parent() {
136-
create_dir_all(parent).unwrap();
137-
}
138-
139-
tarball::unpack(&tarball_bytes, &bin_dir);
140-
141-
// Sign the binaries on macOS
142-
if os == "macos" {
143-
for bin in &[&bitcoin_node_bin, &bitcoin_cli_bin] {
144-
std::process::Command::new("codesign")
145-
.arg("--sign")
146-
.arg("-")
147-
.arg(bin)
148-
.output()
149-
.expect("Failed to sign Bitcoin Core binary");
155+
tarball::unpack(&tarball_bytes, &bin_dir);
156+
157+
// Sign the binaries on macOS
158+
if os == "macos" {
159+
for bin in &[&bitcoin_node_bin, &bitcoin_cli_bin] {
160+
std::process::Command::new("codesign")
161+
.arg("--sign")
162+
.arg("-")
163+
.arg(bin)
164+
.output()
165+
.expect("Failed to sign Bitcoin Core binary");
166+
}
150167
}
151168
}
152-
}
169+
170+
bitcoin_node_bin
171+
};
153172

154173
// Add IPC and basic args
155174
conf.args.extend(vec![
@@ -158,6 +177,7 @@ impl BitcoinCore {
158177
"-debug=rpc",
159178
"-logtimemicros=1",
160179
]);
180+
conf.args.extend(extra_args);
161181

162182
// Launch bitcoin-node using corepc-node (which will manage the process for us)
163183
let timeout = std::time::Duration::from_secs(10);
@@ -278,7 +298,17 @@ pub struct TemplateProvider {
278298
impl TemplateProvider {
279299
/// Start a new [`TemplateProvider`] instance with Bitcoin Core v30.2+ and standalone sv2-tp.
280300
pub fn start(port: u16, sv2_interval: u32, difficulty_level: DifficultyLevel) -> Self {
281-
let bitcoin_core = BitcoinCore::start(port, difficulty_level);
301+
Self::start_with_args(port, sv2_interval, difficulty_level, vec![])
302+
}
303+
304+
/// Start with extra arguments passed to bitcoin-node.
305+
pub fn start_with_args(
306+
port: u16,
307+
sv2_interval: u32,
308+
difficulty_level: DifficultyLevel,
309+
extra_bitcoin_args: Vec<&str>,
310+
) -> Self {
311+
let bitcoin_core = BitcoinCore::start_with_args(port, difficulty_level, extra_bitcoin_args);
282312

283313
let current_dir: PathBuf = std::env::current_dir().expect("failed to read current dir");
284314
let bin_dir = current_dir.join("template-provider");

integration-tests/tests/template_provider_integration.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,98 @@ async fn tp_high_diff() {
2323
assert_eq!(blockchain_info.difficulty, 77761.1123986095);
2424
assert_eq!(blockchain_info.chain, "signet");
2525
}
26+
27+
// This test verifies that coinbase outputs survive the full round-trip through the mining stack.
28+
// A mempool transaction triggers a witness commitment output, which must be preserved through
29+
// TP -> Pool -> Translator -> Minerd and back to Bitcoin Core via block submission.
30+
#[tokio::test]
31+
async fn tp_coinbase_outputs_round_trip() {
32+
start_tracing();
33+
let sv2_interval = Some(5);
34+
let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low);
35+
tp.fund_wallet().unwrap();
36+
let current_block_hash = tp.get_best_block_hash().unwrap();
37+
38+
let (pool, pool_addr) = start_pool(sv2_tp_config(tp_addr), vec![], vec![]).await;
39+
40+
// Create a mempool transaction to trigger witness commitment output
41+
tp.create_mempool_transaction().unwrap();
42+
43+
let (translator, tproxy_addr) =
44+
start_sv2_translator(&[pool_addr], false, vec![], vec![], None).await;
45+
let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await;
46+
47+
// Poll until a block is mined and accepted by Bitcoin Core
48+
let timeout = tokio::time::Duration::from_secs(60);
49+
let poll_interval = tokio::time::Duration::from_secs(2);
50+
let start_time = tokio::time::Instant::now();
51+
loop {
52+
tokio::time::sleep(poll_interval).await;
53+
let new_block_hash = tp.get_best_block_hash().unwrap();
54+
if new_block_hash != current_block_hash {
55+
shutdown_all!(pool, translator);
56+
return;
57+
}
58+
if start_time.elapsed() > timeout {
59+
panic!(
60+
"Block should have been mined and accepted within {} seconds, \
61+
confirming coinbase outputs survived the round-trip",
62+
timeout.as_secs()
63+
);
64+
}
65+
}
66+
}
67+
68+
// This test verifies that multiple coinbase outputs survive the full round-trip.
69+
// Requires a custom Bitcoin Core build with -testmulticoinbase support, which adds extra
70+
// OP_RETURN outputs to the coinbase. Set BITCOIN_NODE_BIN to the custom binary path.
71+
#[tokio::test]
72+
async fn tp_multiple_coinbase_outputs_round_trip() {
73+
start_tracing();
74+
75+
if std::env::var("BITCOIN_NODE_BIN").is_err() {
76+
eprintln!(
77+
"Skipping tp_multiple_coinbase_outputs_round_trip: \
78+
BITCOIN_NODE_BIN not set (requires custom Bitcoin Core with -testmulticoinbase)"
79+
);
80+
return;
81+
}
82+
83+
let sv2_interval = Some(5);
84+
let (tp, tp_addr) = start_template_provider_with_args(
85+
sv2_interval,
86+
DifficultyLevel::Low,
87+
vec!["-testmulticoinbase"],
88+
);
89+
tp.fund_wallet().unwrap();
90+
let current_block_hash = tp.get_best_block_hash().unwrap();
91+
92+
let (pool, pool_addr) = start_pool(sv2_tp_config(tp_addr), vec![], vec![]).await;
93+
94+
// Create a mempool transaction to add a witness commitment OP_RETURN
95+
tp.create_mempool_transaction().unwrap();
96+
97+
let (translator, tproxy_addr) =
98+
start_sv2_translator(&[pool_addr], false, vec![], vec![], None).await;
99+
let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await;
100+
101+
// Poll until a block is mined and accepted by Bitcoin Core
102+
let timeout = tokio::time::Duration::from_secs(60);
103+
let poll_interval = tokio::time::Duration::from_secs(2);
104+
let start_time = tokio::time::Instant::now();
105+
loop {
106+
tokio::time::sleep(poll_interval).await;
107+
let new_block_hash = tp.get_best_block_hash().unwrap();
108+
if new_block_hash != current_block_hash {
109+
shutdown_all!(pool, translator);
110+
return;
111+
}
112+
if start_time.elapsed() > timeout {
113+
panic!(
114+
"Block should have been mined and accepted within {} seconds, \
115+
confirming all coinbase outputs survived the round-trip",
116+
timeout.as_secs()
117+
);
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)