/ fedimint-testing / src / btc / real.rs
real.rs
  1  use std::io::Cursor;
  2  use std::sync::Arc;
  3  use std::time::Duration;
  4  
  5  use anyhow::Context;
  6  use async_trait::async_trait;
  7  use bitcoin::{Address, Transaction, Txid};
  8  use bitcoincore_rpc::{Client, RpcApi};
  9  use fedimint_bitcoind::DynBitcoindRpc;
 10  use fedimint_core::encoding::Decodable;
 11  use fedimint_core::module::registry::ModuleDecoderRegistry;
 12  use fedimint_core::task::{block_in_place, sleep_in_test};
 13  use fedimint_core::txoproof::TxOutProof;
 14  use fedimint_core::util::SafeUrl;
 15  use fedimint_core::{task, Amount};
 16  use fedimint_logging::LOG_TEST;
 17  use lazy_static::lazy_static;
 18  use tracing::{debug, trace};
 19  
 20  use crate::btc::BitcoinTest;
 21  
 22  lazy_static! {
 23      /// Global lock we use to isolate tests that need exclusive control over shared `bitcoind`
 24      static ref REAL_BITCOIN_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::new(());
 25  }
 26  
 27  /// Fixture implementing bitcoin node under test by talking to a `bitcoind` with
 28  /// no locking considerations.
 29  ///
 30  /// This function assumes the caller already took care of locking
 31  /// considerations).
 32  #[derive(Clone)]
 33  struct RealBitcoinTestNoLock {
 34      client: Arc<Client>,
 35      /// RPC used to connect to bitcoind, used for waiting for the RPC to sync
 36      rpc: DynBitcoindRpc,
 37  }
 38  
 39  impl RealBitcoinTestNoLock {
 40      const ERROR: &'static str = "Bitcoin RPC returned an error";
 41  }
 42  
 43  #[async_trait]
 44  impl BitcoinTest for RealBitcoinTestNoLock {
 45      async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
 46          unimplemented!(
 47              "You should never try to lock `RealBitcoinTestNoLock`. Lock `RealBitcoinTest` instead"
 48          )
 49      }
 50  
 51      async fn mine_blocks(&self, block_num: u64) {
 52          if let Some(block_hash) = self
 53              .client
 54              .generate_to_address(block_num, &self.get_new_address().await)
 55              .expect(Self::ERROR)
 56              .last()
 57          {
 58              let last_mined_block = self
 59                  .client
 60                  .get_block_header_info(block_hash)
 61                  .expect("rpc failed");
 62              let expected_block_count = last_mined_block.height as u64 + 1;
 63              // waits for the rpc client to catch up to bitcoind
 64              loop {
 65                  let current_block_count = self.rpc.get_block_count().await.expect("rpc failed");
 66                  if current_block_count < expected_block_count {
 67                      debug!(
 68                          target: LOG_TEST,
 69                          ?block_num,
 70                          ?expected_block_count,
 71                          ?current_block_count,
 72                          "Waiting for blocks to be mined"
 73                      );
 74                      sleep_in_test("waiting for blocks to be mined", Duration::from_millis(200))
 75                          .await;
 76                  } else {
 77                      debug!(
 78                          target: LOG_TEST,
 79                          ?block_num,
 80                          ?expected_block_count,
 81                          ?current_block_count,
 82                          "Mined blocks"
 83                      );
 84                      break;
 85                  }
 86              }
 87          };
 88      }
 89  
 90      async fn prepare_funding_wallet(&self) {
 91          let block_count = self.client.get_block_count().expect("should not fail");
 92          if block_count < 100 {
 93              self.mine_blocks(100 - block_count).await;
 94          }
 95      }
 96  
 97      async fn send_and_mine_block(
 98          &self,
 99          address: &Address,
100          amount: bitcoin::Amount,
101      ) -> (TxOutProof, Transaction) {
102          let id = self
103              .client
104              .send_to_address(address, amount, None, None, None, None, None, None)
105              .expect(Self::ERROR);
106          self.mine_blocks(1).await;
107  
108          let tx = self
109              .client
110              .get_raw_transaction(&id, None)
111              .expect(Self::ERROR);
112          let proof = TxOutProof::consensus_decode(
113              &mut Cursor::new(loop {
114                  match self.client.get_tx_out_proof(&[id], None) {
115                      Ok(o) => break o,
116                      Err(e) => {
117                          if e.to_string().contains("not yet in block") {
118                              // mostly to yield, as we no other yield points
119                              task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
120                              continue;
121                          }
122                          panic!("Could not get txoutproof: {e}");
123                      }
124                  }
125              }),
126              &ModuleDecoderRegistry::default(),
127          )
128          .expect(Self::ERROR);
129  
130          (proof, tx)
131      }
132      async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
133          self.mine_blocks(1).await;
134          self.client
135              .get_received_by_address(address, None)
136              .expect(Self::ERROR)
137              .into()
138      }
139  
140      async fn get_new_address(&self) -> Address {
141          self.client
142              .get_new_address(None, None)
143              .expect(Self::ERROR)
144              .assume_checked()
145      }
146  
147      async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
148          loop {
149              match self.client.get_mempool_entry(txid) {
150                  Ok(tx) => return tx.fees.base.into(),
151                  Err(_) => {
152                      sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
153                      continue;
154                  }
155              }
156          }
157      }
158  }
159  
160  /// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
161  /// unlocked version (lock each call separately)
162  ///
163  /// Default version (and thus the only one with `new`)
164  pub struct RealBitcoinTest {
165      inner: RealBitcoinTestNoLock,
166  }
167  
168  impl RealBitcoinTest {
169      const ERROR: &'static str = "Bitcoin RPC returned an error";
170  
171      pub fn new(url: &SafeUrl, rpc: DynBitcoindRpc) -> Self {
172          let (host, auth) =
173              fedimint_bitcoind::bitcoincore::from_url_to_url_auth(url).expect("correct url");
174          let client = Arc::new(Client::new(&host, auth).expect(Self::ERROR));
175  
176          Self {
177              inner: RealBitcoinTestNoLock { client, rpc },
178          }
179      }
180  }
181  
182  /// Fixture implementing bitcoin node under test by talking to a `bitcoind` -
183  /// locked version - locks the global lock during construction
184  pub struct RealBitcoinTestLocked {
185      inner: RealBitcoinTestNoLock,
186      _guard: fs_lock::FileLock,
187  }
188  
189  #[async_trait]
190  impl BitcoinTest for RealBitcoinTest {
191      async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
192          trace!("Trying to acquire global bitcoin lock");
193          let _guard = block_in_place(|| {
194              let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
195              fs_lock::FileLock::new_exclusive(
196                  std::fs::OpenOptions::new()
197                      .write(true)
198                      .create(true)
199                      .truncate(true)
200                      .open(&lock_file_path)
201                      .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
202              )
203              .context("Failed to acquire exclusive lock file")
204          })
205          .expect("Failed to lock");
206          trace!("Acquired global bitcoin lock");
207          Box::new(RealBitcoinTestLocked {
208              inner: self.inner.clone(),
209              _guard,
210          })
211      }
212  
213      async fn mine_blocks(&self, block_num: u64) {
214          let _lock = self.lock_exclusive().await;
215          self.inner.mine_blocks(block_num).await;
216      }
217  
218      async fn prepare_funding_wallet(&self) {
219          let _lock = self.lock_exclusive().await;
220          self.inner.prepare_funding_wallet().await;
221      }
222  
223      async fn send_and_mine_block(
224          &self,
225          address: &Address,
226          amount: bitcoin::Amount,
227      ) -> (TxOutProof, Transaction) {
228          let _lock = self.lock_exclusive().await;
229          self.inner.send_and_mine_block(address, amount).await
230      }
231  
232      async fn get_new_address(&self) -> Address {
233          let _lock = self.lock_exclusive().await;
234          self.inner.get_new_address().await
235      }
236  
237      async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
238          let _lock = self.lock_exclusive().await;
239          self.inner.mine_block_and_get_received(address).await
240      }
241  
242      async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
243          let _lock = self.lock_exclusive().await;
244          self.inner.get_mempool_tx_fee(txid).await
245      }
246  }
247  
248  #[async_trait]
249  impl BitcoinTest for RealBitcoinTestLocked {
250      async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
251          panic!("Double-locking would lead to a hang");
252      }
253  
254      async fn mine_blocks(&self, block_num: u64) {
255          let pre = self.inner.client.get_block_count().unwrap();
256          self.inner.mine_blocks(block_num).await;
257          let post = self.inner.client.get_block_count().unwrap();
258          assert_eq!(post - pre, block_num);
259      }
260  
261      async fn prepare_funding_wallet(&self) {
262          self.inner.prepare_funding_wallet().await;
263      }
264  
265      async fn send_and_mine_block(
266          &self,
267          address: &Address,
268          amount: bitcoin::Amount,
269      ) -> (TxOutProof, Transaction) {
270          self.inner.send_and_mine_block(address, amount).await
271      }
272  
273      async fn get_new_address(&self) -> Address {
274          self.inner.get_new_address().await
275      }
276  
277      async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
278          self.inner.mine_block_and_get_received(address).await
279      }
280  
281      async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
282          self.inner.get_mempool_tx_fee(txid).await
283      }
284  }