fedimint_testing/btc/
mock.rs

1use std::collections::BTreeMap;
2use std::iter::repeat;
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Context, Result, format_err};
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::{DynBitcoindRpc, IBitcoindRpc, IBitcoindRpcFactory, register_bitcoind};
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, Feerate};
23use rand::rngs::OsRng;
24use tracing::debug;
25
26use super::BitcoinTest;
27
28#[derive(Debug, Clone)]
29pub struct FakeBitcoinFactory {
30    pub bitcoin: FakeBitcoinTest,
31    pub config: BitcoinRpcConfig,
32}
33
34impl FakeBitcoinFactory {
35    /// Registers a fake bitcoin rpc factory for testing
36    pub fn register_new() -> FakeBitcoinFactory {
37        let kind = format!("test_btc-{}", rand::random::<u64>());
38        let factory = FakeBitcoinFactory {
39            bitcoin: FakeBitcoinTest::new(),
40            config: BitcoinRpcConfig {
41                kind: kind.clone(),
42                url: "http://ignored".parse().unwrap(),
43            },
44        };
45        register_bitcoind(kind, factory.clone().into());
46        factory
47    }
48}
49
50impl IBitcoindRpcFactory for FakeBitcoinFactory {
51    fn create_connection(&self, _url: &SafeUrl) -> Result<DynBitcoindRpc> {
52        Ok(self.bitcoin.clone().into())
53    }
54}
55
56#[derive(Debug)]
57struct FakeBitcoinTestInner {
58    /// Simulates mined bitcoin blocks
59    blocks: Vec<Block>,
60    /// Simulates pending transactions in the mempool
61    pending: Vec<Transaction>,
62    /// Tracks how much bitcoin was sent to an address (doesn't track sending
63    /// out of it)
64    addresses: BTreeMap<Txid, Amount>,
65    /// Simulates the merkle tree proofs
66    proofs: BTreeMap<Txid, TxOutProof>,
67    /// Simulates the script history
68    scripts: BTreeMap<ScriptBuf, Vec<Transaction>>,
69    /// Tracks the block height a transaction was included
70    txid_to_block_height: BTreeMap<Txid, usize>,
71}
72
73#[derive(Clone, Debug)]
74pub struct FakeBitcoinTest {
75    inner: Arc<std::sync::RwLock<FakeBitcoinTestInner>>,
76}
77
78impl Default for FakeBitcoinTest {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl FakeBitcoinTest {
85    pub fn new() -> Self {
86        let inner = FakeBitcoinTestInner {
87            blocks: vec![genesis_block(Network::Regtest)],
88            pending: vec![],
89            addresses: BTreeMap::new(),
90            proofs: BTreeMap::new(),
91            scripts: BTreeMap::new(),
92            txid_to_block_height: BTreeMap::new(),
93        };
94        FakeBitcoinTest {
95            inner: std::sync::RwLock::new(inner).into(),
96        }
97    }
98
99    fn pending_merkle_tree(pending: &[Transaction]) -> PartialMerkleTree {
100        let txs = pending
101            .iter()
102            .map(Transaction::compute_txid)
103            .collect::<Vec<Txid>>();
104        let matches = repeat(true).take(txs.len()).collect::<Vec<bool>>();
105        PartialMerkleTree::from_txids(txs.as_slice(), matches.as_slice())
106    }
107
108    /// Create a fake bitcoin transaction with given outputs
109    ///
110    /// Nonce is used to avoid same txids for transactions with same outputs,
111    /// which can accidenatally happen due to how simplicit our fakes are.
112    fn new_transaction(out: Vec<TxOut>, nonce: u32) -> Transaction {
113        Transaction {
114            version: bitcoin::transaction::Version(0),
115            lock_time: LockTime::from_height(nonce).unwrap(),
116            input: vec![],
117            output: out,
118        }
119    }
120
121    fn mine_block(
122        addresses: &mut BTreeMap<Txid, Amount>,
123        blocks: &mut Vec<Block>,
124        pending: &mut Vec<Transaction>,
125        txid_to_block_height: &mut BTreeMap<Txid, usize>,
126    ) -> bitcoin::BlockHash {
127        debug!(
128            "Mining block: {} transactions, {} blocks",
129            pending.len(),
130            blocks.len()
131        );
132        let root = BlockHash::hash(&[0]);
133        // block height is 0-based, so blocks.len() before appending the current block
134        // gives the correct height
135        let block_height = blocks.len();
136        for tx in pending.iter() {
137            addresses.insert(tx.compute_txid(), Amount::from_sats(output_sum(tx)));
138            txid_to_block_height.insert(tx.compute_txid(), block_height);
139        }
140        // all blocks need at least one transaction
141        if pending.is_empty() {
142            pending.push(Self::new_transaction(vec![], blocks.len() as u32));
143        }
144        let merkle_root = Self::pending_merkle_tree(pending)
145            .extract_matches(&mut vec![], &mut vec![])
146            .unwrap();
147        let block = Block {
148            header: BlockHeader {
149                version: Version::from_consensus(0),
150                prev_blockhash: blocks.last().map_or(root, |b| b.header.block_hash()),
151                merkle_root,
152                time: 0,
153                bits: CompactTarget::from_consensus(0),
154                nonce: 0,
155            },
156            txdata: pending.clone(),
157        };
158        pending.clear();
159        blocks.push(block.clone());
160        block.block_hash()
161    }
162}
163
164#[async_trait]
165impl BitcoinTest for FakeBitcoinTest {
166    async fn lock_exclusive(&self) -> Box<dyn BitcoinTest + Send + Sync> {
167        // With  FakeBitcoinTest, every test spawns their own instance,
168        // so not need to lock anything
169        Box::new(self.clone())
170    }
171
172    async fn mine_blocks(&self, block_num: u64) -> Vec<bitcoin::BlockHash> {
173        let mut inner = self.inner.write().unwrap();
174
175        let FakeBitcoinTestInner {
176            ref mut blocks,
177            ref mut pending,
178            ref mut addresses,
179            ref mut txid_to_block_height,
180            ..
181        } = *inner;
182
183        (1..=block_num)
184            .map(|_| FakeBitcoinTest::mine_block(addresses, blocks, pending, txid_to_block_height))
185            .collect()
186    }
187
188    async fn prepare_funding_wallet(&self) {
189        // In fake wallet this might not be technically necessary,
190        // but it makes it behave more like the `RealBitcoinTest`.
191        let block_count = self.inner.write().unwrap().blocks.len() as u64;
192        if block_count < 100 {
193            self.mine_blocks(100 - block_count).await;
194        }
195    }
196
197    async fn send_and_mine_block(
198        &self,
199        address: &Address,
200        amount: bitcoin::Amount,
201    ) -> (TxOutProof, Transaction) {
202        let mut inner = self.inner.write().unwrap();
203
204        let transaction = FakeBitcoinTest::new_transaction(
205            vec![TxOut {
206                value: amount,
207                script_pubkey: address.script_pubkey(),
208            }],
209            inner.blocks.len() as u32,
210        );
211        inner
212            .addresses
213            .insert(transaction.compute_txid(), amount.into());
214
215        inner.pending.push(transaction.clone());
216        let merkle_proof = FakeBitcoinTest::pending_merkle_tree(&inner.pending);
217
218        let FakeBitcoinTestInner {
219            ref mut blocks,
220            ref mut pending,
221            ref mut addresses,
222            ref mut txid_to_block_height,
223            ..
224        } = *inner;
225        FakeBitcoinTest::mine_block(addresses, blocks, pending, txid_to_block_height);
226        let block_header = inner.blocks.last().unwrap().header;
227        let proof = TxOutProof {
228            block_header,
229            merkle_proof,
230        };
231        inner
232            .proofs
233            .insert(transaction.compute_txid(), proof.clone());
234        inner
235            .scripts
236            .insert(address.script_pubkey(), vec![transaction.clone()]);
237
238        (proof, transaction)
239    }
240
241    async fn get_new_address(&self) -> Address {
242        let ctx = bitcoin::secp256k1::Secp256k1::new();
243        let (_, public_key) = ctx.generate_keypair(&mut OsRng);
244
245        Address::p2wpkh(&bitcoin::CompressedPublicKey(public_key), Network::Regtest)
246    }
247
248    async fn mine_block_and_get_received(&self, address: &Address) -> Amount {
249        self.mine_blocks(1).await;
250        let sats = self
251            .inner
252            .read()
253            .unwrap()
254            .blocks
255            .iter()
256            .flat_map(|block| block.txdata.iter().flat_map(|tx| tx.output.clone()))
257            .find(|out| out.script_pubkey == address.script_pubkey())
258            .map_or(0, |tx| tx.value.to_sat());
259        Amount::from_sats(sats)
260    }
261
262    async fn get_mempool_tx_fee(&self, txid: &Txid) -> Amount {
263        loop {
264            let (pending, addresses) = {
265                let inner = self.inner.read().unwrap();
266                (inner.pending.clone(), inner.addresses.clone())
267            };
268
269            let mut fee = Amount::ZERO;
270            let maybe_tx = pending.iter().find(|tx| tx.compute_txid() == *txid);
271
272            let tx = match maybe_tx {
273                None => {
274                    sleep_in_test("no transaction found", Duration::from_millis(100)).await;
275                    continue;
276                }
277                Some(tx) => tx,
278            };
279
280            for input in &tx.input {
281                fee += *addresses
282                    .get(&input.previous_output.txid)
283                    .expect("previous transaction should be known");
284            }
285
286            for output in &tx.output {
287                fee -= output.value.into();
288            }
289
290            return fee;
291        }
292    }
293
294    async fn get_tx_block_height(&self, txid: &Txid) -> Option<u64> {
295        self.inner
296            .read()
297            .expect("RwLock poisoned")
298            .txid_to_block_height
299            .get(txid)
300            .map(|height| height.to_owned() as u64)
301    }
302
303    async fn get_block_count(&self) -> u64 {
304        self.inner.read().expect("RwLock poisoned").blocks.len() as u64
305    }
306
307    async fn get_mempool_tx(&self, txid: &Txid) -> Option<bitcoin::Transaction> {
308        let inner = self.inner.read().unwrap();
309        let mempool_transactions = inner.pending.clone();
310        mempool_transactions
311            .iter()
312            .find(|tx| tx.compute_txid() == *txid)
313            .map(std::borrow::ToOwned::to_owned)
314    }
315}
316
317#[async_trait]
318impl IBitcoindRpc for FakeBitcoinTest {
319    async fn get_network(&self) -> Result<bitcoin::Network> {
320        Ok(bitcoin::Network::Regtest)
321    }
322
323    async fn get_block_count(&self) -> Result<u64> {
324        Ok(self.inner.read().unwrap().blocks.len() as u64)
325    }
326
327    async fn get_block_hash(&self, height: u64) -> Result<bitcoin::BlockHash> {
328        self.inner
329            .read()
330            .unwrap()
331            .blocks
332            .get(height as usize)
333            .map(|block| block.header.block_hash())
334            .context("No block with that height found")
335    }
336
337    async fn get_block(&self, block_hash: &bitcoin::BlockHash) -> Result<bitcoin::Block> {
338        self.inner
339            .read()
340            .unwrap()
341            .blocks
342            .iter()
343            .find(|block| block.header.block_hash() == *block_hash)
344            .context("No block with that hash found")
345            .cloned()
346    }
347
348    async fn get_fee_rate(&self, _confirmation_target: u16) -> Result<Option<Feerate>> {
349        Ok(Some(Feerate { sats_per_kvb: 2000 }))
350    }
351
352    async fn submit_transaction(&self, transaction: bitcoin::Transaction) {
353        let mut inner = self.inner.write().unwrap();
354        inner.pending.push(transaction);
355
356        let mut filtered = BTreeMap::<Vec<OutPoint>, bitcoin::Transaction>::new();
357
358        // Simulate the mempool keeping txs with higher fees (less output)
359        // TODO: This looks borked, should remove from `filtered` on higher fee or
360        // something, and check per-input anyway. Probably doesn't matter, and I
361        // don't want to touch it.
362        for tx in &inner.pending {
363            match filtered.get(&inputs(tx)) {
364                Some(found) if output_sum(tx) > output_sum(found) => {}
365                _ => {
366                    filtered.insert(inputs(tx), tx.clone());
367                }
368            }
369        }
370
371        inner.pending = filtered.into_values().collect();
372    }
373
374    async fn get_tx_block_height(&self, txid: &bitcoin::Txid) -> Result<Option<u64>> {
375        for (height, block) in self.inner.read().unwrap().blocks.iter().enumerate() {
376            if block.txdata.iter().any(|tx| &tx.compute_txid() == txid) {
377                return Ok(Some(height as u64));
378            }
379        }
380        Ok(None)
381    }
382
383    async fn is_tx_in_block(
384        &self,
385        txid: &bitcoin::Txid,
386        block_hash: &bitcoin::BlockHash,
387        block_height: u64,
388    ) -> Result<bool> {
389        let block = &self.inner.read().unwrap().blocks[block_height as usize];
390        assert!(
391            &block.block_hash() == block_hash,
392            "Block height for hash does not match expected height"
393        );
394
395        Ok(block.txdata.iter().any(|tx| &tx.compute_txid() == txid))
396    }
397
398    async fn watch_script_history(&self, _: &bitcoin::ScriptBuf) -> Result<()> {
399        Ok(())
400    }
401
402    async fn get_script_history(
403        &self,
404        script: &bitcoin::ScriptBuf,
405    ) -> Result<Vec<bitcoin::Transaction>> {
406        let inner = self.inner.read().unwrap();
407        Ok(inner.scripts.get(script).cloned().unwrap_or_default())
408    }
409
410    async fn get_txout_proof(&self, txid: bitcoin::Txid) -> Result<TxOutProof> {
411        let inner = self.inner.read().unwrap();
412        let proof = inner.proofs.get(&txid);
413        Ok(proof.ok_or(format_err!("No proof stored"))?.clone())
414    }
415
416    async fn get_sync_percentage(&self) -> anyhow::Result<Option<f64>> {
417        Ok(None)
418    }
419
420    fn get_bitcoin_rpc_config(&self) -> BitcoinRpcConfig {
421        BitcoinRpcConfig {
422            kind: "mock_kind".to_string(),
423            url: "http://mock".parse().unwrap(),
424        }
425    }
426}
427
428fn output_sum(tx: &Transaction) -> u64 {
429    tx.output.iter().map(|output| output.value.to_sat()).sum()
430}
431
432fn inputs(tx: &Transaction) -> Vec<OutPoint> {
433    tx.input.iter().map(|input| input.previous_output).collect()
434}