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