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