|
| 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