fedimint_testing/btc/
real.rs1use 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)]
26struct RealBitcoinTestNoLock {
27 client: Arc<Client>,
28 rpc: DynServerBitcoinRpc,
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 const BLOCK_NUM_LIMIT: u64 = 32;
52
53 if BLOCK_NUM_LIMIT < block_num {
54 warn!(
55 target: LOG_TEST,
56 %block_num,
57 "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."
58 );
59 let mut block_num = block_num;
60 let mut blocks = vec![];
61
62 loop {
63 if BLOCK_NUM_LIMIT < block_num {
64 block_num -= BLOCK_NUM_LIMIT;
65 blocks.append(
66 &mut Box::pin(async { self.mine_blocks(BLOCK_NUM_LIMIT).await }).await,
67 );
68 } else {
69 blocks.append(&mut Box::pin(async { self.mine_blocks(block_num).await }).await);
70 return blocks;
71 }
72 }
73 }
74
75 let new_address = self.get_new_address().await;
76 let mined_block_hashes = block_in_place(|| {
77 self.client
78 .generate_to_address(block_num, &new_address)
79 .expect(Self::ERROR)
80 });
81
82 if let Some(block_hash) = mined_block_hashes.last() {
83 let last_mined_block = block_in_place(|| {
84 self.client
85 .get_block_header_info(block_hash)
86 .expect("rpc failed")
87 });
88 let expected_block_count = last_mined_block.height as u64 + 1;
89 loop {
91 let current_block_count = self.rpc.get_block_count().await.expect("rpc failed");
92 if current_block_count < expected_block_count {
93 debug!(
94 target: LOG_TEST,
95 %block_num,
96 %expected_block_count,
97 %current_block_count,
98 "Waiting for blocks to be mined"
99 );
100 sleep_in_test("waiting for blocks to be mined", Duration::from_millis(200))
101 .await;
102 } else {
103 debug!(
104 target: LOG_TEST,
105 ?block_num,
106 %expected_block_count,
107 %current_block_count,
108 "Mined blocks"
109 );
110 break;
111 }
112 }
113 }
114
115 mined_block_hashes
116 }
117
118 async fn prepare_funding_wallet(&self) {
119 let block_count =
120 block_in_place(|| self.client.get_block_count().expect("should not fail"));
121 if block_count < 100 {
122 self.mine_blocks(100 - block_count).await;
123 }
124 }
125
126 async fn send_and_mine_block(
127 &self,
128 address: &Address,
129 amount: bitcoin::Amount,
130 ) -> (TxOutProof, Transaction) {
131 let id = block_in_place(|| {
132 self.client
133 .send_to_address(address, amount, None, None, None, None, None, None)
134 .expect(Self::ERROR)
135 });
136 let mined_block_hashes = self.mine_blocks(1).await;
137 let mined_block_hash = mined_block_hashes.first().expect("mined a block");
138
139 let tx = block_in_place(|| {
140 self.client
141 .get_raw_transaction(&id, Some(mined_block_hash))
142 .expect(Self::ERROR)
143 });
144 let proof = TxOutProof::consensus_decode_whole(
145 &loop {
146 match block_in_place(|| self.client.get_tx_out_proof(&[id], None)) {
147 Ok(o) => break o,
148 Err(e) => {
149 if e.to_string().contains("not yet in block") {
150 task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
152 continue;
153 }
154 panic!("Could not get txoutproof: {e}");
155 }
156 }
157 },
158 &ModuleDecoderRegistry::default(),
159 )
160 .expect(Self::ERROR);
161
162 (proof, tx)
163 }
164 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
165 self.mine_blocks(1).await;
166 block_in_place(|| {
167 self.client
168 .get_received_by_address(address, None)
169 .expect(Self::ERROR)
170 .into()
171 })
172 }
173
174 async fn get_new_address(&self) -> Address {
175 block_in_place(|| {
176 self.client
177 .get_new_address(None, None)
178 .expect(Self::ERROR)
179 .assume_checked()
180 })
181 }
182
183 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
184 loop {
185 if let Ok(tx) = block_in_place(|| self.client.get_mempool_entry(txid)) {
186 return tx.fees.base.into();
187 }
188
189 sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
190 }
191 }
192
193 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
194 let current_block_count = block_in_place(|| {
195 self.client
196 .get_block_count()
197 .expect("failed to fetch chain tip")
198 });
199 (0..=current_block_count)
200 .position(|height| {
201 block_in_place(|| {
202 let block_hash = self
203 .client
204 .get_block_hash(height)
205 .expect("failed to fetch block hash");
206
207 self.client
208 .get_block_info(&block_hash)
209 .expect("failed to fetch block info")
210 .tx
211 .iter()
212 .any(|id| id == txid)
213 })
214 })
215 .map(|height| height as u64)
216 }
217
218 async fn get_block_count(&self) -> u64 {
219 block_in_place(|| {
220 self.client
221 .get_block_count()
222 .map(|count| count + 1)
224 .expect("failed to fetch block count")
225 })
226 }
227
228 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
229 block_in_place(|| self.client.get_raw_transaction(txid, None).ok())
230 }
231}
232
233pub struct RealBitcoinTest {
238 inner: RealBitcoinTestNoLock,
239}
240
241impl RealBitcoinTest {
242 const ERROR: &'static str = "Bitcoin RPC returned an error";
243
244 pub fn new(url: &SafeUrl, rpc: DynServerBitcoinRpc) -> Self {
245 let auth = Auth::UserPass(
246 url.username().to_owned(),
247 url.password().unwrap().to_owned(),
248 );
249
250 let host = url.without_auth().unwrap().to_string();
251
252 let client = Arc::new(Client::new(&host, auth).expect(Self::ERROR));
253
254 Self {
255 inner: RealBitcoinTestNoLock { client, rpc },
256 }
257 }
258}
259
260pub struct RealBitcoinTestLocked {
263 inner: RealBitcoinTestNoLock,
264 _guard: fs_lock::FileLock,
265}
266
267#[async_trait]
268impl BitcoinTest for RealBitcoinTest {
269 async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
270 trace!("Trying to acquire global bitcoin lock");
271 let _guard = block_in_place(|| {
272 let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
273 fs_lock::FileLock::new_exclusive(
274 std::fs::OpenOptions::new()
275 .write(true)
276 .create(true)
277 .truncate(true)
278 .open(&lock_file_path)
279 .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
280 )
281 .context("Failed to acquire exclusive lock file")
282 })
283 .expect("Failed to lock");
284 trace!("Acquired global bitcoin lock");
285 Box::new(RealBitcoinTestLocked {
286 inner: self.inner.clone(),
287 _guard,
288 })
289 }
290
291 async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
292 let _lock = self.lock_exclusive().await;
293 self.inner.mine_blocks(block_num).await
294 }
295
296 async fn prepare_funding_wallet(&self) {
297 let _lock = self.lock_exclusive().await;
298 self.inner.prepare_funding_wallet().await;
299 }
300
301 async fn send_and_mine_block(
302 &self,
303 address: &Address,
304 amount: bitcoin::Amount,
305 ) -> (TxOutProof, Transaction) {
306 let _lock = self.lock_exclusive().await;
307 self.inner.send_and_mine_block(address, amount).await
308 }
309
310 async fn get_new_address(&self) -> Address {
311 let _lock = self.lock_exclusive().await;
312 self.inner.get_new_address().await
313 }
314
315 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
316 let _lock = self.lock_exclusive().await;
317 self.inner.mine_block_and_get_received(address).await
318 }
319
320 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
321 let _lock = self.lock_exclusive().await;
322 self.inner.get_mempool_tx_fee(txid).await
323 }
324
325 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
326 let _lock = self.lock_exclusive().await;
327 self.inner.get_tx_block_height(txid).await
328 }
329
330 async fn get_block_count(&self) -> u64 {
331 let _lock = self.lock_exclusive().await;
332 self.inner.get_block_count().await
333 }
334
335 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
336 let _lock = self.lock_exclusive().await;
337 self.inner.get_mempool_tx(txid).await
338 }
339}
340
341#[async_trait]
342impl BitcoinTest for RealBitcoinTestLocked {
343 async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
344 panic!("Double-locking would lead to a hang");
345 }
346
347 async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
348 let pre = block_in_place(|| self.inner.client.get_block_count().unwrap());
349 let mined_block_hashes = self.inner.mine_blocks(block_num).await;
350 let post = block_in_place(|| self.inner.client.get_block_count().unwrap());
351 assert_eq!(post - pre, block_num);
352 mined_block_hashes
353 }
354
355 async fn prepare_funding_wallet(&self) {
356 self.inner.prepare_funding_wallet().await;
357 }
358
359 async fn send_and_mine_block(
360 &self,
361 address: &Address,
362 amount: bitcoin::Amount,
363 ) -> (TxOutProof, Transaction) {
364 self.inner.send_and_mine_block(address, amount).await
365 }
366
367 async fn get_new_address(&self) -> Address {
368 self.inner.get_new_address().await
369 }
370
371 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
372 self.inner.mine_block_and_get_received(address).await
373 }
374
375 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
376 self.inner.get_mempool_tx_fee(txid).await
377 }
378
379 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
380 self.inner.get_tx_block_height(txid).await
381 }
382
383 async fn get_block_count(&self) -> u64 {
384 self.inner.get_block_count().await
385 }
386
387 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
388 self.inner.get_mempool_tx(txid).await
389 }
390}