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 mined_block_hashes = self
76            .client
77            .generate_to_address(block_num, &self.get_new_address().await)
78            .expect(Self::ERROR);
79
80        if let Some(block_hash) = mined_block_hashes.last() {
81            let last_mined_block = self
82                .client
83                .get_block_header_info(block_hash)
84                .expect("rpc failed");
85            let expected_block_count = last_mined_block.height as u64 + 1;
86            // waits for the rpc client to catch up to bitcoind
87            loop {
88                let current_block_count = self.rpc.get_block_count().await.expect("rpc failed");
89                if current_block_count < expected_block_count {
90                    debug!(
91                        target: LOG_TEST,
92                        ?block_num,
93                        ?expected_block_count,
94                        ?current_block_count,
95                        "Waiting for blocks to be mined"
96                    );
97                    sleep_in_test("waiting for blocks to be mined", Duration::from_millis(200))
98                        .await;
99                } else {
100                    debug!(
101                        target: LOG_TEST,
102                        ?block_num,
103                        ?expected_block_count,
104                        ?current_block_count,
105                        "Mined blocks"
106                    );
107                    break;
108                }
109            }
110        }
111
112        mined_block_hashes
113    }
114
115    async fn prepare_funding_wallet(&self) {
116        let block_count = self.client.get_block_count().expect("should not fail");
117        if block_count < 100 {
118            self.mine_blocks(100 - block_count).await;
119        }
120    }
121
122    async fn send_and_mine_block(
123        &self,
124        address: &Address,
125        amount: bitcoin::Amount,
126    ) -> (TxOutProof, Transaction) {
127        let id = self
128            .client
129            .send_to_address(address, amount, None, None, None, None, None, None)
130            .expect(Self::ERROR);
131        let mined_block_hashes = self.mine_blocks(1).await;
132        let mined_block_hash = mined_block_hashes.first().expect("mined a block");
133
134        let tx = self
135            .client
136            .get_raw_transaction(&id, Some(mined_block_hash))
137            .expect(Self::ERROR);
138        let proof = TxOutProof::consensus_decode_whole(
139            &loop {
140                match self.client.get_tx_out_proof(&[id], None) {
141                    Ok(o) => break o,
142                    Err(e) => {
143                        if e.to_string().contains("not yet in block") {
144                            // mostly to yield, as we no other yield points
145                            task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
146                            continue;
147                        }
148                        panic!("Could not get txoutproof: {e}");
149                    }
150                }
151            },
152            &ModuleDecoderRegistry::default(),
153        )
154        .expect(Self::ERROR);
155
156        (proof, tx)
157    }
158    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
159        self.mine_blocks(1).await;
160        self.client
161            .get_received_by_address(address, None)
162            .expect(Self::ERROR)
163            .into()
164    }
165
166    async fn get_new_address(&self) -> Address {
167        self.client
168            .get_new_address(None, None)
169            .expect(Self::ERROR)
170            .assume_checked()
171    }
172
173    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
174        loop {
175            if let Ok(tx) = self.client.get_mempool_entry(txid) {
176                return tx.fees.base.into();
177            }
178
179            sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
180        }
181    }
182
183    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
184        let current_block_count = self
185            .client
186            .get_block_count()
187            .expect("failed to fetch chain tip");
188        (0..=current_block_count)
189            .position(|height| {
190                let block_hash = self
191                    .client
192                    .get_block_hash(height)
193                    .expect("failed to fetch block hash");
194
195                self.client
196                    .get_block_info(&block_hash)
197                    .expect("failed to fetch block info")
198                    .tx
199                    .iter()
200                    .any(|id| id == txid)
201            })
202            .map(|height| height as u64)
203    }
204
205    async fn get_block_count(&self) -> u64 {
206        self.client
207            .get_block_count()
208            // The RPC function is confusingly named and actually returns the block height
209            .map(|count| count + 1)
210            .expect("failed to fetch block count")
211    }
212
213    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
214        self.client.get_raw_transaction(txid, None).ok()
215    }
216}
217
218/// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
219/// unlocked version (lock each call separately)
220///
221/// Default version (and thus the only one with `new`)
222pub struct RealBitcoinTest {
223    inner: RealBitcoinTestNoLock,
224}
225
226impl RealBitcoinTest {
227    const ERROR: &'static str = "Bitcoin RPC returned an error";
228
229    pub fn new(url: &SafeUrl, rpc: DynServerBitcoinRpc) -> Self {
230        let auth = Auth::UserPass(
231            url.username().to_owned(),
232            url.password().unwrap().to_owned(),
233        );
234
235        let host = url.without_auth().unwrap().to_string();
236
237        let client = Arc::new(Client::new(&host, auth).expect(Self::ERROR));
238
239        Self {
240            inner: RealBitcoinTestNoLock { client, rpc },
241        }
242    }
243}
244
245/// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
246/// locked version - locks the global lock during construction
247pub struct RealBitcoinTestLocked {
248    inner: RealBitcoinTestNoLock,
249    _guard: fs_lock::FileLock,
250}
251
252#[async_trait]
253impl BitcoinTest for RealBitcoinTest {
254    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
255        trace!("Trying to acquire global bitcoin lock");
256        let _guard = block_in_place(|| {
257            let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
258            fs_lock::FileLock::new_exclusive(
259                std::fs::OpenOptions::new()
260                    .write(true)
261                    .create(true)
262                    .truncate(true)
263                    .open(&lock_file_path)
264                    .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
265            )
266            .context("Failed to acquire exclusive lock file")
267        })
268        .expect("Failed to lock");
269        trace!("Acquired global bitcoin lock");
270        Box::new(RealBitcoinTestLocked {
271            inner: self.inner.clone(),
272            _guard,
273        })
274    }
275
276    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
277        let _lock = self.lock_exclusive().await;
278        self.inner.mine_blocks(block_num).await
279    }
280
281    async fn prepare_funding_wallet(&self) {
282        let _lock = self.lock_exclusive().await;
283        self.inner.prepare_funding_wallet().await;
284    }
285
286    async fn send_and_mine_block(
287        &self,
288        address: &Address,
289        amount: bitcoin::Amount,
290    ) -> (TxOutProof, Transaction) {
291        let _lock = self.lock_exclusive().await;
292        self.inner.send_and_mine_block(address, amount).await
293    }
294
295    async fn get_new_address(&self) -> Address {
296        let _lock = self.lock_exclusive().await;
297        self.inner.get_new_address().await
298    }
299
300    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
301        let _lock = self.lock_exclusive().await;
302        self.inner.mine_block_and_get_received(address).await
303    }
304
305    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
306        let _lock = self.lock_exclusive().await;
307        self.inner.get_mempool_tx_fee(txid).await
308    }
309
310    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
311        let _lock = self.lock_exclusive().await;
312        self.inner.get_tx_block_height(txid).await
313    }
314
315    async fn get_block_count(&self) -> u64 {
316        let _lock = self.lock_exclusive().await;
317        self.inner.get_block_count().await
318    }
319
320    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
321        let _lock = self.lock_exclusive().await;
322        self.inner.get_mempool_tx(txid).await
323    }
324}
325
326#[async_trait]
327impl BitcoinTest for RealBitcoinTestLocked {
328    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
329        panic!("Double-locking would lead to a hang");
330    }
331
332    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
333        let pre = self.inner.client.get_block_count().unwrap();
334        let mined_block_hashes = self.inner.mine_blocks(block_num).await;
335        let post = self.inner.client.get_block_count().unwrap();
336        assert_eq!(post - pre, block_num);
337        mined_block_hashes
338    }
339
340    async fn prepare_funding_wallet(&self) {
341        self.inner.prepare_funding_wallet().await;
342    }
343
344    async fn send_and_mine_block(
345        &self,
346        address: &Address,
347        amount: bitcoin::Amount,
348    ) -> (TxOutProof, Transaction) {
349        self.inner.send_and_mine_block(address, amount).await
350    }
351
352    async fn get_new_address(&self) -> Address {
353        self.inner.get_new_address().await
354    }
355
356    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
357        self.inner.mine_block_and_get_received(address).await
358    }
359
360    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
361        self.inner.get_mempool_tx_fee(txid).await
362    }
363
364    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
365        self.inner.get_tx_block_height(txid).await
366    }
367
368    async fn get_block_count(&self) -> u64 {
369        self.inner.get_block_count().await
370    }
371
372    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
373        self.inner.get_mempool_tx(txid).await
374    }
375}