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