Skip to content

Commit 19941e9

Browse files
committed
Create test harness crate
1 parent 2051713 commit 19941e9

File tree

3 files changed

+425
-0
lines changed

3 files changed

+425
-0
lines changed

testutil/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "testutil"
3+
version = "0.0.1"
4+
edition = "2021"
5+
6+
[dependencies]
7+
bitcoind = { version = "0.36.0", features = ["26_0"] }
8+
spaced = { path = "../node" }
9+
assert_cmd = "2.0.16"

testutil/src/lib.rs

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
pub extern crate bitcoind;
2+
pub mod spaced;
3+
4+
use std::{sync::Arc, time::Duration};
5+
6+
use ::spaced::{
7+
jsonrpsee::tokio,
8+
node::protocol::{
9+
bitcoin,
10+
bitcoin::{
11+
absolute, address::NetworkChecked, block, block::Header, hashes::Hash,
12+
key::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
13+
OutPoint, ScriptBuf, ScriptHash, Sequence, Transaction, TxIn, TxMerkleNode, TxOut,
14+
Txid,
15+
},
16+
},
17+
rpc::RpcClient,
18+
};
19+
use anyhow::Result;
20+
use bitcoind::{
21+
anyhow,
22+
bitcoincore_rpc::{
23+
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
24+
RpcApi,
25+
},
26+
BitcoinD,
27+
};
28+
29+
use crate::spaced::SpaceD;
30+
31+
#[derive(Debug)]
32+
pub struct TestRig {
33+
pub bitcoind: Arc<BitcoinD>,
34+
pub spaced: SpaceD,
35+
}
36+
37+
impl TestRig {
38+
pub async fn new() -> Result<Self> {
39+
let mut conf = bitcoind::Conf::default();
40+
// The RPC auth uses username "user" and password "password". If we
41+
// don't set this, bitcoind's RPC API becomes inaccessible to spaced due
42+
// to auth issues.
43+
conf.args = vec![
44+
"-regtest",
45+
"-fallbackfee=0.0001",
46+
"-rpcauth=user:70dbb4f60ccc95e154da97a43b7a9d06$00c10a3849edf2f10173e80d0bdadbde793ad9a80e6e6f9f71f978fb5c797343"
47+
];
48+
49+
let bitcoind =
50+
tokio::task::spawn_blocking(move || BitcoinD::from_downloaded_with_conf(&conf))
51+
.await
52+
.expect("handle")?;
53+
54+
let rpc_url = bitcoind.rpc_url();
55+
let spaced_conf = spaced::Conf {
56+
args: vec![
57+
"--chain",
58+
"regtest",
59+
"--bitcoin-rpc-url",
60+
&rpc_url,
61+
"--bitcoin-rpc-user",
62+
"user",
63+
"--bitcoin-rpc-password",
64+
"password",
65+
"--block-index",
66+
],
67+
};
68+
69+
let spaced = SpaceD::new(spaced_conf).await?;
70+
Ok(TestRig {
71+
bitcoind: Arc::new(bitcoind),
72+
spaced,
73+
})
74+
}
75+
76+
/// Waits until spaced tip == bitcoind tip
77+
pub async fn wait_until_synced(&self) -> anyhow::Result<()> {
78+
loop {
79+
let c = self.bitcoind.clone();
80+
let count = tokio::task::spawn_blocking(move || c.client.get_block_count())
81+
.await
82+
.expect("handle")? as u32;
83+
84+
let info = self.spaced.client.get_server_info().await?;
85+
if count == info.tip.height {
86+
return Ok(());
87+
}
88+
89+
tokio::time::sleep(Duration::from_millis(100)).await;
90+
}
91+
}
92+
93+
/// Waits until named wallet tip == bitcoind tip
94+
pub async fn wait_until_wallet_synced(&self, wallet_name: &str) -> anyhow::Result<()> {
95+
loop {
96+
let c = self.bitcoind.clone();
97+
let count = tokio::task::spawn_blocking(move || c.client.get_block_count())
98+
.await
99+
.expect("handle")? as u32;
100+
101+
let info = self
102+
.spaced
103+
.client
104+
.wallet_get_info(wallet_name.to_string())
105+
.await?;
106+
if count == info.tip {
107+
return Ok(());
108+
}
109+
tokio::time::sleep(Duration::from_millis(100)).await;
110+
}
111+
}
112+
113+
/// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
114+
/// `address`.
115+
///
116+
/// An async verison of bdk's testenv mine_blocks:
117+
/// https://github.com/bitcoindevkit/bdk/blob/master/crates/testenv/src/lib.rs
118+
pub async fn mine_blocks(
119+
&self,
120+
count: usize,
121+
address: Option<Address>,
122+
) -> Result<Vec<BlockHash>> {
123+
let coinbase_address = match address {
124+
Some(address) => address,
125+
None => {
126+
let c = self.bitcoind.clone();
127+
tokio::task::spawn_blocking(move || c.client.get_new_address(None, None))
128+
.await
129+
.expect("handle")?
130+
.assume_checked()
131+
}
132+
};
133+
let block_hashes = self
134+
.bitcoind
135+
.client
136+
.generate_to_address(count as _, &coinbase_address)?;
137+
Ok(block_hashes)
138+
}
139+
140+
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
141+
///
142+
/// An async version of bdk's testenv mine_empty_block:
143+
/// https://github.com/bitcoindevkit/bdk/blob/master/crates/testenv/src/lib.rs
144+
pub async fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
145+
let c = self.bitcoind.clone();
146+
let bt = tokio::task::spawn_blocking(move || {
147+
c.client.get_block_template(
148+
GetBlockTemplateModes::Template,
149+
&[GetBlockTemplateRules::SegWit],
150+
&[],
151+
)
152+
})
153+
.await
154+
.expect("handle")?;
155+
156+
let txdata = vec![Transaction {
157+
version: transaction::Version::ONE,
158+
lock_time: absolute::LockTime::from_height(0)?,
159+
input: vec![TxIn {
160+
previous_output: OutPoint::default(),
161+
script_sig: ScriptBuf::builder()
162+
.push_int(bt.height as _)
163+
// random number so that re-mining creates unique block
164+
.push_int(random())
165+
.into_script(),
166+
sequence: Sequence::default(),
167+
witness: bitcoin::Witness::new(),
168+
}],
169+
output: vec![TxOut {
170+
value: Amount::ZERO,
171+
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
172+
}],
173+
}];
174+
175+
let bits: [u8; 4] = bt
176+
.bits
177+
.clone()
178+
.try_into()
179+
.expect("rpc provided us with invalid bits");
180+
181+
let mut block = Block {
182+
header: Header {
183+
version: block::Version::default(),
184+
prev_blockhash: bt.previous_block_hash,
185+
merkle_root: TxMerkleNode::all_zeros(),
186+
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
187+
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
188+
nonce: 0,
189+
},
190+
txdata,
191+
};
192+
193+
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
194+
195+
for nonce in 0..=u32::MAX {
196+
block.header.nonce = nonce;
197+
if block.header.target().is_met_by(block.block_hash()) {
198+
break;
199+
}
200+
}
201+
202+
let block_hash = block.header.block_hash();
203+
204+
let c = self.bitcoind.clone();
205+
tokio::task::spawn_blocking(move || c.client.submit_block(&block))
206+
.await
207+
.expect("handle")?;
208+
209+
Ok((bt.height as usize, block_hash))
210+
}
211+
212+
/// Invalidate a number of blocks of a given size `count`.
213+
///
214+
/// An async version of bdk's testenv invalidate_blocks:
215+
/// https://github.com/bitcoindevkit/bdk/blob/master/crates/testenv/src/lib.rs
216+
pub async fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
217+
let mut hash = self.get_best_block_hash().await?;
218+
219+
for _ in 0..count {
220+
let prev_hash = self
221+
.bitcoind
222+
.client
223+
.get_block_info(&hash)?
224+
.previousblockhash;
225+
226+
let c = self.bitcoind.clone();
227+
tokio::task::spawn_blocking(move || c.client.invalidate_block(&hash))
228+
.await
229+
.expect("handle")?;
230+
231+
match prev_hash {
232+
Some(prev_hash) => hash = prev_hash,
233+
None => break,
234+
}
235+
}
236+
Ok(())
237+
}
238+
239+
pub async fn get_block_count(&self) -> Result<u64> {
240+
let c = self.bitcoind.clone();
241+
Ok(
242+
tokio::task::spawn_blocking(move || c.client.get_block_count())
243+
.await
244+
.expect("handle")?,
245+
)
246+
}
247+
248+
pub async fn get_best_block_hash(&self) -> Result<BlockHash> {
249+
let c = self.bitcoind.clone();
250+
Ok(
251+
tokio::task::spawn_blocking(move || c.client.get_best_block_hash())
252+
.await
253+
.expect("handle")?,
254+
)
255+
}
256+
257+
pub async fn get_block_hash(&self, height: u64) -> Result<BlockHash> {
258+
let c = self.bitcoind.clone();
259+
Ok(
260+
tokio::task::spawn_blocking(move || c.client.get_block_hash(height))
261+
.await
262+
.expect("handle")?,
263+
)
264+
}
265+
266+
/// Reorg a number of blocks of a given size `count`.
267+
/// Refer to [`SpaceD::mine_empty_block`] for more information.
268+
///
269+
/// An async version of bdk's testenv reorg:
270+
/// https://github.com/bitcoindevkit/bdk/blob/master/crates/testenv/src/lib.rs
271+
pub async fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
272+
let start_height = self.get_block_count().await?;
273+
self.invalidate_blocks(count).await?;
274+
275+
let res = self.mine_blocks(count, None).await?;
276+
assert_eq!(
277+
self.get_block_count().await?,
278+
start_height,
279+
"reorg should not result in height change"
280+
);
281+
Ok(res)
282+
}
283+
284+
/// Reorg with a number of empty blocks of a given size `count`.
285+
///
286+
/// An async version of bdk's testenv reorg_empty_blocks:
287+
/// https://github.com/bitcoindevkit/bdk/blob/master/crates/testenv/src/lib.rs
288+
pub async fn reorg_empty_blocks(&self, count: usize) -> Result<Vec<(usize, BlockHash)>> {
289+
let start_height = self.get_block_count().await?;
290+
self.invalidate_blocks(count).await?;
291+
292+
let mut res = Vec::with_capacity(count);
293+
for _ in 0..count {
294+
res.push(self.mine_empty_block().await?);
295+
}
296+
assert_eq!(
297+
self.get_block_count().await?,
298+
start_height,
299+
"reorg should not result in height change"
300+
);
301+
Ok(res)
302+
}
303+
304+
/// Send a tx of a given `amount` to a given `address`.
305+
///
306+
/// An async version of bdk's testenv send:
307+
/// https://github.com/bitcoindevkit/bdk/blob/master/crates/testenv/src/lib.rs
308+
pub async fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> Result<Txid> {
309+
let c = self.bitcoind.clone();
310+
let addr = address.clone();
311+
let txid = tokio::task::spawn_blocking(move || {
312+
c.client
313+
.send_to_address(&addr, amount, None, None, None, None, None, None)
314+
})
315+
.await
316+
.expect("handle")?;
317+
Ok(txid)
318+
}
319+
}

0 commit comments

Comments
 (0)