Skip to content

Commit d0fb197

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

3 files changed

Lines changed: 185 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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,108 @@ async fn tp_high_diff() {
2323
assert_eq!(blockchain_info.difficulty, 77761.1123986095);
2424
assert_eq!(blockchain_info.chain, "signet");
2525
}
26+
27+
// Verifies coinbase outputs survive the full round-trip: TP -> Pool -> Translator -> Minerd.
28+
// A SegWit mempool transaction triggers Bitcoin Core to add a witness commitment (OP_RETURN)
29+
// to the coinbase. sv2-tp serializes this as coinbase_tx_outputs in NewTemplate. The pool
30+
// incorporates it into the coinbase, and the miner submits a solution back to Bitcoin Core.
31+
// If any output was dropped during serialization, Bitcoin Core rejects the block.
32+
//
33+
// This catches the sv2-tp < v1.0.4 bug where only .at(0) was serialized (sv2-tp PR #55).
34+
#[tokio::test]
35+
async fn tp_coinbase_outputs_round_trip() {
36+
start_tracing();
37+
let sv2_interval = Some(5);
38+
let (tp, tp_addr) = start_template_provider(sv2_interval, DifficultyLevel::Low);
39+
tp.fund_wallet().unwrap();
40+
let current_block_hash = tp.get_best_block_hash().unwrap();
41+
42+
let (pool, pool_addr) = start_pool(sv2_tp_config(tp_addr), vec![], vec![]).await;
43+
44+
// Create a SegWit mempool transaction to trigger witness commitment output
45+
tp.create_mempool_transaction().unwrap();
46+
47+
let (translator, tproxy_addr) =
48+
start_sv2_translator(&[pool_addr], false, vec![], vec![], None).await;
49+
let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await;
50+
51+
// Poll until a block is mined and accepted by Bitcoin Core.
52+
// If coinbase outputs were serialized incorrectly, the block is rejected.
53+
let timeout = tokio::time::Duration::from_secs(60);
54+
let poll_interval = tokio::time::Duration::from_secs(2);
55+
let start_time = tokio::time::Instant::now();
56+
loop {
57+
tokio::time::sleep(poll_interval).await;
58+
let new_block_hash = tp.get_best_block_hash().unwrap();
59+
if new_block_hash != current_block_hash {
60+
shutdown_all!(pool, translator);
61+
return;
62+
}
63+
if start_time.elapsed() > timeout {
64+
panic!(
65+
"Block should have been mined and accepted within {} seconds, \
66+
confirming coinbase outputs survived the round-trip",
67+
timeout.as_secs()
68+
);
69+
}
70+
}
71+
}
72+
73+
// Verifies multiple coinbase outputs survive the full round-trip.
74+
// Requires a custom Bitcoin Core build with -testmulticoinbase support.
75+
// Set BITCOIN_NODE_BIN to the custom bitcoin-node binary path.
76+
//
77+
// With -testmulticoinbase, Bitcoin Core adds 2 extra OP_RETURN outputs to the coinbase.
78+
// Combined with the witness commitment, we get 3 OP_RETURN outputs total. The old .at(0)
79+
// bug would drop all but the first, causing Bitcoin Core to reject the block.
80+
#[tokio::test]
81+
async fn tp_multiple_coinbase_outputs_round_trip() {
82+
start_tracing();
83+
84+
if std::env::var("BITCOIN_NODE_BIN").is_err() {
85+
eprintln!(
86+
"Skipping tp_multiple_coinbase_outputs_round_trip: \
87+
BITCOIN_NODE_BIN not set (requires custom Bitcoin Core with -testmulticoinbase)"
88+
);
89+
return;
90+
}
91+
92+
let sv2_interval = Some(5);
93+
let (tp, tp_addr) = start_template_provider_with_args(
94+
sv2_interval,
95+
DifficultyLevel::Low,
96+
vec!["-testmulticoinbase"],
97+
);
98+
tp.fund_wallet().unwrap();
99+
let current_block_hash = tp.get_best_block_hash().unwrap();
100+
101+
let (pool, pool_addr) = start_pool(sv2_tp_config(tp_addr), vec![], vec![]).await;
102+
103+
// Create a SegWit mempool transaction (adds witness commitment as another OP_RETURN)
104+
tp.create_mempool_transaction().unwrap();
105+
106+
let (translator, tproxy_addr) =
107+
start_sv2_translator(&[pool_addr], false, vec![], vec![], None).await;
108+
let (_minerd, _) = start_minerd(tproxy_addr, None, None, false).await;
109+
110+
// Poll until a block is mined and accepted by Bitcoin Core.
111+
// With 3 OP_RETURN outputs, the old .at(0) bug would cause block rejection.
112+
let timeout = tokio::time::Duration::from_secs(60);
113+
let poll_interval = tokio::time::Duration::from_secs(2);
114+
let start_time = tokio::time::Instant::now();
115+
loop {
116+
tokio::time::sleep(poll_interval).await;
117+
let new_block_hash = tp.get_best_block_hash().unwrap();
118+
if new_block_hash != current_block_hash {
119+
shutdown_all!(pool, translator);
120+
return;
121+
}
122+
if start_time.elapsed() > timeout {
123+
panic!(
124+
"Block should have been mined and accepted within {} seconds, \
125+
confirming all coinbase outputs survived the round-trip",
126+
timeout.as_secs()
127+
);
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)