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_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 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 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 .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
324pub 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
351pub 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}