Skip to main content

fedimint_testing/btc/
mock.rs

1use std::collections::BTreeMap;
2use std::iter::repeat_n;
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result, bail};
7use async_trait::async_trait;
8use bitcoin::absolute::LockTime;
9use bitcoin::block::{Header as BlockHeader, Version};
10use bitcoin::constants::genesis_block;
11use bitcoin::hash_types::Txid;
12use bitcoin::hashes::Hash;
13use bitcoin::merkle_tree::PartialMerkleTree;
14use bitcoin::{
15    Address, Block, BlockHash, CompactTarget, Network, OutPoint, ScriptBuf, Transaction, TxOut,
16};
17use fedimint_bitcoind::{BitcoinRpcError, BlockchainInfo, IBitcoindRpc};
18use fedimint_core::envs::BitcoinRpcConfig;
19use fedimint_core::task::sleep_in_test;
20use fedimint_core::txoproof::TxOutProof;
21use fedimint_core::util::SafeUrl;
22use fedimint_core::{Amount, ChainId, Feerate};
23use fedimint_server_core::bitcoin_rpc::IServerBitcoinRpc;
24use rand::rngs::OsRng;
25use tracing::debug;
26
27use super::BitcoinTest;
28
29#[derive(Debug)]
30struct FakeBitcoinTestInner {
31    /// Simulates mined bitcoin blocks
32    blocks: Vec<Block>,
33    /// Simulates pending transactions in the mempool
34    pending: Vec<Transaction>,
35    /// Tracks how much bitcoin was sent to an address (doesn't track sending
36    /// out of it)
37    addresses: BTreeMap<Txid, Amount>,
38    /// Simulates the merkle tree proofs
39    proofs: BTreeMap<Txid, TxOutProof>,
40    /// Simulates the script history
41    scripts: BTreeMap<ScriptBuf, Vec<Transaction>>,
42    /// Tracks the block height a transaction was included
43    txid_to_block_height: BTreeMap<Txid, usize>,
44    /// Makes every mempool transaction fetch fail, to model a backend that goes
45    /// away partway through a guardian's mempool scan.
46    ///
47    /// Listing the mempool is deliberately left working, so a scan still sees
48    /// which transactions exist and fails only when fetching them.
49    fail_mempool_tx_fetches: bool,
50    /// How long every mempool transaction fetch takes, to model a scan that
51    /// outlasts a guardian's time budget.
52    mempool_tx_fetch_delay: Duration,
53}
54
55#[derive(Clone, Debug)]
56pub struct FakeBitcoinTest {
57    inner: Arc<std::sync::RwLock<FakeBitcoinTestInner>>,
58}
59
60impl Default for FakeBitcoinTest {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl FakeBitcoinTest {
67    pub fn new() -> Self {
68        let inner = FakeBitcoinTestInner {
69            blocks: vec![genesis_block(Network::Regtest)],
70            pending: vec![],
71            addresses: BTreeMap::new(),
72            proofs: BTreeMap::new(),
73            scripts: BTreeMap::new(),
74            txid_to_block_height: BTreeMap::new(),
75            fail_mempool_tx_fetches: false,
76            mempool_tx_fetch_delay: Duration::ZERO,
77        };
78        let res = FakeBitcoinTest {
79            inner: std::sync::RwLock::new(inner).into(),
80        };
81
82        // We always need one custom block for the ChainId
83        res.mine_blocks_no_async(1);
84
85        res
86    }
87
88    fn pending_merkle_tree(pending: &[Transaction]) -> PartialMerkleTree {
89        let txs = pending
90            .iter()
91            .map(Transaction::compute_txid)
92            .collect::<Vec<Txid>>();
93        let matches = repeat_n(true, txs.len()).collect::<Vec<bool>>();
94        PartialMerkleTree::from_txids(txs.as_slice(), matches.as_slice())
95    }
96
97    /// Create a fake bitcoin transaction with given outputs
98    ///
99    /// Nonce is used to avoid same txids for transactions with same outputs,
100    /// which can accidenatally happen due to how simplicit our fakes are.
101    fn new_transaction(out: Vec<TxOut>, nonce: u32) -> Transaction {
102        Transaction {
103            version: bitcoin::transaction::Version(0),
104            lock_time: LockTime::from_height(nonce).unwrap(),
105            input: vec![],
106            output: out,
107        }
108    }
109
110    fn mine_block(
111        addresses: &mut BTreeMap<Txid, Amount>,
112        blocks: &mut Vec<Block>,
113        pending: &mut Vec<Transaction>,
114        txid_to_block_height: &mut BTreeMap<Txid, usize>,
115    ) -> bitcoin::BlockHash {
116        debug!(
117            "Mining block: {} transactions, {} blocks",
118            pending.len(),
119            blocks.len()
120        );
121        let root = BlockHash::hash(&[0]);
122        // block height is 0-based, so blocks.len() before appending the current block
123        // gives the correct height
124        let block_height = blocks.len();
125        for tx in pending.iter() {
126            addresses.insert(tx.compute_txid(), Amount::from_sats(output_sum(tx)));
127            txid_to_block_height.insert(tx.compute_txid(), block_height);
128        }
129        // all blocks need at least one transaction
130        if pending.is_empty() {
131            pending.push(Self::new_transaction(vec![], blocks.len() as u32));
132        }
133        let merkle_root = Self::pending_merkle_tree(pending)
134            .extract_matches(&mut vec![], &mut vec![])
135            .unwrap();
136        let block = Block {
137            header: BlockHeader {
138                version: Version::from_consensus(0),
139                prev_blockhash: blocks.last().map_or(root, |b| b.header.block_hash()),
140                merkle_root,
141                time: 0,
142                bits: CompactTarget::from_consensus(0),
143                nonce: 0,
144            },
145            txdata: pending.clone(),
146        };
147        pending.clear();
148        blocks.push(block.clone());
149        block.block_hash()
150    }
151
152    fn mine_blocks_no_async(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
153        let mut inner = self.inner.write().unwrap();
154
155        let FakeBitcoinTestInner {
156            ref mut blocks,
157            ref mut pending,
158            ref mut addresses,
159            ref mut txid_to_block_height,
160            ..
161        } = *inner;
162
163        (1..=block_num)
164            .map(|_| FakeBitcoinTest::mine_block(addresses, blocks, pending, txid_to_block_height))
165            .collect()
166    }
167}
168
169#[async_trait]
170impl BitcoinTest for FakeBitcoinTest {
171    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
172        // With  FakeBitcoinTest, every test spawns their own instance,
173        // so not need to lock anything
174        Box::new(self.clone())
175    }
176
177    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
178        self.mine_blocks_no_async(block_num)
179    }
180
181    async fn prepare_funding_wallet(&self) {
182        // In fake wallet this might not be technically necessary,
183        // but it makes it behave more like the `RealBitcoinTest`.
184        let block_count = self.inner.write().unwrap().blocks.len() as u64;
185        if block_count < 100 {
186            self.mine_blocks(100 - block_count).await;
187        }
188    }
189
190    async fn send_without_mining(&self, address: &Address, amount: bitcoin::Amount) -> Transaction {
191        let mut inner = self.inner.write().unwrap();
192
193        let transaction = FakeBitcoinTest::new_transaction(
194            vec![TxOut {
195                value: amount,
196                script_pubkey: address.script_pubkey(),
197            }],
198            inner.blocks.len() as u32,
199        );
200
201        inner
202            .addresses
203            .insert(transaction.compute_txid(), amount.into());
204
205        inner.pending.push(transaction.clone());
206
207        transaction
208    }
209
210    async fn send_and_mine_block(
211        &self,
212        address: &Address,
213        amount: bitcoin::Amount,
214    ) -> (TxOutProof, Transaction) {
215        let mut inner = self.inner.write().unwrap();
216
217        let transaction = FakeBitcoinTest::new_transaction(
218            vec![TxOut {
219                value: amount,
220                script_pubkey: address.script_pubkey(),
221            }],
222            inner.blocks.len() as u32,
223        );
224        inner
225            .addresses
226            .insert(transaction.compute_txid(), amount.into());
227
228        inner.pending.push(transaction.clone());
229        let merkle_proof = FakeBitcoinTest::pending_merkle_tree(&inner.pending);
230
231        let FakeBitcoinTestInner {
232            ref mut blocks,
233            ref mut pending,
234            ref mut addresses,
235            ref mut txid_to_block_height,
236            ..
237        } = *inner;
238        FakeBitcoinTest::mine_block(addresses, blocks, pending, txid_to_block_height);
239        let block_header = inner.blocks.last().unwrap().header;
240        let proof = TxOutProof {
241            block_header,
242            merkle_proof,
243        };
244        inner
245            .proofs
246            .insert(transaction.compute_txid(), proof.clone());
247        inner
248            .scripts
249            .insert(address.script_pubkey(), vec![transaction.clone()]);
250
251        (proof, transaction)
252    }
253
254    async fn get_new_address(&self) -> Address {
255        let ctx = bitcoin::secp256k1::Secp256k1::new();
256        let (_, public_key) = ctx.generate_keypair(&mut OsRng);
257
258        Address::p2wpkh(&bitcoin::CompressedPublicKey(public_key), Network::Regtest)
259    }
260
261    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
262        self.mine_blocks(1).await;
263        let sats = self
264            .inner
265            .read()
266            .unwrap()
267            .blocks
268            .iter()
269            .flat_map(|block| block.txdata.iter().flat_map(|tx| tx.output.clone()))
270            .find(|out| out.script_pubkey == address.script_pubkey())
271            .map_or(0, |tx| tx.value.to_sat());
272        Amount::from_sats(sats)
273    }
274
275    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
276        loop {
277            let (pending, addresses) = {
278                let inner = self.inner.read().unwrap();
279                (inner.pending.clone(), inner.addresses.clone())
280            };
281
282            let mut fee = Amount::ZERO;
283            let maybe_tx = pending.iter().find(|tx| tx.compute_txid() == *txid);
284
285            let Some(tx) = maybe_tx else {
286                sleep_in_test("no transaction found", Duration::from_millis(100)).await;
287                continue;
288            };
289
290            for input in &tx.input {
291                fee += *addresses
292                    .get(&input.previous_output.txid)
293                    .expect("previous transaction should be known");
294            }
295
296            for output in &tx.output {
297                fee -= output.value.into();
298            }
299
300            return fee;
301        }
302    }
303
304    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
305        self.inner
306            .read()
307            .expect("RwLock poisoned")
308            .txid_to_block_height
309            .get(txid)
310            .map(|height| height.to_owned() as u64)
311    }
312
313    async fn get_block_count(&self) -> u64 {
314        self.inner.read().expect("RwLock poisoned").blocks.len() as u64
315    }
316
317    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
318        let inner = self.inner.read().unwrap();
319        let mempool_transactions = inner.pending.clone();
320        mempool_transactions
321            .iter()
322            .find(|tx| tx.compute_txid() == *txid)
323            .map(std::borrow::ToOwned::to_owned)
324    }
325
326    fn fail_mempool_tx_fetches(&self, failing: bool) {
327        self.inner
328            .write()
329            .expect("RwLock poisoned")
330            .fail_mempool_tx_fetches = failing;
331    }
332
333    fn delay_mempool_tx_fetches(&self, delay: Duration) {
334        self.inner
335            .write()
336            .expect("RwLock poisoned")
337            .mempool_tx_fetch_delay = delay;
338    }
339}
340
341#[async_trait]
342impl IBitcoindRpc for FakeBitcoinTest {
343    async fn get_tx_block_height(
344        &self,
345        txid: &bitcoin::Txid,
346    ) -> Result<Option<u64>, BitcoinRpcError> {
347        for (height, block) in self.inner.read().unwrap().blocks.iter().enumerate() {
348            if block.txdata.iter().any(|tx| &tx.compute_txid() == txid) {
349                return Ok(Some(height as u64));
350            }
351        }
352        Ok(None)
353    }
354
355    async fn watch_script_history(&self, _: &bitcoin::ScriptBuf) -> Result<(), BitcoinRpcError> {
356        Ok(())
357    }
358
359    async fn get_script_history(
360        &self,
361        script: &bitcoin::ScriptBuf,
362    ) -> Result<Vec<bitcoin::Transaction>, BitcoinRpcError> {
363        let inner = self.inner.read().unwrap();
364        Ok(inner.scripts.get(script).cloned().unwrap_or_default())
365    }
366
367    async fn get_txout_proof(&self, txid: bitcoin::Txid) -> Result<TxOutProof, BitcoinRpcError> {
368        let inner = self.inner.read().unwrap();
369        let proof = inner.proofs.get(&txid);
370        Ok(proof
371            .ok_or(BitcoinRpcError::ProofNotFound { txid })?
372            .clone())
373    }
374
375    async fn get_info(&self) -> Result<BlockchainInfo, BitcoinRpcError> {
376        let inner = self.inner.read().unwrap();
377        let count = inner.blocks.len() as u64;
378        let synced = inner.pending.is_empty();
379        Ok(BlockchainInfo {
380            block_height: count - 1,
381            synced,
382        })
383    }
384}
385
386fn output_sum(tx: &Transaction) -> u64 {
387    tx.output.iter().map(|output| output.value.to_sat()).sum()
388}
389
390fn inputs(tx: &Transaction) -> Vec<OutPoint> {
391    tx.input.iter().map(|input| input.previous_output).collect()
392}
393
394#[async_trait::async_trait]
395impl IServerBitcoinRpc for FakeBitcoinTest {
396    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
397        BitcoinRpcConfig {
398            kind: "mock_kind".to_string(),
399            url: "http://mock".parse().unwrap(),
400        }
401    }
402
403    fn get_url(&self) -> SafeUrl {
404        "http://mock".parse().unwrap()
405    }
406
407    async fn get_block_count(&self) -> Result<u64> {
408        Ok(self.inner.read().unwrap().blocks.len() as u64)
409    }
410
411    async fn get_block_hash(&self, height: u64) -> Result<bitcoin::BlockHash> {
412        self.inner
413            .read()
414            .unwrap()
415            .blocks
416            .get(height as usize)
417            .map(|block| block.header.block_hash())
418            .context("No block with that height found")
419    }
420
421    async fn get_block(&self, block_hash: &bitcoin::BlockHash) -> Result<bitcoin::Block> {
422        self.inner
423            .read()
424            .unwrap()
425            .blocks
426            .iter()
427            .find(|block| block.header.block_hash() == *block_hash)
428            .context("No block with that hash found")
429            .cloned()
430    }
431
432    async fn get_feerate(&self) -> Result<Option<Feerate>> {
433        Ok(Some(Feerate { sats_per_kvb: 2000 }))
434    }
435
436    /// Models a backend that can enumerate its mempool, as bitcoind can.
437    ///
438    /// Esplora-backed guardians return `None` here instead, so tests that need
439    /// to cover the no-visibility path cannot use this fixture.
440    async fn get_mempool_txids(&self) -> Result<Option<Vec<bitcoin::Txid>>> {
441        Ok(Some(
442            self.inner
443                .read()
444                .unwrap()
445                .pending
446                .iter()
447                .map(bitcoin::Transaction::compute_txid)
448                .collect(),
449        ))
450    }
451
452    async fn get_mempool_tx(&self, txid: &bitcoin::Txid) -> Result<Option<bitcoin::Transaction>> {
453        // Read before sleeping, so the lock is not held across the delay.
454        let delay = self.inner.read().unwrap().mempool_tx_fetch_delay;
455
456        if !delay.is_zero() {
457            sleep_in_test("delaying a mempool transaction fetch", delay).await;
458        }
459
460        let inner = self.inner.read().unwrap();
461
462        // Bitcoin Core reports "no such transaction" as `Ok(None)`, so an error
463        // here models the backend itself failing rather than the transaction
464        // having left the mempool.
465        if inner.fail_mempool_tx_fetches {
466            bail!("Mempool transaction fetches are failing");
467        }
468
469        Ok(inner
470            .pending
471            .iter()
472            .find(|tx| tx.compute_txid() == *txid)
473            .cloned())
474    }
475
476    async fn submit_transaction(&self, transaction: bitcoin::Transaction) -> anyhow::Result<()> {
477        let mut inner = self.inner.write().unwrap();
478        inner.pending.push(transaction);
479
480        let mut filtered = BTreeMap::<Vec<OutPoint>, bitcoin::Transaction>::new();
481
482        // Simulate the mempool keeping txs with higher fees (less output)
483        // TODO: This looks borked, should remove from `filtered` on higher fee or
484        // something, and check per-input anyway. Probably doesn't matter, and I
485        // don't want to touch it.
486        for tx in &inner.pending {
487            match filtered.get(&inputs(tx)) {
488                Some(found) if output_sum(tx) > output_sum(found) => {}
489                _ => {
490                    filtered.insert(inputs(tx), tx.clone());
491                }
492            }
493        }
494
495        inner.pending = filtered.into_values().collect();
496
497        Ok(())
498    }
499
500    async fn get_sync_progress(&self) -> anyhow::Result<Option<f64>> {
501        Ok(None)
502    }
503
504    async fn get_chain_id(&self) -> anyhow::Result<ChainId> {
505        self.get_block_hash(1).await.map(ChainId::new)
506    }
507}