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::{Client, RpcApi};
8use fedimint_bitcoind::DynBitcoindRpc;
9use fedimint_core::encoding::Decodable;
10use fedimint_core::module::registry::ModuleDecoderRegistry;
11use fedimint_core::task::{block_in_place, sleep_in_test};
12use fedimint_core::txoproof::TxOutProof;
13use fedimint_core::util::SafeUrl;
14use fedimint_core::{Amount, task};
15use fedimint_logging::LOG_TEST;
16use tracing::{debug, trace};
17
18use crate::btc::BitcoinTest;
19
20#[derive(Clone)]
26struct RealBitcoinTestNoLock {
27 client: Arc<Client>,
28 rpc: DynBitcoindRpc,
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 let mined_block_hashes = self
46 .client
47 .generate_to_address(block_num, &self.get_new_address().await)
48 .expect(Self::ERROR);
49
50 if let Some(block_hash) = mined_block_hashes.last() {
51 let last_mined_block = self
52 .client
53 .get_block_header_info(block_hash)
54 .expect("rpc failed");
55 let expected_block_count = last_mined_block.height as u64 + 1;
56 loop {
58 let current_block_count = self.rpc.get_block_count().await.expect("rpc failed");
59 if current_block_count < expected_block_count {
60 debug!(
61 target: LOG_TEST,
62 ?block_num,
63 ?expected_block_count,
64 ?current_block_count,
65 "Waiting for blocks to be mined"
66 );
67 sleep_in_test("waiting for blocks to be mined", Duration::from_millis(200))
68 .await;
69 } else {
70 debug!(
71 target: LOG_TEST,
72 ?block_num,
73 ?expected_block_count,
74 ?current_block_count,
75 "Mined blocks"
76 );
77 break;
78 }
79 }
80 };
81
82 mined_block_hashes
83 }
84
85 async fn prepare_funding_wallet(&self) {
86 let block_count = self.client.get_block_count().expect("should not fail");
87 if block_count < 100 {
88 self.mine_blocks(100 - block_count).await;
89 }
90 }
91
92 async fn send_and_mine_block(
93 &self,
94 address: &Address,
95 amount: bitcoin::Amount,
96 ) -> (TxOutProof, Transaction) {
97 let id = self
98 .client
99 .send_to_address(address, amount, None, None, None, None, None, None)
100 .expect(Self::ERROR);
101 let mined_block_hashes = self.mine_blocks(1).await;
102 let mined_block_hash = mined_block_hashes.first().expect("mined a block");
103
104 let tx = self
105 .client
106 .get_raw_transaction(&id, Some(mined_block_hash))
107 .expect(Self::ERROR);
108 let proof = TxOutProof::consensus_decode_whole(
109 &loop {
110 match self.client.get_tx_out_proof(&[id], None) {
111 Ok(o) => break o,
112 Err(e) => {
113 if e.to_string().contains("not yet in block") {
114 task::sleep_in_test("not yet in block", Duration::from_millis(1)).await;
116 continue;
117 }
118 panic!("Could not get txoutproof: {e}");
119 }
120 }
121 },
122 &ModuleDecoderRegistry::default(),
123 )
124 .expect(Self::ERROR);
125
126 (proof, tx)
127 }
128 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
129 self.mine_blocks(1).await;
130 self.client
131 .get_received_by_address(address, None)
132 .expect(Self::ERROR)
133 .into()
134 }
135
136 async fn get_new_address(&self) -> Address {
137 self.client
138 .get_new_address(None, None)
139 .expect(Self::ERROR)
140 .assume_checked()
141 }
142
143 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
144 loop {
145 if let Ok(tx) = self.client.get_mempool_entry(txid) {
146 return tx.fees.base.into();
147 }
148
149 sleep_in_test("could not get mempool tx fee", Duration::from_millis(100)).await;
150 }
151 }
152
153 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
154 let current_block_count = self
155 .client
156 .get_block_count()
157 .expect("failed to fetch chain tip");
158 (0..=current_block_count)
159 .position(|height| {
160 let block_hash = self
161 .client
162 .get_block_hash(height)
163 .expect("failed to fetch block hash");
164
165 self.client
166 .get_block_info(&block_hash)
167 .expect("failed to fetch block info")
168 .tx
169 .iter()
170 .any(|id| id == txid)
171 })
172 .map(|height| height as u64)
173 }
174
175 async fn get_block_count(&self) -> u64 {
176 self.client
177 .get_block_count()
178 .map(|count| count + 1)
180 .expect("failed to fetch block count")
181 }
182
183 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
184 self.client.get_raw_transaction(txid, None).ok()
185 }
186}
187
188pub struct RealBitcoinTest {
193 inner: RealBitcoinTestNoLock,
194}
195
196impl RealBitcoinTest {
197 const ERROR: &'static str = "Bitcoin RPC returned an error";
198
199 pub fn new(url: &SafeUrl, rpc: DynBitcoindRpc) -> Self {
200 let (host, auth) =
201 fedimint_bitcoind::bitcoincore::from_url_to_url_auth(url).expect("correct url");
202 let client = Arc::new(Client::new(&host, auth).expect(Self::ERROR));
203
204 Self {
205 inner: RealBitcoinTestNoLock { client, rpc },
206 }
207 }
208}
209
210pub struct RealBitcoinTestLocked {
213 inner: RealBitcoinTestNoLock,
214 _guard: fs_lock::FileLock,
215}
216
217#[async_trait]
218impl BitcoinTest for RealBitcoinTest {
219 async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
220 trace!("Trying to acquire global bitcoin lock");
221 let _guard = block_in_place(|| {
222 let lock_file_path = std::env::temp_dir().join("fm-test-bitcoind-lock");
223 fs_lock::FileLock::new_exclusive(
224 std::fs::OpenOptions::new()
225 .write(true)
226 .create(true)
227 .truncate(true)
228 .open(&lock_file_path)
229 .with_context(|| format!("Failed to open {}", lock_file_path.display()))?,
230 )
231 .context("Failed to acquire exclusive lock file")
232 })
233 .expect("Failed to lock");
234 trace!("Acquired global bitcoin lock");
235 Box::new(RealBitcoinTestLocked {
236 inner: self.inner.clone(),
237 _guard,
238 })
239 }
240
241 async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
242 let _lock = self.lock_exclusive().await;
243 self.inner.mine_blocks(block_num).await
244 }
245
246 async fn prepare_funding_wallet(&self) {
247 let _lock = self.lock_exclusive().await;
248 self.inner.prepare_funding_wallet().await;
249 }
250
251 async fn send_and_mine_block(
252 &self,
253 address: &Address,
254 amount: bitcoin::Amount,
255 ) -> (TxOutProof, Transaction) {
256 let _lock = self.lock_exclusive().await;
257 self.inner.send_and_mine_block(address, amount).await
258 }
259
260 async fn get_new_address(&self) -> Address {
261 let _lock = self.lock_exclusive().await;
262 self.inner.get_new_address().await
263 }
264
265 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
266 let _lock = self.lock_exclusive().await;
267 self.inner.mine_block_and_get_received(address).await
268 }
269
270 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
271 let _lock = self.lock_exclusive().await;
272 self.inner.get_mempool_tx_fee(txid).await
273 }
274
275 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
276 let _lock = self.lock_exclusive().await;
277 self.inner.get_tx_block_height(txid).await
278 }
279
280 async fn get_block_count(&self) -> u64 {
281 let _lock = self.lock_exclusive().await;
282 self.inner.get_block_count().await
283 }
284
285 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
286 let _lock = self.lock_exclusive().await;
287 self.inner.get_mempool_tx(txid).await
288 }
289}
290
291#[async_trait]
292impl BitcoinTest for RealBitcoinTestLocked {
293 async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
294 panic!("Double-locking would lead to a hang");
295 }
296
297 async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
298 let pre = self.inner.client.get_block_count().unwrap();
299 let mined_block_hashes = self.inner.mine_blocks(block_num).await;
300 let post = self.inner.client.get_block_count().unwrap();
301 assert_eq!(post - pre, block_num);
302 mined_block_hashes
303 }
304
305 async fn prepare_funding_wallet(&self) {
306 self.inner.prepare_funding_wallet().await;
307 }
308
309 async fn send_and_mine_block(
310 &self,
311 address: &Address,
312 amount: bitcoin::Amount,
313 ) -> (TxOutProof, Transaction) {
314 self.inner.send_and_mine_block(address, amount).await
315 }
316
317 async fn get_new_address(&self) -> Address {
318 self.inner.get_new_address().await
319 }
320
321 async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
322 self.inner.mine_block_and_get_received(address).await
323 }
324
325 async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
326 self.inner.get_mempool_tx_fee(txid).await
327 }
328
329 async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
330 self.inner.get_tx_block_height(txid).await
331 }
332
333 async fn get_block_count(&self) -> u64 {
334 self.inner.get_block_count().await
335 }
336
337 async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
338 self.inner.get_mempool_tx(txid).await
339 }
340}