Skip to content

Commit d263ae4

Browse files
committed
ci,docs: prepare wallet-capable integration test environment
1 parent abc4cc9 commit d263ae4

File tree

5 files changed

+218
-19
lines changed

5 files changed

+218
-19
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ jobs:
2929
- name: Build Bitcoin Core
3030
run: |
3131
cd bitcoin
32-
cmake -B build -DENABLE_WALLET=OFF -DBUILD_TESTS=OFF -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
32+
cmake -B build -DENABLE_WALLET=ON -DBUILD_TESTS=OFF -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
3333
cmake --build build -j $(nproc)
3434
- name: Run Bitcoin Core Daemon
3535
run: cd bitcoin && ./build/bin/bitcoin node -chain=regtest -ipcbind=unix -server -debug=ipc -daemon
36-
- name: Generate Blocks
37-
run: cd bitcoin && ./build/bin/bitcoin rpc -regtest -rpcwait generatetodescriptor 101 "raw(51)"
3836
- name: Run Test Suite
37+
env:
38+
BITCOIN_BIN: ${{ github.workspace }}/bitcoin/build/bin/bitcoin
3939
run: cargo test

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ capnp = "0.25.0"
1818
capnpc = "0.25.0"
1919

2020
[dev-dependencies]
21+
bitcoin-primitives = { git = "https://github.com/rust-bitcoin/rust-bitcoin", package = "bitcoin-primitives", tag = "bitcoin-0.33.0-beta" }
2122
capnp-rpc = "0.25.0"
23+
encoding = { git = "https://github.com/rust-bitcoin/rust-bitcoin", package = "bitcoin-consensus-encoding", tag = "bitcoin-0.33.0-beta" }
2224
futures = "0.3.0"
2325
serial_test = "3"
2426
tokio = { version = "1", features = ["rt-multi-thread", "net", "macros", "io-util", "time"] }

README.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The integration tests connect to a running bitcoin node via IPC.
2020

2121
```sh
2222
cd /path/to/bitcoin
23-
cmake -B build -DENABLE_WALLET=OFF -DBUILD_TESTS=OFF
23+
cmake -B build -DENABLE_WALLET=ON -DBUILD_TESTS=OFF
2424
cmake --build build -j$(nproc)
2525
```
2626

@@ -30,23 +30,20 @@ cmake --build build -j$(nproc)
3030
./build/bin/bitcoin node -chain=regtest -ipcbind=unix -server -debug=ipc -daemon
3131
```
3232

33-
### 3. Generate blocks
33+
### 3. Run tests
3434

35-
The mining tests require chain height > 16. At height <= 16, `createNewBlock`
36-
fails with `bad-cb-length` because the BIP34 height push is too short for the
37-
coinbase scriptSig minimum.
35+
If `bitcoin` is not in your `PATH`, set `BITCOIN_BIN` to the full path of
36+
the Bitcoin Core binary.
3837

39-
```sh
40-
./build/bin/bitcoin rpc -chain=regtest -rpcwait generatetodescriptor 101 "raw(51)"
41-
```
42-
43-
### 4. Run tests
38+
The test harness bootstraps regtest chain state and ensures the test wallet is
39+
available before running integration tests.
4440

4541
```sh
42+
BITCOIN_BIN=./build/bin/bitcoin \
4643
cargo test
4744
```
4845

49-
### 5. Stop bitcoin
46+
### 4. Stop bitcoin
5047

5148
```sh
5249
./build/bin/bitcoin rpc -chain=regtest stop

tests/test.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use tokio::task::LocalSet;
66
mod bitcoin_core_util;
77

88
use bitcoin_core_util::{
9-
bootstrap, connect_unix_stream, destroy_template, make_block_template, make_mining,
9+
bitcoin_test_wallet, bootstrap, connect_unix_stream, create_mempool_self_transfer,
10+
destroy_template, ensure_wallet_loaded_and_funded, make_block_template, make_mining,
1011
unix_socket_path,
1112
};
1213

@@ -321,3 +322,29 @@ async fn mining_check_block_and_interrupt() {
321322
})
322323
.await;
323324
}
325+
326+
/// Minimal coverage for wallet/mempool helpers added for future mempool tests.
327+
#[tokio::test]
328+
#[serial_test::serial]
329+
async fn wallet_helpers_create_mempool_transaction() {
330+
let wallet = bitcoin_test_wallet();
331+
assert!(!wallet.is_empty(), "test wallet name must not be empty");
332+
333+
ensure_wallet_loaded_and_funded(&wallet);
334+
let (txid, wtxid, raw_tx) = create_mempool_self_transfer(&wallet);
335+
336+
assert_eq!(txid.len(), 32, "txid must be 32 bytes");
337+
assert_eq!(wtxid.len(), 32, "wtxid must be 32 bytes");
338+
assert!(
339+
txid.iter().any(|byte| *byte != 0),
340+
"txid should not be the all-zero hash"
341+
);
342+
assert!(
343+
wtxid.iter().any(|byte| *byte != 0),
344+
"wtxid should not be the all-zero hash"
345+
);
346+
assert!(
347+
!raw_tx.is_empty(),
348+
"raw transaction bytes must not be empty"
349+
);
350+
}

tests/util/bitcoin_core.rs

Lines changed: 177 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
use std::path::{Path, PathBuf};
1+
use std::{
2+
path::{Path, PathBuf},
3+
process::Command,
4+
sync::Once,
5+
};
26

37
use bitcoin_capnp_types::{
48
init_capnp::init,
59
mining_capnp::{block_template, mining},
610
proxy_capnp::{thread, thread_map},
711
};
12+
use bitcoin_primitives::Transaction;
813
use capnp_rpc::{RpcSystem, rpc_twoparty_capnp::Side, twoparty::VatNetwork};
14+
use encoding::decode_from_slice;
915
use futures::io::BufReader;
1016
use tokio::net::{UnixStream, unix::OwnedReadHalf};
1117
use tokio_util::compat::{Compat, TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
1218

19+
static CHAIN_SETUP: Once = Once::new();
20+
1321
pub fn unix_socket_path() -> PathBuf {
1422
let home_dir_string = std::env::var("HOME").unwrap();
1523
let home_dir = home_dir_string.parse::<PathBuf>().unwrap();
@@ -25,6 +33,170 @@ pub fn unix_socket_path() -> PathBuf {
2533
regtest_dir.join("node.sock")
2634
}
2735

36+
fn bitcoin_bin() -> String {
37+
std::env::var("BITCOIN_BIN").unwrap_or_else(|_| "bitcoin".to_owned())
38+
}
39+
40+
pub fn bitcoin_test_wallet() -> String {
41+
std::env::var("BITCOIN_TEST_WALLET").unwrap_or_else(|_| "ipc-test".to_owned())
42+
}
43+
44+
fn bitcoin_rpc(wallet: Option<&str>, args: &[&str]) -> Result<String, String> {
45+
let owned_args: Vec<String> = args.iter().map(|arg| (*arg).to_owned()).collect();
46+
bitcoin_rpc_owned(wallet, &owned_args)
47+
}
48+
49+
fn bitcoin_rpc_owned(wallet: Option<&str>, args: &[String]) -> Result<String, String> {
50+
let mut command = Command::new(bitcoin_bin());
51+
command.arg("rpc").arg("-regtest").arg("-rpcwait");
52+
if let Some(wallet) = wallet {
53+
command.arg(format!("-rpcwallet={wallet}"));
54+
}
55+
command.args(args);
56+
57+
let output = command
58+
.output()
59+
.map_err(|e| format!("failed to execute bitcoin rpc command: {e}"))?;
60+
if output.status.success() {
61+
Ok(String::from_utf8(output.stdout)
62+
.unwrap_or_else(|_| String::new())
63+
.trim()
64+
.to_owned())
65+
} else {
66+
Err(format!(
67+
"bitcoin rpc command failed: {}",
68+
String::from_utf8_lossy(&output.stderr).trim()
69+
))
70+
}
71+
}
72+
73+
fn ensure_wallet_loaded(wallet: &str) {
74+
if bitcoin_rpc(Some(wallet), &["getwalletinfo"]).is_err() {
75+
// First try loading an existing wallet from disk (common when regtest data
76+
// directory is re-used), then fall back to creating it.
77+
if bitcoin_rpc(None, &["loadwallet", wallet]).is_err() {
78+
let _ = bitcoin_rpc(None, &["createwallet", wallet]);
79+
}
80+
81+
bitcoin_rpc(Some(wallet), &["getwalletinfo"]).unwrap_or_else(|e| {
82+
panic!("wallet {wallet} is not available after load/create attempts: {e}")
83+
});
84+
}
85+
}
86+
87+
fn ensure_bootstrap_chain_ready() {
88+
// `call_once` serializes bootstrap initialization across all tests in this
89+
// process. Other callers block until this setup completes.
90+
CHAIN_SETUP.call_once(|| {
91+
let wallet = bitcoin_test_wallet();
92+
ensure_chain_height_at_least(101, &wallet);
93+
});
94+
}
95+
96+
fn ensure_chain_height_at_least(min_height: u32, wallet: &str) {
97+
ensure_wallet_loaded(wallet);
98+
let height: u32 = bitcoin_rpc(None, &["getblockcount"])
99+
.unwrap_or_else(|e| panic!("failed to query block height: {e}"))
100+
.parse()
101+
.unwrap_or_else(|e| panic!("failed to parse block height: {e}"));
102+
if height < min_height {
103+
let blocks_to_generate = (min_height - height).to_string();
104+
let address = bitcoin_rpc(Some(wallet), &["getnewaddress"])
105+
.unwrap_or_else(|e| panic!("failed to get new address from {wallet}: {e}"));
106+
bitcoin_rpc(
107+
Some(wallet),
108+
&[
109+
"generatetoaddress",
110+
blocks_to_generate.as_str(),
111+
address.as_str(),
112+
],
113+
)
114+
.unwrap_or_else(|e| {
115+
panic!("failed to mine {blocks_to_generate} blocks to reach height {min_height}: {e}")
116+
});
117+
}
118+
}
119+
120+
pub fn ensure_wallet_loaded_and_funded(wallet: &str) {
121+
ensure_wallet_loaded(wallet);
122+
123+
// getbalance "*" 1 only counts confirmed spendable funds.
124+
let balance: f64 = bitcoin_rpc(Some(wallet), &["getbalance", "*", "1"])
125+
.unwrap_or_else(|e| panic!("failed to query wallet balance for {wallet}: {e}"))
126+
.parse()
127+
.unwrap_or_else(|e| panic!("failed to parse wallet balance for {wallet}: {e}"));
128+
129+
if balance < 1.0 {
130+
let address = bitcoin_rpc(Some(wallet), &["getnewaddress"])
131+
.unwrap_or_else(|e| panic!("failed to get new address from {wallet}: {e}"));
132+
// Mining a single block can mature older coinbase outputs when balance is low.
133+
bitcoin_rpc(Some(wallet), &["generatetoaddress", "1", address.as_str()])
134+
.unwrap_or_else(|e| panic!("failed to mine blocks to wallet {wallet}: {e}"));
135+
}
136+
}
137+
138+
fn send_self_transfer(wallet: &str) -> Result<String, String> {
139+
let address = bitcoin_rpc(Some(wallet), &["getnewaddress"])?;
140+
let send_args = vec![
141+
"-named".to_owned(),
142+
"sendtoaddress".to_owned(),
143+
format!("address={address}"),
144+
"amount=1".to_owned(),
145+
"fee_rate=25".to_owned(),
146+
];
147+
bitcoin_rpc_owned(Some(wallet), &send_args)
148+
}
149+
150+
fn decode_hex(hex: &str) -> Vec<u8> {
151+
assert_eq!(
152+
hex.len() % 2,
153+
0,
154+
"hex string must have an even number of characters"
155+
);
156+
(0..hex.len())
157+
.step_by(2)
158+
.map(|i| {
159+
u8::from_str_radix(&hex[i..i + 2], 16)
160+
.unwrap_or_else(|e| panic!("invalid hex at byte {i}: {e}"))
161+
})
162+
.collect()
163+
}
164+
165+
fn display_hash_to_internal_bytes(hex: &str) -> Vec<u8> {
166+
let mut bytes = decode_hex(hex);
167+
bytes.reverse();
168+
bytes
169+
}
170+
171+
pub fn create_mempool_self_transfer(wallet: &str) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
172+
let txid = match send_self_transfer(wallet) {
173+
Ok(txid) => txid,
174+
Err(_) => {
175+
// If the wallet exists but is unfunded or in an unexpected state,
176+
// try to recover by ensuring funding and retry once.
177+
ensure_wallet_loaded_and_funded(wallet);
178+
send_self_transfer(wallet)
179+
.unwrap_or_else(|e| panic!("failed to create self-transfer in {wallet}: {e}"))
180+
}
181+
};
182+
let raw_tx_hex = bitcoin_rpc(None, &["getrawtransaction", txid.as_str()])
183+
.unwrap_or_else(|e| panic!("failed to fetch raw transaction {txid}: {e}"));
184+
let raw_tx = decode_hex(&raw_tx_hex);
185+
let tx: Transaction = decode_from_slice(&raw_tx)
186+
.unwrap_or_else(|e| panic!("failed to deserialize raw transaction {txid}: {e}"));
187+
let txid_display = format!("{:x}", tx.compute_txid());
188+
assert_eq!(
189+
txid_display, txid,
190+
"transaction id from raw tx should match RPC txid"
191+
);
192+
let wtxid_display = format!("{:x}", tx.compute_wtxid());
193+
(
194+
display_hash_to_internal_bytes(&txid_display),
195+
display_hash_to_internal_bytes(&wtxid_display),
196+
raw_tx,
197+
)
198+
}
199+
28200
pub async fn connect_unix_stream(
29201
path: impl AsRef<Path>,
30202
) -> VatNetwork<BufReader<Compat<OwnedReadHalf>>> {
@@ -55,6 +227,8 @@ pub async fn connect_unix_stream(
55227
pub async fn bootstrap(
56228
mut rpc_system: RpcSystem<capnp_rpc::rpc_twoparty_capnp::Side>,
57229
) -> (init::Client, thread::Client) {
230+
ensure_bootstrap_chain_ready();
231+
58232
let client: init::Client = rpc_system.bootstrap(Side::Server);
59233
tokio::task::spawn_local(rpc_system);
60234
let create_client_response = client
@@ -91,9 +265,8 @@ pub async fn make_mining(init: &init::Client, thread: &thread::Client) -> mining
91265
/// The node must have height > 16. At height <= 16 the BIP34 height push
92266
/// is only one byte, which is shorter than the two-byte minimum scriptSig
93267
/// required by consensus (see `CheckTransaction`), causing `createNewBlock`
94-
/// to fail with `bad-cb-length`. Either generate blocks via bitcoin rpc
95-
/// (`generatetodescriptor`) before running these tests, or (in a real miner)
96-
/// pad the coinbase scriptSig with an extra push like `OP_0`.
268+
/// to fail with `bad-cb-length`. `bootstrap()` ensures chain height is at
269+
/// least 101 before tests run, which satisfies this precondition.
97270
pub async fn make_block_template(
98271
mining: &mining::Client,
99272
thread: &thread::Client,

0 commit comments

Comments
 (0)