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