• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bitcoindevkit / bdk / 10790757472

10 Sep 2024 10:36AM UTC coverage: 81.706% (-0.03%) from 81.732%
10790757472

Pull #1320

github

web-flow
Merge 5f41abfd3 into 876065333
Pull Request #1320: ci: move to Nix (simpler, easy to maintain version)

8 of 9 new or added lines in 3 files covered. (88.89%)

7 existing lines in 1 file now uncovered.

11099 of 13584 relevant lines covered (81.71%)

14467.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.54
/crates/testenv/src/lib.rs
1
use bdk_chain::{
2
    bitcoin::{
3
        address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
4
        secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
5
        ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
6
    },
7
    local_chain::CheckPoint,
8
    BlockId,
9
};
10
use bitcoincore_rpc::{
11
    bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
12
    RpcApi,
13
};
14
use electrsd::bitcoind::anyhow::Context;
15

16
pub use electrsd;
17
pub use electrsd::bitcoind;
18
pub use electrsd::bitcoind::anyhow;
19
pub use electrsd::bitcoind::bitcoincore_rpc;
20
pub use electrsd::electrum_client;
21
use electrsd::electrum_client::ElectrumApi;
22
use std::time::Duration;
23

24
/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
25
/// instance connected to it.
26
pub struct TestEnv {
27
    pub bitcoind: electrsd::bitcoind::BitcoinD,
28
    pub electrsd: electrsd::ElectrsD,
29
}
30

31
/// Configuration parameters.
32
#[derive(Debug)]
33
pub struct Config<'a> {
34
    /// [`bitcoind::Conf`]
35
    pub bitcoind: bitcoind::Conf<'a>,
36
    /// [`electrsd::Conf`]
37
    pub electrsd: electrsd::Conf<'a>,
38
}
39

40
impl<'a> Default for Config<'a> {
41
    /// Use the default configuration plus set `http_enabled = true` for [`electrsd::Conf`]
42
    /// which is required for testing `bdk_esplora`.
43
    fn default() -> Self {
116✔
44
        Self {
116✔
45
            bitcoind: bitcoind::Conf::default(),
116✔
46
            electrsd: {
116✔
47
                let mut conf = electrsd::Conf::default();
116✔
48
                conf.http_enabled = true;
116✔
49
                conf
116✔
50
            },
116✔
51
        }
116✔
52
    }
116✔
53
}
54

55
impl TestEnv {
56
    /// Construct a new [`TestEnv`] instance with the default configuration used by BDK.
57
    pub fn new() -> anyhow::Result<Self> {
116✔
58
        TestEnv::new_with_config(Config::default())
116✔
59
    }
116✔
60

61
    /// Construct a new [`TestEnv`] instance with the provided [`Config`].
62
    pub fn new_with_config(config: Config) -> anyhow::Result<Self> {
116✔
63
        let bitcoind_exe = match std::env::var("BITCOIND_EXEC") {
116✔
64
            Ok(path) => path,
116✔
UNCOV
65
            Err(_) => bitcoind::downloaded_exe_path().context(
×
NEW
66
                "you need to provide an env var BITCOIND_EXEC or specify a bitcoind version feature",
×
UNCOV
67
            )?,
×
68
        };
69
        let bitcoind = bitcoind::BitcoinD::with_conf(bitcoind_exe, &config.bitcoind)?;
116✔
70

71
        let electrs_exe = match std::env::var("ELECTRS_EXEC") {
116✔
72
            Ok(path) => path,
116✔
UNCOV
73
            Err(_) => electrsd::downloaded_exe_path()
×
UNCOV
74
                .context("electrs version feature must be enabled")?,
×
75
        };
76
        let electrsd = electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &config.electrsd)?;
116✔
77

78
        Ok(Self { bitcoind, electrsd })
116✔
79
    }
116✔
80

81
    /// Exposes the [`ElectrumApi`] calls from the Electrum client.
82
    pub fn electrum_client(&self) -> &impl ElectrumApi {
×
83
        &self.electrsd.client
×
84
    }
×
85

86
    /// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
87
    pub fn rpc_client(&self) -> &impl RpcApi {
335✔
88
        &self.bitcoind.client
335✔
89
    }
335✔
90

91
    // Reset `electrsd` so that new blocks can be seen.
92
    pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
5✔
93
        let mut electrsd_conf = electrsd::Conf::default();
5✔
94
        electrsd_conf.http_enabled = true;
5✔
95
        let electrsd = match std::env::var_os("ELECTRS_EXEC") {
5✔
96
            Some(env_electrs_exe) => {
5✔
97
                electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
5✔
98
            }
99
            None => {
UNCOV
100
                let electrs_exe = electrsd::downloaded_exe_path()
×
UNCOV
101
                    .expect("electrs version feature must be enabled");
×
UNCOV
102
                electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
×
103
            }
104
        }?;
×
105
        self.electrsd = electrsd;
5✔
106
        Ok(self)
5✔
107
    }
5✔
108

109
    /// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
110
    /// `address`.
111
    pub fn mine_blocks(
312✔
112
        &self,
312✔
113
        count: usize,
312✔
114
        address: Option<Address>,
312✔
115
    ) -> anyhow::Result<Vec<BlockHash>> {
312✔
116
        let coinbase_address = match address {
312✔
117
            Some(address) => address,
30✔
118
            None => self
282✔
119
                .bitcoind
282✔
120
                .client
282✔
121
                .get_new_address(None, None)?
282✔
122
                .assume_checked(),
282✔
123
        };
124
        let block_hashes = self
312✔
125
            .bitcoind
312✔
126
            .client
312✔
127
            .generate_to_address(count as _, &coinbase_address)?;
312✔
128
        Ok(block_hashes)
312✔
129
    }
312✔
130

131
    /// Mine a block that is guaranteed to be empty even with transactions in the mempool.
132
    pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
1,675✔
133
        let bt = self.bitcoind.client.get_block_template(
1,675✔
134
            GetBlockTemplateModes::Template,
1,675✔
135
            &[GetBlockTemplateRules::SegWit],
1,675✔
136
            &[],
1,675✔
137
        )?;
1,675✔
138

139
        let txdata = vec![Transaction {
1,675✔
140
            version: transaction::Version::ONE,
1,675✔
141
            lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
1,675✔
142
            input: vec![TxIn {
1,675✔
143
                previous_output: bdk_chain::bitcoin::OutPoint::default(),
1,675✔
144
                script_sig: ScriptBuf::builder()
1,675✔
145
                    .push_int(bt.height as _)
1,675✔
146
                    // randomn number so that re-mining creates unique block
1,675✔
147
                    .push_int(random())
1,675✔
148
                    .into_script(),
1,675✔
149
                sequence: bdk_chain::bitcoin::Sequence::default(),
1,675✔
150
                witness: bdk_chain::bitcoin::Witness::new(),
1,675✔
151
            }],
1,675✔
152
            output: vec![TxOut {
1,675✔
153
                value: Amount::ZERO,
1,675✔
154
                script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
1,675✔
155
            }],
1,675✔
156
        }];
1,675✔
157

1,675✔
158
        let bits: [u8; 4] = bt
1,675✔
159
            .bits
1,675✔
160
            .clone()
1,675✔
161
            .try_into()
1,675✔
162
            .expect("rpc provided us with invalid bits");
1,675✔
163

164
        let mut block = Block {
1,675✔
165
            header: Header {
166
                version: bdk_chain::bitcoin::block::Version::default(),
1,675✔
167
                prev_blockhash: bt.previous_block_hash,
1,675✔
168
                merkle_root: TxMerkleNode::all_zeros(),
1,675✔
169
                time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
1,675✔
170
                bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
1,675✔
171
                nonce: 0,
1,675✔
172
            },
1,675✔
173
            txdata,
1,675✔
174
        };
1,675✔
175

1,675✔
176
        block.header.merkle_root = block.compute_merkle_root().expect("must compute");
1,675✔
177

178
        for nonce in 0..=u32::MAX {
3,370✔
179
            block.header.nonce = nonce;
3,370✔
180
            if block.header.target().is_met_by(block.block_hash()) {
3,370✔
181
                break;
1,675✔
182
            }
1,695✔
183
        }
184

185
        self.bitcoind.client.submit_block(&block)?;
1,675✔
186
        Ok((bt.height as usize, block.block_hash()))
1,675✔
187
    }
1,675✔
188

189
    /// This method waits for the Electrum notification indicating that a new block has been mined.
190
    /// `timeout` is the maximum [`Duration`] we want to wait for a response from Electrsd.
191
    pub fn wait_until_electrum_sees_block(&self, timeout: Duration) -> anyhow::Result<()> {
82✔
192
        self.electrsd.client.block_headers_subscribe()?;
82✔
193
        let delay = Duration::from_millis(200);
82✔
194
        let start = std::time::Instant::now();
82✔
195

196
        while start.elapsed() < timeout {
164✔
197
            self.electrsd.trigger()?;
164✔
198
            self.electrsd.client.ping()?;
164✔
199
            if self.electrsd.client.block_headers_pop()?.is_some() {
164✔
200
                return Ok(());
82✔
201
            }
82✔
202

82✔
203
            std::thread::sleep(delay);
82✔
204
        }
205

206
        Err(anyhow::Error::msg(
×
207
            "Timed out waiting for Electrsd to get block header",
×
208
        ))
×
209
    }
82✔
210

211
    /// This method waits for Electrsd to see a transaction with given `txid`. `timeout` is the
212
    /// maximum [`Duration`] we want to wait for a response from Electrsd.
213
    pub fn wait_until_electrum_sees_txid(
5✔
214
        &self,
5✔
215
        txid: Txid,
5✔
216
        timeout: Duration,
5✔
217
    ) -> anyhow::Result<()> {
5✔
218
        let delay = Duration::from_millis(200);
5✔
219
        let start = std::time::Instant::now();
5✔
220

221
        while start.elapsed() < timeout {
130✔
222
            if self.electrsd.client.transaction_get(&txid).is_ok() {
130✔
223
                return Ok(());
5✔
224
            }
125✔
225

125✔
226
            std::thread::sleep(delay);
125✔
227
        }
228

229
        Err(anyhow::Error::msg(
×
230
            "Timed out waiting for Electrsd to get transaction",
×
231
        ))
×
232
    }
5✔
233

234
    /// Invalidate a number of blocks of a given size `count`.
235
    pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
201✔
236
        let mut hash = self.bitcoind.client.get_best_block_hash()?;
201✔
237
        for _ in 0..count {
201✔
238
            let prev_hash = self
1,051✔
239
                .bitcoind
1,051✔
240
                .client
1,051✔
241
                .get_block_info(&hash)?
1,051✔
242
                .previousblockhash;
243
            self.bitcoind.client.invalidate_block(&hash)?;
1,051✔
244
            match prev_hash {
1,051✔
245
                Some(prev_hash) => hash = prev_hash,
1,051✔
246
                None => break,
×
247
            }
248
        }
249
        Ok(())
201✔
250
    }
201✔
251

252
    /// Reorg a number of blocks of a given size `count`.
253
    /// Refer to [`TestEnv::mine_empty_block`] for more information.
254
    pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
6✔
255
        let start_height = self.bitcoind.client.get_block_count()?;
6✔
256
        self.invalidate_blocks(count)?;
6✔
257

258
        let res = self.mine_blocks(count, None);
6✔
259
        assert_eq!(
6✔
260
            self.bitcoind.client.get_block_count()?,
6✔
261
            start_height,
262
            "reorg should not result in height change"
×
263
        );
264
        res
6✔
265
    }
6✔
266

267
    /// Reorg with a number of empty blocks of a given size `count`.
268
    pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
195✔
269
        let start_height = self.bitcoind.client.get_block_count()?;
195✔
270
        self.invalidate_blocks(count)?;
195✔
271

272
        let res = (0..count)
195✔
273
            .map(|_| self.mine_empty_block())
1,015✔
274
            .collect::<Result<Vec<_>, _>>()?;
195✔
275
        assert_eq!(
195✔
276
            self.bitcoind.client.get_block_count()?,
195✔
277
            start_height,
278
            "reorg should not result in height change"
×
279
        );
280
        Ok(res)
195✔
281
    }
195✔
282

283
    /// Send a tx of a given `amount` to a given `address`.
284
    pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
265✔
285
        let txid = self
265✔
286
            .bitcoind
265✔
287
            .client
265✔
288
            .send_to_address(address, amount, None, None, None, None, None, None)?;
265✔
289
        Ok(txid)
265✔
290
    }
265✔
291

292
    /// Create a checkpoint linked list of all the blocks in the chain.
293
    pub fn make_checkpoint_tip(&self) -> CheckPoint {
90✔
294
        CheckPoint::from_block_ids((0_u32..).map_while(|height| {
4,900✔
295
            self.bitcoind
4,900✔
296
                .client
4,900✔
297
                .get_block_hash(height as u64)
4,900✔
298
                .ok()
4,900✔
299
                .map(|hash| BlockId { height, hash })
4,900✔
300
        }))
4,900✔
301
        .expect("must craft tip")
90✔
302
    }
90✔
303

304
    /// Get the genesis hash of the blockchain.
305
    pub fn genesis_hash(&self) -> anyhow::Result<BlockHash> {
30✔
306
        let hash = self.bitcoind.client.get_block_hash(0)?;
30✔
307
        Ok(hash)
30✔
308
    }
30✔
309
}
310

311
#[cfg(test)]
312
mod test {
313
    use crate::TestEnv;
314
    use core::time::Duration;
315
    use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
316

317
    /// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
318
    #[test]
319
    fn test_reorg_is_detected_in_electrsd() -> Result<()> {
1✔
320
        let env = TestEnv::new()?;
1✔
321

322
        // Mine some blocks.
323
        env.mine_blocks(101, None)?;
1✔
324
        env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
1✔
325
        let height = env.bitcoind.client.get_block_count()?;
1✔
326
        let blocks = (0..=height)
1✔
327
            .map(|i| env.bitcoind.client.get_block_hash(i))
103✔
328
            .collect::<Result<Vec<_>, _>>()?;
1✔
329

330
        // Perform reorg on six blocks.
331
        env.reorg(6)?;
1✔
332
        env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
1✔
333
        let reorged_height = env.bitcoind.client.get_block_count()?;
1✔
334
        let reorged_blocks = (0..=height)
1✔
335
            .map(|i| env.bitcoind.client.get_block_hash(i))
103✔
336
            .collect::<Result<Vec<_>, _>>()?;
1✔
337

338
        assert_eq!(height, reorged_height);
1✔
339

340
        // Block hashes should not be equal on the six reorged blocks.
341
        for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
103✔
342
            match i <= height as usize - 6 {
103✔
343
                true => assert_eq!(block, reorged_block),
97✔
344
                false => assert_ne!(block, reorged_block),
6✔
345
            }
346
        }
347

348
        Ok(())
1✔
349
    }
1✔
350
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc