fedimint_testing/btc/
real.rs

1use std::sync::Arc;
2use std::time::Duration;
3
4use anyhow::Context;
5use async_trait::async_trait;
6use bitcoin::{Address, Transaction, Txid};
7use bitcoincore_rpc::{Auth, Client, RpcApi};
8use fedimint_core::encoding::Decodable;
9use fedimint_core::module::registry::ModuleDecoderRegistry;
10use fedimint_core::task::{block_in_place, sleep_in_test};
11use fedimint_core::txoproof::TxOutProof;
12use fedimint_core::util::SafeUrl;
13use fedimint_core::{Amount, task};
14use fedimint_logging::LOG_TEST;
15use fedimint_server_core::bitcoin_rpc::DynServerBitcoinRpc;
16use tracing::{debug, trace, warn};
17
18use crate::btc::BitcoinTest;
19
20/// Fixture implementing bitcoin node under test by talking to a `bitcoind` with
21/// no locking considerations.
22///
23/// This function assumes the caller already took care of locking
24/// considerations).
25#[derive(Clone)]
26struct RealBitcoinTestNoLock {
27    client: Arc<Client>,
28    /// RPC used to connect to bitcoind, used for waiting for the RPC to sync
29    rpc: DynServerBitcoinRpc,
30}
31
32impl RealBitcoinTestNoLock {
33    const ERROR: &'static str = "Bitcoin RPC returned an error";
34}
35
36#[async_trait]
37impl BitcoinTest for RealBitcoinTestNoLock {
38    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
39        unimplemented!(
40            "You should never try to lock `RealBitcoinTestNoLock`. Lock `RealBitcoinTest` instead"
41        )
42    }
43
44    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
45        // `Stdio::piped` can't even parse outputs larger
46        // than pipe buffer size (64K on Linux, 16K on MacOS), so
47        // we should split larger requesteds into smaller chunks.
48        //
49        // On top of it mining a lot of blocks is just slow, so should
50        // be avoided.
51        const BLOCK_NUM_LIMIT: u64 = 32;
52
53        if BLOCK_NUM_LIMIT < block_num {
54            warn!(
55                target: LOG_TEST,
56                %block_num,
57                "Mining a lot of blocks (even when split) is a terrible idea and can lead to issues. Splitting request just to make it work somehow."
58            );
59            let mut block_num = block_num;
60            let mut blocks = vec![];
61
62            loop {
63                if BLOCK_NUM_LIMIT < block_num {
64                    block_num -= BLOCK_NUM_LIMIT;
65                    blocks.append(
66                        &mut Box::pin(async { self.mine_blocks(BLOCK_NUM_LIMIT).await }).await,
67                    );
68                } else {
69                    blocks.append(&mut Box::pin(async { self.mine_blocks(block_num).await }).await);
70                    return blocks;
71                }
72            }
73        }
74
75        let new_address = self.get_new_address().await;
76        let mined_block_hashes = block_in_place(|| {
77            self.client
78                .generate_to_address(block_num, &new_address)
79                .expect(Self::ERROR)
80        });
81
82        if let Some(block_hash) = mined_block_hashes.last() {
83            let last_mined_block = block_in_place(|| {
84                self.client
85                    .get_block_header_info(block_hash)
86                    .expect("rpc failed")
87            });
88            let expected_block_count = last_mined_block.height as u64 + 1;
89            // waits for the rpc client to catch up to bitcoind
90            loop {
91                let current_block_count = self.rpc.get_block_count().await.expect("rpc failed");
92                if current_block_count < expected_block_count {
93                    debug!(
94                        target: LOG_TEST,
95                        %block_num,
96                        %expected_block_count,
97                        %current_block_count,
98                        "Waiting for blocks to be mined"
99                    );
100                    sleep_in_test("waiting for blocks to be mined", Duration::from_millis(200))
101                        .await;
102                } else {
103                    debug!(
104                        target: LOG_TEST,
105                        ?block_num,
106                        %expected_block_count,
107                        %current_block_count,
108                        "Mined blocks"
109                    );
110                    break;
111                }
112            }
113        }
114
115        mined_block_hashes
116    }
117
118    async fn prepare_funding_wallet(&self) {
119        let block_count =
120            block_in_place(|| self.client.get_block_count().expect("should not fail"));
121        if block_count < 100 {
122            self.mine_blocks(100 - block_count).await;
123        }
124    }
125
126    async fn send_and_mine_block(
127        &self,
128        address: &Address,
129        amount: bitcoin::Amount,
130    ) -> (TxOutProof, Transaction) {
131        let id = block_in_place(|| {
132            self.client
133                .send_to_address(address, amount, None, None, None, None, None, None)
134                .expect(Self::ERROR)
135        });
136        let mined_block_hashes = self.mine_blocks(1).await;
137        let mined_block_hash = mined_block_hashes.first().expect("mined a block");
138
139        let tx = block_in_place(|| {
140            self.client
141                .get_raw_transaction(&id, Some(mined_block_hash))
142                .expect(Self::ERROR)
143        });
144        let proof = TxOutProof::consensus_decode_whole(
145            &loop {
146                match block_in_place(|| self.client.get_tx_out_proof(&[id], None)) {
147                    Ok(o) => break o,
148                    Err(e) => {
149                        if e.to_string().contains("not yet in block") {
150                            // mostly to yield, as we no other yield points
151                            task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
152                            continue;
153                        }
154                        panic!("Could not get txoutproof: {e}");
155                    }
156                }
157            },
158            &ModuleDecoderRegistry::default(),
159        )
160        .expect(Self::ERROR);
161
162        (proof, tx)
163    }
164    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
165        self.mine_blocks(1).await;
166        block_in_place(|| {
167            self.client
168                .get_received_by_address(address, None)
169                .expect(Self::ERROR)
170                .into()
171        })
172    }
173
174    async fn get_new_address(&self) -> Address {
175        block_in_place(|| {
176            self.client
177                .get_new_address(None, None)
178                .expect(Self::ERROR)
179                .assume_checked()
180        })
181    }
182
183    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
184        loop {
185            if let Ok(tx) = block_in_place(|| self.client.get_mempool_entry(txid)) {
186                return tx.fees.base.into();
187            }
188
189            sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
190        }
191    }
192
193    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
194        let current_block_count = block_in_place(|| {
195            self.client
196                .get_block_count()
197                .expect("failed to fetch chain tip")
198        });
199        (0..=current_block_count)
200            .position(|height| {
201                block_in_place(|| {
202                    let block_hash = self
203                        .client
204                        .get_block_hash(height)
205                        .expect("failed to fetch block hash");
206
207                    self.client
208                        .get_block_info(&block_hash)
209                        .expect("failed to fetch block info")
210                        .tx
211                        .iter()
212                        .any(|id| id == txid)
213                })
214            })
215            .map(|height| height as u64)
216    }
217
218    async fn get_block_count(&self) -> u64 {
219        block_in_place(|| {
220            self.client
221                .get_block_count()
222                // The RPC function is confusingly named and actually returns the block height
223                .map(|count| count + 1)
224                .expect("failed to fetch block count")
225        })
226    }
227
228    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
229        block_in_place(|| self.client.get_raw_transaction(txid, None).ok())
230    }
231}
232
233/// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
234/// unlocked version (lock each call separately)
235///
236/// Default version (and thus the only one with `new`)
237pub struct RealBitcoinTest {
238    inner: RealBitcoinTestNoLock,
239}
240
241impl RealBitcoinTest {
242    const ERROR: &'static str = "Bitcoin RPC returned an error";
243
244    pub fn new(url: &SafeUrl, rpc: DynServerBitcoinRpc) -> Self {
245        let auth = Auth::UserPass(
246            url.username().to_owned(),
247            url.password().unwrap().to_owned(),
248        );
249
250        let host = url.without_auth().unwrap().to_string();
251
252        let client = Arc::new(Client::new(&host, auth).expect(Self::ERROR));
253
254        Self {
255            inner: RealBitcoinTestNoLock { client, rpc },
256        }
257    }
258}
259
260/// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
261/// locked version - locks the global lock during construction
262pub struct RealBitcoinTestLocked {
263    inner: RealBitcoinTestNoLock,
264    _guard: fs_lock::FileLock,
265}
266
267#[async_trait]
268impl BitcoinTest for RealBitcoinTest {
269    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
270        trace!("Trying to acquire global bitcoin lock");
271        let _guard = block_in_place(|| {
272            let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
273            fs_lock::FileLock::new_exclusive(
274                std::fs::OpenOptions::new()
275                    .write(true)
276                    .create(true)
277                    .truncate(true)
278                    .open(&lock_file_path)
279                    .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
280            )
281            .context("Failed to acquire exclusive lock file")
282        })
283        .expect("Failed to lock");
284        trace!("Acquired global bitcoin lock");
285        Box::new(RealBitcoinTestLocked {
286            inner: self.inner.clone(),
287            _guard,
288        })
289    }
290
291    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
292        let _lock = self.lock_exclusive().await;
293        self.inner.mine_blocks(block_num).await
294    }
295
296    async fn prepare_funding_wallet(&self) {
297        let _lock = self.lock_exclusive().await;
298        self.inner.prepare_funding_wallet().await;
299    }
300
301    async fn send_and_mine_block(
302        &self,
303        address: &Address,
304        amount: bitcoin::Amount,
305    ) -> (TxOutProof, Transaction) {
306        let _lock = self.lock_exclusive().await;
307        self.inner.send_and_mine_block(address, amount).await
308    }
309
310    async fn get_new_address(&self) -> Address {
311        let _lock = self.lock_exclusive().await;
312        self.inner.get_new_address().await
313    }
314
315    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
316        let _lock = self.lock_exclusive().await;
317        self.inner.mine_block_and_get_received(address).await
318    }
319
320    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
321        let _lock = self.lock_exclusive().await;
322        self.inner.get_mempool_tx_fee(txid).await
323    }
324
325    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
326        let _lock = self.lock_exclusive().await;
327        self.inner.get_tx_block_height(txid).await
328    }
329
330    async fn get_block_count(&self) -> u64 {
331        let _lock = self.lock_exclusive().await;
332        self.inner.get_block_count().await
333    }
334
335    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
336        let _lock = self.lock_exclusive().await;
337        self.inner.get_mempool_tx(txid).await
338    }
339}
340
341#[async_trait]
342impl BitcoinTest for RealBitcoinTestLocked {
343    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
344        panic!("Double-locking would lead to a hang");
345    }
346
347    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
348        let pre = block_in_place(|| self.inner.client.get_block_count().unwrap());
349        let mined_block_hashes = self.inner.mine_blocks(block_num).await;
350        let post = block_in_place(|| self.inner.client.get_block_count().unwrap());
351        assert_eq!(post - pre, block_num);
352        mined_block_hashes
353    }
354
355    async fn prepare_funding_wallet(&self) {
356        self.inner.prepare_funding_wallet().await;
357    }
358
359    async fn send_and_mine_block(
360        &self,
361        address: &Address,
362        amount: bitcoin::Amount,
363    ) -> (TxOutProof, Transaction) {
364        self.inner.send_and_mine_block(address, amount).await
365    }
366
367    async fn get_new_address(&self) -> Address {
368        self.inner.get_new_address().await
369    }
370
371    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
372        self.inner.mine_block_and_get_received(address).await
373    }
374
375    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
376        self.inner.get_mempool_tx_fee(txid).await
377    }
378
379    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
380        self.inner.get_tx_block_height(txid).await
381    }
382
383    async fn get_block_count(&self) -> u64 {
384        self.inner.get_block_count().await
385    }
386
387    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
388        self.inner.get_mempool_tx(txid).await
389    }
390}