Skip to content

Commit c846ad8

Browse files
committed
test(chain): relevant_conflicts
1 parent 7003ad4 commit c846ad8

File tree

2 files changed

+179
-3
lines changed

2 files changed

+179
-3
lines changed

crates/chain/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ rusqlite = { version = "0.31.0", features = ["bundled"], optional = true }
2727
[dev-dependencies]
2828
rand = "0.8"
2929
proptest = "1.2.0"
30-
bdk_testenv = { path = "../testenv", default-features = false }
30+
bdk_testenv = { path = "../testenv" }
3131
criterion = { version = "0.2" }
3232

3333
[features]

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 178 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,198 @@
33
#[macro_use]
44
mod common;
55

6-
use std::{collections::BTreeSet, sync::Arc};
6+
use std::{
7+
collections::{BTreeSet, HashMap},
8+
sync::Arc,
9+
};
710

811
use bdk_chain::{
912
indexed_tx_graph::{self, IndexedTxGraph},
1013
indexer::keychain_txout::KeychainTxOutIndex,
1114
local_chain::LocalChain,
15+
spk_txout::SpkTxOutIndex,
1216
tx_graph, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt,
1317
SpkIterator,
1418
};
1519
use bdk_testenv::{
20+
anyhow::{self},
21+
bitcoincore_rpc::{json::CreateRawTransactionInput, RpcApi},
1622
block_id, hash,
1723
utils::{new_tx, DESCRIPTORS},
24+
TestEnv,
25+
};
26+
use bitcoin::{
27+
secp256k1::Secp256k1, Address, Amount, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut,
28+
Txid,
1829
};
19-
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
2030
use miniscript::Descriptor;
2131

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 transcation.
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+
22198
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
23199
/// in topological order.
24200
///

0 commit comments

Comments
 (0)