|
3 | 3 | #[macro_use]
|
4 | 4 | mod common;
|
5 | 5 |
|
6 |
| -use std::{collections::BTreeSet, sync::Arc}; |
| 6 | +use std::{ |
| 7 | + collections::{BTreeSet, HashMap}, |
| 8 | + sync::Arc, |
| 9 | +}; |
7 | 10 |
|
8 | 11 | use bdk_chain::{
|
9 | 12 | indexed_tx_graph::{self, IndexedTxGraph},
|
10 | 13 | indexer::keychain_txout::KeychainTxOutIndex,
|
11 | 14 | local_chain::LocalChain,
|
| 15 | + spk_txout::SpkTxOutIndex, |
12 | 16 | tx_graph, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt,
|
13 | 17 | SpkIterator,
|
14 | 18 | };
|
15 | 19 | use bdk_testenv::{
|
| 20 | + anyhow::{self}, |
| 21 | + bitcoincore_rpc::{json::CreateRawTransactionInput, RpcApi}, |
16 | 22 | block_id, hash,
|
17 | 23 | utils::{new_tx, DESCRIPTORS},
|
| 24 | + TestEnv, |
| 25 | +}; |
| 26 | +use bitcoin::{ |
| 27 | + secp256k1::Secp256k1, Address, Amount, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, |
| 28 | + Txid, |
18 | 29 | };
|
19 |
| -use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; |
20 | 30 | use miniscript::Descriptor;
|
21 | 31 |
|
| 32 | +fn gen_spk() -> ScriptBuf { |
| 33 | + use bitcoin::secp256k1::{Secp256k1, SecretKey}; |
| 34 | + |
| 35 | + let secp = Secp256k1::new(); |
| 36 | + let (x_only_pk, _) = SecretKey::new(&mut rand::thread_rng()) |
| 37 | + .public_key(&secp) |
| 38 | + .x_only_public_key(); |
| 39 | + ScriptBuf::new_p2tr(&secp, x_only_pk, None) |
| 40 | +} |
| 41 | + |
| 42 | +/// Conflicts of relevant transactions must also be considered relevant. |
| 43 | +/// |
| 44 | +/// This allows the receiving structures to determine the reason why a given transaction is not part |
| 45 | +/// of the best history. I.e. Is this transaction evicted from the mempool because of insufficient |
| 46 | +/// fee, or because a conflict is confirmed? |
| 47 | +/// |
| 48 | +/// This tests the behavior of the "relevant-conflicts" logic. |
| 49 | +#[test] |
| 50 | +fn relevant_conflicts() -> anyhow::Result<()> { |
| 51 | + type SpkTxGraph = IndexedTxGraph<ConfirmationBlockTime, SpkTxOutIndex<()>>; |
| 52 | + |
| 53 | + /// This environment contains a sender and receiver. |
| 54 | + /// |
| 55 | + /// The sender sends a transaction to the receiver and attempts to cancel it later. |
| 56 | + struct ScenarioEnv { |
| 57 | + env: TestEnv, |
| 58 | + graph: SpkTxGraph, |
| 59 | + tx_send: Transaction, |
| 60 | + tx_cancel: Transaction, |
| 61 | + } |
| 62 | + |
| 63 | + impl ScenarioEnv { |
| 64 | + fn new() -> anyhow::Result<Self> { |
| 65 | + let env = TestEnv::new()?; |
| 66 | + let client = env.rpc_client(); |
| 67 | + |
| 68 | + let sender_addr = client |
| 69 | + .get_new_address(None, None)? |
| 70 | + .require_network(Network::Regtest)?; |
| 71 | + |
| 72 | + let recv_spk = gen_spk(); |
| 73 | + let recv_addr = Address::from_script(&recv_spk, &bitcoin::params::REGTEST)?; |
| 74 | + |
| 75 | + let mut graph = SpkTxGraph::default(); |
| 76 | + assert!(graph.index.insert_spk((), recv_spk)); |
| 77 | + |
| 78 | + env.mine_blocks(1, Some(sender_addr.clone()))?; |
| 79 | + env.mine_blocks(101, None)?; |
| 80 | + |
| 81 | + let tx_input = client |
| 82 | + .list_unspent(None, None, None, None, None)? |
| 83 | + .into_iter() |
| 84 | + .take(1) |
| 85 | + .map(|r| CreateRawTransactionInput { |
| 86 | + txid: r.txid, |
| 87 | + vout: r.vout, |
| 88 | + sequence: None, |
| 89 | + }) |
| 90 | + .collect::<Vec<_>>(); |
| 91 | + let tx_send = { |
| 92 | + let outputs = |
| 93 | + HashMap::from([(recv_addr.to_string(), Amount::from_btc(49.999_99)?)]); |
| 94 | + let tx = client.create_raw_transaction(&tx_input, &outputs, None, Some(true))?; |
| 95 | + client |
| 96 | + .sign_raw_transaction_with_wallet(&tx, None, None)? |
| 97 | + .transaction()? |
| 98 | + }; |
| 99 | + let tx_cancel = { |
| 100 | + let outputs = |
| 101 | + HashMap::from([(sender_addr.to_string(), Amount::from_btc(49.999_98)?)]); |
| 102 | + let tx = client.create_raw_transaction(&tx_input, &outputs, None, Some(true))?; |
| 103 | + client |
| 104 | + .sign_raw_transaction_with_wallet(&tx, None, None)? |
| 105 | + .transaction()? |
| 106 | + }; |
| 107 | + |
| 108 | + Ok(Self { |
| 109 | + env, |
| 110 | + graph, |
| 111 | + tx_send, |
| 112 | + tx_cancel, |
| 113 | + }) |
| 114 | + } |
| 115 | + |
| 116 | + /// Rudimentary sync implementation. |
| 117 | + /// |
| 118 | + /// Scans through all transactions in the blockchain + mempool. |
| 119 | + fn sync(&mut self) -> anyhow::Result<()> { |
| 120 | + let client = self.env.rpc_client(); |
| 121 | + for height in 0..=client.get_block_count()? { |
| 122 | + let hash = client.get_block_hash(height)?; |
| 123 | + let block = client.get_block(&hash)?; |
| 124 | + let _ = self.graph.apply_block_relevant(&block, height as _); |
| 125 | + } |
| 126 | + let _ = self.graph.batch_insert_relevant_unconfirmed( |
| 127 | + client |
| 128 | + .get_raw_mempool()? |
| 129 | + .into_iter() |
| 130 | + .map(|txid| client.get_raw_transaction(&txid, None).map(|tx| (tx, 0))) |
| 131 | + .collect::<Result<Vec<_>, _>>()?, |
| 132 | + ); |
| 133 | + Ok(()) |
| 134 | + } |
| 135 | + |
| 136 | + /// Broadcast the original sending transaction. |
| 137 | + fn broadcast_send(&self) -> anyhow::Result<Txid> { |
| 138 | + let client = self.env.rpc_client(); |
| 139 | + Ok(client.send_raw_transaction(&self.tx_send)?) |
| 140 | + } |
| 141 | + |
| 142 | + /// Broadcast the cancellation transaction. |
| 143 | + fn broadcast_cancel(&self) -> anyhow::Result<Txid> { |
| 144 | + let client = self.env.rpc_client(); |
| 145 | + Ok(client.send_raw_transaction(&self.tx_cancel)?) |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + // Broadcast `tx_send`. |
| 150 | + // Sync. |
| 151 | + // Broadcast `tx_cancel`. |
| 152 | + // `tx_cancel` gets confirmed. |
| 153 | + // Sync. |
| 154 | + // Expect: Both `tx_send` and `tx_cancel` appears in `recv_graph`. |
| 155 | + { |
| 156 | + let mut env = ScenarioEnv::new()?; |
| 157 | + let send_txid = env.broadcast_send()?; |
| 158 | + env.sync()?; |
| 159 | + let cancel_txid = env.broadcast_cancel()?; |
| 160 | + env.env.mine_blocks(6, None)?; |
| 161 | + env.sync()?; |
| 162 | + |
| 163 | + assert_eq!(env.graph.graph().full_txs().count(), 2); |
| 164 | + assert!(env.graph.graph().get_tx(send_txid).is_some()); |
| 165 | + assert!(env.graph.graph().get_tx(cancel_txid).is_some()); |
| 166 | + } |
| 167 | + |
| 168 | + // Broadcast `tx_send`. |
| 169 | + // Sync. |
| 170 | + // Broadcast `tx_cancel`. |
| 171 | + // Sync. |
| 172 | + // Expect: Both `tx_send` and `tx_cancel` appears in `recv_graph`. |
| 173 | + { |
| 174 | + let mut env = ScenarioEnv::new()?; |
| 175 | + let send_txid = env.broadcast_send()?; |
| 176 | + env.sync()?; |
| 177 | + let cancel_txid = env.broadcast_cancel()?; |
| 178 | + env.sync()?; |
| 179 | + |
| 180 | + assert_eq!(env.graph.graph().full_txs().count(), 2); |
| 181 | + assert!(env.graph.graph().get_tx(send_txid).is_some()); |
| 182 | + assert!(env.graph.graph().get_tx(cancel_txid).is_some()); |
| 183 | + } |
| 184 | + |
| 185 | + // If we don't see `tx_send` in the first place, `tx_cancel` should not be relevant. |
| 186 | + { |
| 187 | + let mut env = ScenarioEnv::new()?; |
| 188 | + let _ = env.broadcast_send()?; |
| 189 | + let _ = env.broadcast_cancel()?; |
| 190 | + env.sync()?; |
| 191 | + |
| 192 | + assert_eq!(env.graph.graph().full_txs().count(), 0); |
| 193 | + } |
| 194 | + |
| 195 | + Ok(()) |
| 196 | +} |
| 197 | + |
22 | 198 | /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
23 | 199 | /// in topological order.
|
24 | 200 | ///
|
|
0 commit comments