Skip to main content

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/// Newtype wrapper around bitcoincore_rpc::Client that automatically wraps all
21/// calls in `block_in_place` to ensure they don't block the async runtime.
22#[derive(Clone)]
23struct BitcoinClient {
24    inner: Arc<Client>,
25}
26
27impl BitcoinClient {
28    fn new(client: Arc<Client>) -> Self {
29        Self { inner: client }
30    }
31
32    fn generate_to_address(
33        &self,
34        block_num: u64,
35        address: &Address,
36    ) -> Result<Vec<bitcoin::BlockHash>, bitcoincore_rpc::Error> {
37        block_in_place(|| self.inner.generate_to_address(block_num, address))
38    }
39
40    fn get_block_header_info(
41        &self,
42        hash: &bitcoin::BlockHash,
43    ) -> Result<bitcoincore_rpc::json::GetBlockHeaderResult, bitcoincore_rpc::Error> {
44        block_in_place(|| self.inner.get_block_header_info(hash))
45    }
46
47    fn get_block_count(&self) -> Result<u64, bitcoincore_rpc::Error> {
48        block_in_place(|| self.inner.get_block_count())
49    }
50
51    fn send_to_address(
52        &self,
53        address: &Address,
54        amount: bitcoin::Amount,
55    ) -> Result<Txid, bitcoincore_rpc::Error> {
56        block_in_place(|| {
57            self.inner
58                .send_to_address(address, amount, None, None, None, None, None, None)
59        })
60    }
61
62    fn get_raw_transaction(
63        &self,
64        txid: &Txid,
65        block_hash: Option<&bitcoin::BlockHash>,
66    ) -> Result<Transaction, bitcoincore_rpc::Error> {
67        block_in_place(|| self.inner.get_raw_transaction(txid, block_hash))
68    }
69
70    fn get_tx_out_proof(
71        &self,
72        txids: &[Txid],
73        block_hash: Option<&bitcoin::BlockHash>,
74    ) -> Result<Vec<u8>, bitcoincore_rpc::Error> {
75        block_in_place(|| self.inner.get_tx_out_proof(txids, block_hash))
76    }
77
78    fn get_received_by_address(
79        &self,
80        address: &Address,
81        minconf: Option<u32>,
82    ) -> Result<bitcoin::Amount, bitcoincore_rpc::Error> {
83        block_in_place(|| self.inner.get_received_by_address(address, minconf))
84    }
85
86    fn get_new_address(
87        &self,
88        label: Option<&str>,
89        address_type: Option<bitcoincore_rpc::json::AddressType>,
90    ) -> Result<bitcoin::Address<bitcoin::address::NetworkUnchecked>, bitcoincore_rpc::Error> {
91        block_in_place(|| self.inner.get_new_address(label, address_type))
92    }
93
94    fn get_mempool_entry(
95        &self,
96        txid: &Txid,
97    ) -> Result<bitcoincore_rpc::json::GetMempoolEntryResult, bitcoincore_rpc::Error> {
98        block_in_place(|| self.inner.get_mempool_entry(txid))
99    }
100
101    fn get_block_hash(&self, height: u64) -> Result<bitcoin::BlockHash, bitcoincore_rpc::Error> {
102        block_in_place(|| self.inner.get_block_hash(height))
103    }
104
105    fn get_block_info(
106        &self,
107        hash: &bitcoin::BlockHash,
108    ) -> Result<bitcoincore_rpc::json::GetBlockResult, bitcoincore_rpc::Error> {
109        block_in_place(|| self.inner.get_block_info(hash))
110    }
111}
112
113/// Fixture implementing bitcoin node under test by talking to a `bitcoind` with
114/// no locking considerations.
115///
116/// This function assumes the caller already took care of locking
117/// considerations).
118#[derive(Clone)]
119struct RealBitcoinTestNoLock {
120    client: BitcoinClient,
121    /// RPC used to connect to bitcoind, used for waiting for the RPC to sync
122    rpc: DynServerBitcoinRpc,
123}
124
125impl RealBitcoinTestNoLock {
126    const ERROR: &'static str = "Bitcoin RPC returned an error";
127}
128
129#[async_trait]
130impl BitcoinTest for RealBitcoinTestNoLock {
131    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
132        unimplemented!(
133            "You should never try to lock `RealBitcoinTestNoLock`. Lock `RealBitcoinTest` instead"
134        )
135    }
136
137    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
138        // `Stdio::piped` can't even parse outputs larger
139        // than pipe buffer size (64K on Linux, 16K on MacOS), so
140        // we should split larger requesteds into smaller chunks.
141        //
142        // On top of it mining a lot of blocks is just slow, so should
143        // be avoided.
144        const BLOCK_NUM_LIMIT: u64 = 32;
145
146        if BLOCK_NUM_LIMIT < block_num {
147            warn!(
148                target: LOG_TEST,
149                %block_num,
150                "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."
151            );
152            let mut block_num = block_num;
153            let mut blocks = vec![];
154
155            loop {
156                if BLOCK_NUM_LIMIT < block_num {
157                    block_num -= BLOCK_NUM_LIMIT;
158                    blocks.append(
159                        &mut Box::pin(async { self.mine_blocks(BLOCK_NUM_LIMIT).await }).await,
160                    );
161                } else {
162                    blocks.append(&mut Box::pin(async { self.mine_blocks(block_num).await }).await);
163                    return blocks;
164                }
165            }
166        }
167
168        let new_address = self.get_new_address().await;
169        let mined_block_hashes = self
170            .client
171            .generate_to_address(block_num, &new_address)
172            .expect(Self::ERROR);
173
174        if let Some(block_hash) = mined_block_hashes.last() {
175            let last_mined_block = self
176                .client
177                .get_block_header_info(block_hash)
178                .expect("rpc failed");
179            let expected_block_count = last_mined_block.height as u64 + 1;
180            // waits for the rpc client to catch up to bitcoind
181            loop {
182                let current_block_count = self.rpc.get_block_count().await.expect("rpc failed");
183                if current_block_count < expected_block_count {
184                    debug!(
185                        target: LOG_TEST,
186                        %block_num,
187                        %expected_block_count,
188                        %current_block_count,
189                        "Waiting for blocks to be mined"
190                    );
191                    sleep_in_test("waiting for blocks to be mined", Duration::from_millis(200))
192                        .await;
193                } else {
194                    debug!(
195                        target: LOG_TEST,
196                        ?block_num,
197                        %expected_block_count,
198                        %current_block_count,
199                        "Mined blocks"
200                    );
201                    break;
202                }
203            }
204        }
205
206        mined_block_hashes
207    }
208
209    async fn prepare_funding_wallet(&self) {
210        let block_count = self.client.get_block_count().expect("should not fail");
211        if block_count < 100 {
212            self.mine_blocks(100 - block_count).await;
213        }
214    }
215
216    async fn send_without_mining(&self, address: &Address, amount: bitcoin::Amount) -> Transaction {
217        let id = self
218            .client
219            .send_to_address(address, amount)
220            .expect(Self::ERROR);
221
222        // No block is mined, so this resolves out of the mempool.
223        self.client
224            .get_raw_transaction(&id, None)
225            .expect(Self::ERROR)
226    }
227
228    async fn send_and_mine_block(
229        &self,
230        address: &Address,
231        amount: bitcoin::Amount,
232    ) -> (TxOutProof, Transaction) {
233        let id = self
234            .client
235            .send_to_address(address, amount)
236            .expect(Self::ERROR);
237        let mined_block_hashes = self.mine_blocks(1).await;
238        let mined_block_hash = mined_block_hashes.first().expect("mined a block");
239
240        let tx = self
241            .client
242            .get_raw_transaction(&id, Some(mined_block_hash))
243            .expect(Self::ERROR);
244        let proof = TxOutProof::consensus_decode_whole(
245            &loop {
246                match self.client.get_tx_out_proof(&[id], None) {
247                    Ok(o) => break o,
248                    Err(e) => {
249                        if e.to_string().contains("not yet in block") {
250                            // mostly to yield, as we no other yield points
251                            task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
252                            continue;
253                        }
254                        panic!("Could not get txoutproof: {e}");
255                    }
256                }
257            },
258            &ModuleDecoderRegistry::default(),
259        )
260        .expect(Self::ERROR);
261
262        (proof, tx)
263    }
264    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
265        self.mine_blocks(1).await;
266        self.client
267            .get_received_by_address(address, None)
268            .expect(Self::ERROR)
269            .into()
270    }
271
272    async fn get_new_address(&self) -> Address {
273        self.client
274            .get_new_address(None, None)
275            .expect(Self::ERROR)
276            .assume_checked()
277    }
278
279    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
280        loop {
281            if let Ok(tx) = self.client.get_mempool_entry(txid) {
282                return tx.fees.base.into();
283            }
284
285            sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
286        }
287    }
288
289    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
290        let current_block_count = self
291            .client
292            .get_block_count()
293            .expect("failed to fetch chain tip");
294        (0..=current_block_count)
295            .position(|height| {
296                let block_hash = self
297                    .client
298                    .get_block_hash(height)
299                    .expect("failed to fetch block hash");
300
301                self.client
302                    .get_block_info(&block_hash)
303                    .expect("failed to fetch block info")
304                    .tx
305                    .iter()
306                    .any(|id| id == txid)
307            })
308            .map(|height| height as u64)
309    }
310
311    async fn get_block_count(&self) -> u64 {
312        self.client
313            .get_block_count()
314            // The RPC function is confusingly named and actually returns the block height
315            .map(|count| count + 1)
316            .expect("failed to fetch block count")
317    }
318
319    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
320        self.client.get_raw_transaction(txid, None).ok()
321    }
322}
323
324/// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
325/// unlocked version (lock each call separately)
326///
327/// Default version (and thus the only one with `new`)
328pub struct RealBitcoinTest {
329    inner: RealBitcoinTestNoLock,
330}
331
332impl RealBitcoinTest {
333    const ERROR: &'static str = "Bitcoin RPC returned an error";
334
335    pub fn new(url: &SafeUrl, rpc: DynServerBitcoinRpc) -> Self {
336        let auth = Auth::UserPass(
337            url.username().to_owned(),
338            url.password().unwrap().to_owned(),
339        );
340
341        let host = url.without_auth().unwrap().to_string();
342
343        let client = BitcoinClient::new(Arc::new(Client::new(&host, auth).expect(Self::ERROR)));
344
345        Self {
346            inner: RealBitcoinTestNoLock { client, rpc },
347        }
348    }
349}
350
351/// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
352/// locked version - locks the global lock during construction
353pub struct RealBitcoinTestLocked {
354    inner: RealBitcoinTestNoLock,
355    _guard: fs_lock::FileLock,
356}
357
358#[async_trait]
359impl BitcoinTest for RealBitcoinTest {
360    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
361        trace!("Trying to acquire global bitcoin lock");
362        let _guard = block_in_place(|| {
363            let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
364            fs_lock::FileLock::new_exclusive(
365                std::fs::OpenOptions::new()
366                    .write(true)
367                    .create(true)
368                    .truncate(true)
369                    .open(&lock_file_path)
370                    .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
371            )
372            .context("Failed to acquire exclusive lock file")
373        })
374        .expect("Failed to lock");
375        trace!("Acquired global bitcoin lock");
376        Box::new(RealBitcoinTestLocked {
377            inner: self.inner.clone(),
378            _guard,
379        })
380    }
381
382    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
383        let _lock = self.lock_exclusive().await;
384        self.inner.mine_blocks(block_num).await
385    }
386
387    async fn prepare_funding_wallet(&self) {
388        let _lock = self.lock_exclusive().await;
389        self.inner.prepare_funding_wallet().await;
390    }
391
392    async fn send_without_mining(&self, address: &Address, amount: bitcoin::Amount) -> Transaction {
393        let _lock = self.lock_exclusive().await;
394        self.inner.send_without_mining(address, amount).await
395    }
396
397    async fn send_and_mine_block(
398        &self,
399        address: &Address,
400        amount: bitcoin::Amount,
401    ) -> (TxOutProof, Transaction) {
402        let _lock = self.lock_exclusive().await;
403        self.inner.send_and_mine_block(address, amount).await
404    }
405
406    async fn get_new_address(&self) -> Address {
407        let _lock = self.lock_exclusive().await;
408        self.inner.get_new_address().await
409    }
410
411    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
412        let _lock = self.lock_exclusive().await;
413        self.inner.mine_block_and_get_received(address).await
414    }
415
416    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
417        let _lock = self.lock_exclusive().await;
418        self.inner.get_mempool_tx_fee(txid).await
419    }
420
421    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
422        let _lock = self.lock_exclusive().await;
423        self.inner.get_tx_block_height(txid).await
424    }
425
426    async fn get_block_count(&self) -> u64 {
427        let _lock = self.lock_exclusive().await;
428        self.inner.get_block_count().await
429    }
430
431    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
432        let _lock = self.lock_exclusive().await;
433        self.inner.get_mempool_tx(txid).await
434    }
435}
436
437#[async_trait]
438impl BitcoinTest for RealBitcoinTestLocked {
439    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
440        panic!("Double-locking would lead to a hang");
441    }
442
443    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
444        let pre = self.inner.client.get_block_count().unwrap();
445        let mined_block_hashes = self.inner.mine_blocks(block_num).await;
446        let post = self.inner.client.get_block_count().unwrap();
447        assert_eq!(post - pre, block_num);
448        mined_block_hashes
449    }
450
451    async fn prepare_funding_wallet(&self) {
452        self.inner.prepare_funding_wallet().await;
453    }
454
455    async fn send_without_mining(&self, address: &Address, amount: bitcoin::Amount) -> Transaction {
456        self.inner.send_without_mining(address, amount).await
457    }
458
459    async fn send_and_mine_block(
460        &self,
461        address: &Address,
462        amount: bitcoin::Amount,
463    ) -> (TxOutProof, Transaction) {
464        self.inner.send_and_mine_block(address, amount).await
465    }
466
467    async fn get_new_address(&self) -> Address {
468        self.inner.get_new_address().await
469    }
470
471    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
472        self.inner.mine_block_and_get_received(address).await
473    }
474
475    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
476        self.inner.get_mempool_tx_fee(txid).await
477    }
478
479    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
480        self.inner.get_tx_block_height(txid).await
481    }
482
483    async fn get_block_count(&self) -> u64 {
484        self.inner.get_block_count().await
485    }
486
487    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
488        self.inner.get_mempool_tx(txid).await
489    }
490}