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 }