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#[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#[derive(Clone)]
119struct RealBitcoinTestNoLock {
120 client: BitcoinClient,
121 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 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 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_and_mine_block(
217 &self,
218 address: &Address,
219 amount: bitcoin::Amount,
220 ) -> (TxOutProof, Transaction) {
221 let id = self
222 .client
223 .send_to_address(address, amount)
224 .expect(Self::ERROR);
225 let mined_block_hashes = self.mine_blocks(1).await;
226 let mined_block_hash = mined_block_hashes.first().expect("mined a block");
227
228 let tx = self
229 .client
230 .get_raw_transaction(&id, Some(mined_block_hash))
231 .expect(Self::ERROR);
232 let proof = TxOutProof::consensus_decode_whole(
233 &loop {
234 match self.client.get_tx_out_proof(&[id], None) {
235 Ok(o) => break o,
236 Err(e) => {
237 if e.to_string().contains("not yet in block") {
238 task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
240 continue;
241 }
242 panic!("Could not get txoutproof: {e}");
243 }
244 }
245 },
246 &ModuleDecoderRegistry::default(),
247 )
248 .expect(Self::ERROR);
249
250 (proof, tx)
251 }
252 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
253 self.mine_blocks(1).await;
254 self.client
255 .get_received_by_address(address, None)
256 .expect(Self::ERROR)
257 .into()
258 }
259
260 async fn get_new_address(&self) -> Address {
261 self.client
262 .get_new_address(None, None)
263 .expect(Self::ERROR)
264 .assume_checked()
265 }
266
267 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
268 loop {
269 if let Ok(tx) = self.client.get_mempool_entry(txid) {
270 return tx.fees.base.into();
271 }
272
273 sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
274 }
275 }
276
277 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
278 let current_block_count = self
279 .client
280 .get_block_count()
281 .expect("failed to fetch chain tip");
282 (0..=current_block_count)
283 .position(|height| {
284 let block_hash = self
285 .client
286 .get_block_hash(height)
287 .expect("failed to fetch block hash");
288
289 self.client
290 .get_block_info(&block_hash)
291 .expect("failed to fetch block info")
292 .tx
293 .iter()
294 .any(|id| id == txid)
295 })
296 .map(|height| height as u64)
297 }
298
299 async fn get_block_count(&self) -> u64 {
300 self.client
301 .get_block_count()
302 .map(|count| count + 1)
304 .expect("failed to fetch block count")
305 }
306
307 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
308 self.client.get_raw_transaction(txid, None).ok()
309 }
310}
311
312pub struct RealBitcoinTest {
317 inner: RealBitcoinTestNoLock,
318}
319
320impl RealBitcoinTest {
321 const ERROR: &'static str = "Bitcoin RPC returned an error";
322
323 pub fn new(url: &SafeUrl, rpc: DynServerBitcoinRpc) -> Self {
324 let auth = Auth::UserPass(
325 url.username().to_owned(),
326 url.password().unwrap().to_owned(),
327 );
328
329 let host = url.without_auth().unwrap().to_string();
330
331 let client = BitcoinClient::new(Arc::new(Client::new(&host, auth).expect(Self::ERROR)));
332
333 Self {
334 inner: RealBitcoinTestNoLock { client, rpc },
335 }
336 }
337}
338
339pub struct RealBitcoinTestLocked {
342 inner: RealBitcoinTestNoLock,
343 _guard: fs_lock::FileLock,
344}
345
346#[async_trait]
347impl BitcoinTest for RealBitcoinTest {
348 async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
349 trace!("Trying to acquire global bitcoin lock");
350 let _guard = block_in_place(|| {
351 let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
352 fs_lock::FileLock::new_exclusive(
353 std::fs::OpenOptions::new()
354 .write(true)
355 .create(true)
356 .truncate(true)
357 .open(&lock_file_path)
358 .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
359 )
360 .context("Failed to acquire exclusive lock file")
361 })
362 .expect("Failed to lock");
363 trace!("Acquired global bitcoin lock");
364 Box::new(RealBitcoinTestLocked {
365 inner: self.inner.clone(),
366 _guard,
367 })
368 }
369
370 async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
371 let _lock = self.lock_exclusive().await;
372 self.inner.mine_blocks(block_num).await
373 }
374
375 async fn prepare_funding_wallet(&self) {
376 let _lock = self.lock_exclusive().await;
377 self.inner.prepare_funding_wallet().await;
378 }
379
380 async fn send_and_mine_block(
381 &self,
382 address: &Address,
383 amount: bitcoin::Amount,
384 ) -> (TxOutProof, Transaction) {
385 let _lock = self.lock_exclusive().await;
386 self.inner.send_and_mine_block(address, amount).await
387 }
388
389 async fn get_new_address(&self) -> Address {
390 let _lock = self.lock_exclusive().await;
391 self.inner.get_new_address().await
392 }
393
394 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
395 let _lock = self.lock_exclusive().await;
396 self.inner.mine_block_and_get_received(address).await
397 }
398
399 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
400 let _lock = self.lock_exclusive().await;
401 self.inner.get_mempool_tx_fee(txid).await
402 }
403
404 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
405 let _lock = self.lock_exclusive().await;
406 self.inner.get_tx_block_height(txid).await
407 }
408
409 async fn get_block_count(&self) -> u64 {
410 let _lock = self.lock_exclusive().await;
411 self.inner.get_block_count().await
412 }
413
414 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
415 let _lock = self.lock_exclusive().await;
416 self.inner.get_mempool_tx(txid).await
417 }
418}
419
420#[async_trait]
421impl BitcoinTest for RealBitcoinTestLocked {
422 async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
423 panic!("Double-locking would lead to a hang");
424 }
425
426 async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
427 let pre = self.inner.client.get_block_count().unwrap();
428 let mined_block_hashes = self.inner.mine_blocks(block_num).await;
429 let post = self.inner.client.get_block_count().unwrap();
430 assert_eq!(post - pre, block_num);
431 mined_block_hashes
432 }
433
434 async fn prepare_funding_wallet(&self) {
435 self.inner.prepare_funding_wallet().await;
436 }
437
438 async fn send_and_mine_block(
439 &self,
440 address: &Address,
441 amount: bitcoin::Amount,
442 ) -> (TxOutProof, Transaction) {
443 self.inner.send_and_mine_block(address, amount).await
444 }
445
446 async fn get_new_address(&self) -> Address {
447 self.inner.get_new_address().await
448 }
449
450 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
451 self.inner.mine_block_and_get_received(address).await
452 }
453
454 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
455 self.inner.get_mempool_tx_fee(txid).await
456 }
457
458 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
459 self.inner.get_tx_block_height(txid).await
460 }
461
462 async fn get_block_count(&self) -> u64 {
463 self.inner.get_block_count().await
464 }
465
466 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
467 self.inner.get_mempool_tx(txid).await
468 }
469}