From c846ad84dda49d2c76e44786f59eb20e02b7710f Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=BF=97=E5=AE=87?= Date: Sat, 2 Aug 2025 06:16:26 +0000 Subject: [PATCH] test(chain): `relevant_conflicts` --- crates/chain/Cargo.toml | 2 +- crates/chain/tests/test_indexed_tx_graph.rs | 180 +++++++++++++++++++- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 76b1b3ce..c13042ab 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -27,7 +27,7 @@ rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } [dev-dependencies] rand = "0.8" proptest = "1.2.0" -bdk_testenv = { path = "../testenv", default-features = false } +bdk_testenv = { path = "../testenv" } criterion = { version = "0.2" } [features] diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index ca0eaf67..b22b47aa 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -3,22 +3,198 @@ #[macro_use] mod common; -use std::{collections::BTreeSet, sync::Arc}; +use std::{ + collections::{BTreeSet, HashMap}, + sync::Arc, +}; use bdk_chain::{ indexed_tx_graph::{self, IndexedTxGraph}, indexer::keychain_txout::KeychainTxOutIndex, local_chain::LocalChain, + spk_txout::SpkTxOutIndex, tx_graph, Balance, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt, SpkIterator, }; use bdk_testenv::{ + anyhow::{self}, + bitcoincore_rpc::{json::CreateRawTransactionInput, RpcApi}, block_id, hash, utils::{new_tx, DESCRIPTORS}, + TestEnv, +}; +use bitcoin::{ + secp256k1::Secp256k1, Address, Amount, Network, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, + Txid, }; -use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut}; use miniscript::Descriptor; +fn gen_spk() -> ScriptBuf { + use bitcoin::secp256k1::{Secp256k1, SecretKey}; + + let secp = Secp256k1::new(); + let (x_only_pk, _) = SecretKey::new(&mut rand::thread_rng()) + .public_key(&secp) + .x_only_public_key(); + ScriptBuf::new_p2tr(&secp, x_only_pk, None) +} + +/// Conflicts of relevant transactions must also be considered relevant. +/// +/// This allows the receiving structures to determine the reason why a given transaction is not part +/// of the best history. I.e. Is this transaction evicted from the mempool because of insufficient +/// fee, or because a conflict is confirmed? +/// +/// This tests the behavior of the "relevant-conflicts" logic. +#[test] +fn relevant_conflicts() -> anyhow::Result<()> { + type SpkTxGraph = IndexedTxGraph>; + + /// This environment contains a sender and receiver. + /// + /// The sender sends a transaction to the receiver and attempts to cancel it later. + struct ScenarioEnv { + env: TestEnv, + graph: SpkTxGraph, + tx_send: Transaction, + tx_cancel: Transaction, + } + + impl ScenarioEnv { + fn new() -> anyhow::Result { + let env = TestEnv::new()?; + let client = env.rpc_client(); + + let sender_addr = client + .get_new_address(None, None)? + .require_network(Network::Regtest)?; + + let recv_spk = gen_spk(); + let recv_addr = Address::from_script(&recv_spk, &bitcoin::params::REGTEST)?; + + let mut graph = SpkTxGraph::default(); + assert!(graph.index.insert_spk((), recv_spk)); + + env.mine_blocks(1, Some(sender_addr.clone()))?; + env.mine_blocks(101, None)?; + + let tx_input = client + .list_unspent(None, None, None, None, None)? + .into_iter() + .take(1) + .map(|r| CreateRawTransactionInput { + txid: r.txid, + vout: r.vout, + sequence: None, + }) + .collect::>(); + let tx_send = { + let outputs = + HashMap::from([(recv_addr.to_string(), Amount::from_btc(49.999_99)?)]); + let tx = client.create_raw_transaction(&tx_input, &outputs, None, Some(true))?; + client + .sign_raw_transaction_with_wallet(&tx, None, None)? + .transaction()? + }; + let tx_cancel = { + let outputs = + HashMap::from([(sender_addr.to_string(), Amount::from_btc(49.999_98)?)]); + let tx = client.create_raw_transaction(&tx_input, &outputs, None, Some(true))?; + client + .sign_raw_transaction_with_wallet(&tx, None, None)? + .transaction()? + }; + + Ok(Self { + env, + graph, + tx_send, + tx_cancel, + }) + } + + /// Rudimentary sync implementation. + /// + /// Scans through all transactions in the blockchain + mempool. + fn sync(&mut self) -> anyhow::Result<()> { + let client = self.env.rpc_client(); + for height in 0..=client.get_block_count()? { + let hash = client.get_block_hash(height)?; + let block = client.get_block(&hash)?; + let _ = self.graph.apply_block_relevant(&block, height as _); + } + let _ = self.graph.batch_insert_relevant_unconfirmed( + client + .get_raw_mempool()? + .into_iter() + .map(|txid| client.get_raw_transaction(&txid, None).map(|tx| (tx, 0))) + .collect::, _>>()?, + ); + Ok(()) + } + + /// Broadcast the original sending transcation. + fn broadcast_send(&self) -> anyhow::Result { + let client = self.env.rpc_client(); + Ok(client.send_raw_transaction(&self.tx_send)?) + } + + /// Broadcast the cancellation transaction. + fn broadcast_cancel(&self) -> anyhow::Result { + let client = self.env.rpc_client(); + Ok(client.send_raw_transaction(&self.tx_cancel)?) + } + } + + // Broadcast `tx_send`. + // Sync. + // Broadcast `tx_cancel`. + // `tx_cancel` gets confirmed. + // Sync. + // Expect: Both `tx_send` and `tx_cancel` appears in `recv_graph`. + { + let mut env = ScenarioEnv::new()?; + let send_txid = env.broadcast_send()?; + env.sync()?; + let cancel_txid = env.broadcast_cancel()?; + env.env.mine_blocks(6, None)?; + env.sync()?; + + assert_eq!(env.graph.graph().full_txs().count(), 2); + assert!(env.graph.graph().get_tx(send_txid).is_some()); + assert!(env.graph.graph().get_tx(cancel_txid).is_some()); + } + + // Broadcast `tx_send`. + // Sync. + // Broadcast `tx_cancel`. + // Sync. + // Expect: Both `tx_send` and `tx_cancel` appears in `recv_graph`. + { + let mut env = ScenarioEnv::new()?; + let send_txid = env.broadcast_send()?; + env.sync()?; + let cancel_txid = env.broadcast_cancel()?; + env.sync()?; + + assert_eq!(env.graph.graph().full_txs().count(), 2); + assert!(env.graph.graph().get_tx(send_txid).is_some()); + assert!(env.graph.graph().get_tx(cancel_txid).is_some()); + } + + // If we don't see `tx_send` in the first place, `tx_cancel` should not be relevant. + { + let mut env = ScenarioEnv::new()?; + let _ = env.broadcast_send()?; + let _ = env.broadcast_cancel()?; + env.sync()?; + + assert_eq!(env.graph.graph().full_txs().count(), 0); + } + + Ok(()) +} + /// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented /// in topological order. /// -- 2.49.0