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

bitcoindevkit / bdk / 10348217438

12 Aug 2024 08:10AM CUT coverage: 81.794% (-0.02%) from 81.813%
10348217438

Pull #1535

github

web-flow
Merge 2c0bc45ec into 98c49592d
Pull Request #1535: test(electrum): Test sync in reorg and no-reorg situations

19 of 25 new or added lines in 1 file covered. (76.0%)

1 existing line in 1 file now uncovered.

10908 of 13336 relevant lines covered (81.79%)

12995.15 hits per line

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

90.83
/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
pub use electrsd;
15
pub use electrsd::bitcoind;
16
pub use electrsd::bitcoind::anyhow;
17
pub use electrsd::bitcoind::bitcoincore_rpc;
18
pub use electrsd::electrum_client;
19
use electrsd::electrum_client::ElectrumApi;
20
use std::time::Duration;
21

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

29
impl TestEnv {
30
    /// Construct a new [`TestEnv`] instance with default configurations.
31
    pub fn new() -> anyhow::Result<Self> {
116✔
32
        let bitcoind = match std::env::var_os("BITCOIND_EXE") {
116✔
33
            Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
×
34
            None => {
35
                let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
116✔
36
                    .expect(
116✔
37
                "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
116✔
38
                );
116✔
39
                electrsd::bitcoind::BitcoinD::with_conf(
116✔
40
                    bitcoind_exe,
116✔
41
                    &electrsd::bitcoind::Conf::default(),
116✔
42
                )
116✔
43
            }
44
        }?;
×
45

46
        let mut electrsd_conf = electrsd::Conf::default();
116✔
47
        electrsd_conf.http_enabled = true;
116✔
48
        let electrsd = match std::env::var_os("ELECTRS_EXE") {
116✔
49
            Some(env_electrs_exe) => {
×
50
                electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
×
51
            }
52
            None => {
53
                let electrs_exe = electrsd::downloaded_exe_path()
116✔
54
                    .expect("electrs version feature must be enabled");
116✔
55
                electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
116✔
56
            }
57
        }?;
×
58

59
        Ok(Self { bitcoind, electrsd })
116✔
60
    }
116✔
61

62
    /// Exposes the [`ElectrumApi`] calls from the Electrum client.
63
    pub fn electrum_client(&self) -> &impl ElectrumApi {
×
64
        &self.electrsd.client
×
65
    }
×
66

67
    /// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
68
    pub fn rpc_client(&self) -> &impl RpcApi {
335✔
69
        &self.bitcoind.client
335✔
70
    }
335✔
71

72
    // Reset `electrsd` so that new blocks can be seen.
73
    pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
5✔
74
        let mut electrsd_conf = electrsd::Conf::default();
5✔
75
        electrsd_conf.http_enabled = true;
5✔
76
        let electrsd = match std::env::var_os("ELECTRS_EXE") {
5✔
77
            Some(env_electrs_exe) => {
×
78
                electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
×
79
            }
80
            None => {
81
                let electrs_exe = electrsd::downloaded_exe_path()
5✔
82
                    .expect("electrs version feature must be enabled");
5✔
83
                electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
5✔
84
            }
85
        }?;
×
86
        self.electrsd = electrsd;
5✔
87
        Ok(self)
5✔
88
    }
5✔
89

90
    /// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
91
    /// `address`.
92
    pub fn mine_blocks(
312✔
93
        &self,
312✔
94
        count: usize,
312✔
95
        address: Option<Address>,
312✔
96
    ) -> anyhow::Result<Vec<BlockHash>> {
312✔
97
        let coinbase_address = match address {
312✔
98
            Some(address) => address,
30✔
99
            None => self
282✔
100
                .bitcoind
282✔
101
                .client
282✔
102
                .get_new_address(None, None)?
282✔
103
                .assume_checked(),
282✔
104
        };
105
        let block_hashes = self
312✔
106
            .bitcoind
312✔
107
            .client
312✔
108
            .generate_to_address(count as _, &coinbase_address)?;
312✔
109
        Ok(block_hashes)
312✔
110
    }
312✔
111

112
    /// Mine a block that is guaranteed to be empty even with transactions in the mempool.
113
    pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
1,675✔
114
        let bt = self.bitcoind.client.get_block_template(
1,675✔
115
            GetBlockTemplateModes::Template,
1,675✔
116
            &[GetBlockTemplateRules::SegWit],
1,675✔
117
            &[],
1,675✔
118
        )?;
1,675✔
119

120
        let txdata = vec![Transaction {
1,675✔
121
            version: transaction::Version::ONE,
1,675✔
122
            lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
1,675✔
123
            input: vec![TxIn {
1,675✔
124
                previous_output: bdk_chain::bitcoin::OutPoint::default(),
1,675✔
125
                script_sig: ScriptBuf::builder()
1,675✔
126
                    .push_int(bt.height as _)
1,675✔
127
                    // randomn number so that re-mining creates unique block
1,675✔
128
                    .push_int(random())
1,675✔
129
                    .into_script(),
1,675✔
130
                sequence: bdk_chain::bitcoin::Sequence::default(),
1,675✔
131
                witness: bdk_chain::bitcoin::Witness::new(),
1,675✔
132
            }],
1,675✔
133
            output: vec![TxOut {
1,675✔
134
                value: Amount::ZERO,
1,675✔
135
                script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
1,675✔
136
            }],
1,675✔
137
        }];
1,675✔
138

1,675✔
139
        let bits: [u8; 4] = bt
1,675✔
140
            .bits
1,675✔
141
            .clone()
1,675✔
142
            .try_into()
1,675✔
143
            .expect("rpc provided us with invalid bits");
1,675✔
144

145
        let mut block = Block {
1,675✔
146
            header: Header {
147
                version: bdk_chain::bitcoin::block::Version::default(),
1,675✔
148
                prev_blockhash: bt.previous_block_hash,
1,675✔
149
                merkle_root: TxMerkleNode::all_zeros(),
1,675✔
150
                time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
1,675✔
151
                bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
1,675✔
152
                nonce: 0,
1,675✔
153
            },
1,675✔
154
            txdata,
1,675✔
155
        };
1,675✔
156

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

159
        for nonce in 0..=u32::MAX {
3,400✔
160
            block.header.nonce = nonce;
3,400✔
161
            if block.header.target().is_met_by(block.block_hash()) {
3,400✔
162
                break;
1,675✔
163
            }
1,725✔
164
        }
165

166
        self.bitcoind.client.submit_block(&block)?;
1,675✔
167
        Ok((bt.height as usize, block.block_hash()))
1,675✔
168
    }
1,675✔
169

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

177
        while start.elapsed() < timeout {
164✔
178
            self.electrsd.trigger()?;
164✔
179
            self.electrsd.client.ping()?;
164✔
180
            if self.electrsd.client.block_headers_pop()?.is_some() {
164✔
181
                return Ok(());
82✔
182
            }
82✔
183

82✔
184
            std::thread::sleep(delay);
82✔
185
        }
186

NEW
187
        Err(anyhow::Error::msg(
×
NEW
188
            "Timed out waiting for Electrsd to get block header",
×
NEW
189
        ))
×
190
    }
82✔
191

192
    /// This method waits for Electrsd to see a transaction with given `txid`. `timeout` is the
193
    /// maximum [`Duration`] we want to wait for a response from Electrsd.
194
    pub fn wait_until_electrum_sees_txid(
5✔
195
        &self,
5✔
196
        txid: Txid,
5✔
197
        timeout: Duration,
5✔
198
    ) -> anyhow::Result<()> {
5✔
199
        let delay = Duration::from_millis(200);
5✔
200
        let start = std::time::Instant::now();
5✔
201

202
        while start.elapsed() < timeout {
130✔
203
            if self.electrsd.client.transaction_get(&txid).is_ok() {
130✔
204
                return Ok(());
5✔
205
            }
125✔
206

125✔
207
            std::thread::sleep(delay);
125✔
208
        }
209

NEW
210
        Err(anyhow::Error::msg(
×
NEW
211
            "Timed out waiting for Electrsd to get transaction",
×
NEW
212
        ))
×
213
    }
5✔
214

215
    /// Invalidate a number of blocks of a given size `count`.
216
    pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
201✔
217
        let mut hash = self.bitcoind.client.get_best_block_hash()?;
201✔
218
        for _ in 0..count {
201✔
219
            let prev_hash = self
1,051✔
220
                .bitcoind
1,051✔
221
                .client
1,051✔
222
                .get_block_info(&hash)?
1,051✔
223
                .previousblockhash;
224
            self.bitcoind.client.invalidate_block(&hash)?;
1,051✔
225
            match prev_hash {
1,051✔
226
                Some(prev_hash) => hash = prev_hash,
1,051✔
227
                None => break,
×
228
            }
229
        }
230
        Ok(())
201✔
231
    }
201✔
232

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

239
        let res = self.mine_blocks(count, None);
6✔
240
        assert_eq!(
6✔
241
            self.bitcoind.client.get_block_count()?,
6✔
242
            start_height,
243
            "reorg should not result in height change"
×
244
        );
245
        res
6✔
246
    }
6✔
247

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

253
        let res = (0..count)
195✔
254
            .map(|_| self.mine_empty_block())
1,015✔
255
            .collect::<Result<Vec<_>, _>>()?;
195✔
256
        assert_eq!(
195✔
257
            self.bitcoind.client.get_block_count()?,
195✔
258
            start_height,
259
            "reorg should not result in height change"
×
260
        );
261
        Ok(res)
195✔
262
    }
195✔
263

264
    /// Send a tx of a given `amount` to a given `address`.
265
    pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
265✔
266
        let txid = self
265✔
267
            .bitcoind
265✔
268
            .client
265✔
269
            .send_to_address(address, amount, None, None, None, None, None, None)?;
265✔
270
        Ok(txid)
265✔
271
    }
265✔
272

273
    /// Create a checkpoint linked list of all the blocks in the chain.
274
    pub fn make_checkpoint_tip(&self) -> CheckPoint {
90✔
275
        CheckPoint::from_block_ids((0_u32..).map_while(|height| {
4,900✔
276
            self.bitcoind
4,900✔
277
                .client
4,900✔
278
                .get_block_hash(height as u64)
4,900✔
279
                .ok()
4,900✔
280
                .map(|hash| BlockId { height, hash })
4,900✔
281
        }))
4,900✔
282
        .expect("must craft tip")
90✔
283
    }
90✔
284

285
    /// Get the genesis hash of the blockchain.
286
    pub fn genesis_hash(&self) -> anyhow::Result<BlockHash> {
30✔
287
        let hash = self.bitcoind.client.get_block_hash(0)?;
30✔
288
        Ok(hash)
30✔
289
    }
30✔
290
}
291

292
#[cfg(test)]
293
mod test {
294
    use crate::TestEnv;
295
    use core::time::Duration;
296
    use electrsd::bitcoind::{anyhow::Result, bitcoincore_rpc::RpcApi};
297

298
    /// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
299
    #[test]
300
    fn test_reorg_is_detected_in_electrsd() -> Result<()> {
1✔
301
        let env = TestEnv::new()?;
1✔
302

303
        // Mine some blocks.
304
        env.mine_blocks(101, None)?;
1✔
305
        env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
1✔
306
        let height = env.bitcoind.client.get_block_count()?;
1✔
307
        let blocks = (0..=height)
1✔
308
            .map(|i| env.bitcoind.client.get_block_hash(i))
103✔
309
            .collect::<Result<Vec<_>, _>>()?;
1✔
310

311
        // Perform reorg on six blocks.
312
        env.reorg(6)?;
1✔
313
        env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
1✔
314
        let reorged_height = env.bitcoind.client.get_block_count()?;
1✔
315
        let reorged_blocks = (0..=height)
1✔
316
            .map(|i| env.bitcoind.client.get_block_hash(i))
103✔
317
            .collect::<Result<Vec<_>, _>>()?;
1✔
318

319
        assert_eq!(height, reorged_height);
1✔
320

321
        // Block hashes should not be equal on the six reorged blocks.
322
        for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
103✔
323
            match i <= height as usize - 6 {
103✔
324
                true => assert_eq!(block, reorged_block),
97✔
325
                false => assert_ne!(block, reorged_block),
6✔
326
            }
327
        }
328

329
        Ok(())
1✔
330
    }
1✔
331
}
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