@@ -5,7 +5,10 @@ use bdk_chain::{
55 spk_txout:: SpkTxOutIndex ,
66 Balance , ConfirmationBlockTime , IndexedTxGraph , Indexer , Merge , TxGraph ,
77} ;
8- use bdk_core:: bitcoin:: Network ;
8+ use bdk_core:: bitcoin:: {
9+ key:: { Secp256k1 , UntweakedPublicKey } ,
10+ Network ,
11+ } ;
912use bdk_electrum:: BdkElectrumClient ;
1013use bdk_testenv:: {
1114 anyhow,
@@ -14,12 +17,22 @@ use bdk_testenv::{
1417} ;
1518use core:: time:: Duration ;
1619use electrum_client:: ElectrumApi ;
17- use std:: collections:: { BTreeSet , HashSet } ;
20+ use std:: collections:: { BTreeSet , HashMap , HashSet } ;
1821use std:: str:: FromStr ;
1922
2023// Batch size for `sync_with_electrum`.
2124const BATCH_SIZE : usize = 5 ;
2225
26+ pub fn get_test_spk ( ) -> ScriptBuf {
27+ const PK_BYTES : & [ u8 ] = & [
28+ 12 , 244 , 72 , 4 , 163 , 4 , 211 , 81 , 159 , 82 , 153 , 123 , 125 , 74 , 142 , 40 , 55 , 237 , 191 , 231 ,
29+ 31 , 114 , 89 , 165 , 83 , 141 , 8 , 203 , 93 , 240 , 53 , 101 ,
30+ ] ;
31+ let secp = Secp256k1 :: new ( ) ;
32+ let pk = UntweakedPublicKey :: from_slice ( PK_BYTES ) . expect ( "Must be valid PK" ) ;
33+ ScriptBuf :: new_p2tr ( & secp, pk, None )
34+ }
35+
2336fn get_balance (
2437 recv_chain : & LocalChain ,
2538 recv_graph : & IndexedTxGraph < ConfirmationBlockTime , SpkTxOutIndex < ( ) > > ,
@@ -60,6 +73,122 @@ where
6073 Ok ( update)
6174}
6275
76+ // Ensure that a wallet can detect a malicious replacement of an incoming transaction.
77+ //
78+ // This checks that both the Electrum chain source and the receiving structures properly track the
79+ // replaced transaction as missing.
80+ #[ test]
81+ pub fn detect_receive_tx_cancel ( ) -> anyhow:: Result < ( ) > {
82+ const SEND_TX_FEE : Amount = Amount :: from_sat ( 1000 ) ;
83+ const UNDO_SEND_TX_FEE : Amount = Amount :: from_sat ( 2000 ) ;
84+
85+ let env = TestEnv :: new ( ) ?;
86+ let rpc_client = env. rpc_client ( ) ;
87+ let electrum_client = electrum_client:: Client :: new ( env. electrsd . electrum_url . as_str ( ) ) ?;
88+ let client = BdkElectrumClient :: new ( electrum_client) ;
89+
90+ let mut graph = IndexedTxGraph :: < ConfirmationBlockTime , _ > :: new ( SpkTxOutIndex :: < ( ) > :: default ( ) ) ;
91+ let ( chain, _) = LocalChain :: from_genesis_hash ( env. bitcoind . client . get_block_hash ( 0 ) ?) ;
92+
93+ // Get receiving address.
94+ let receiver_spk = get_test_spk ( ) ;
95+ let receiver_addr = Address :: from_script ( & receiver_spk, bdk_chain:: bitcoin:: Network :: Regtest ) ?;
96+ graph. index . insert_spk ( ( ) , receiver_spk) ;
97+
98+ env. mine_blocks ( 101 , None ) ?;
99+
100+ // Select a UTXO to use as an input for constructing our test transactions.
101+ let selected_utxo = rpc_client
102+ . list_unspent ( None , None , None , Some ( false ) , None ) ?
103+ . into_iter ( )
104+ // Find a block reward tx.
105+ . find ( |utxo| utxo. amount == Amount :: from_int_btc ( 50 ) )
106+ . expect ( "Must find a block reward UTXO" ) ;
107+
108+ // Derive the sender's address from the selected UTXO.
109+ let sender_spk = selected_utxo. script_pub_key . clone ( ) ;
110+ let sender_addr = Address :: from_script ( & sender_spk, bdk_chain:: bitcoin:: Network :: Regtest )
111+ . expect ( "Failed to derive address from UTXO" ) ;
112+
113+ // Setup the common inputs used by both `send_tx` and `undo_send_tx`.
114+ let inputs = [ CreateRawTransactionInput {
115+ txid : selected_utxo. txid ,
116+ vout : selected_utxo. vout ,
117+ sequence : None ,
118+ } ] ;
119+
120+ // Create and sign the `send_tx` that sends funds to the receiver address.
121+ let send_tx_outputs = HashMap :: from ( [ (
122+ receiver_addr. to_string ( ) ,
123+ selected_utxo. amount - SEND_TX_FEE ,
124+ ) ] ) ;
125+ let send_tx = rpc_client. create_raw_transaction ( & inputs, & send_tx_outputs, None , Some ( true ) ) ?;
126+ let send_tx = rpc_client
127+ . sign_raw_transaction_with_wallet ( send_tx. raw_hex ( ) , None , None ) ?
128+ . transaction ( ) ?;
129+
130+ // Create and sign the `undo_send_tx` transaction. This redirects funds back to the sender
131+ // address.
132+ let undo_send_outputs = HashMap :: from ( [ (
133+ sender_addr. to_string ( ) ,
134+ selected_utxo. amount - UNDO_SEND_TX_FEE ,
135+ ) ] ) ;
136+ let undo_send_tx =
137+ rpc_client. create_raw_transaction ( & inputs, & undo_send_outputs, None , Some ( true ) ) ?;
138+ let undo_send_tx = rpc_client
139+ . sign_raw_transaction_with_wallet ( undo_send_tx. raw_hex ( ) , None , None ) ?
140+ . transaction ( ) ?;
141+
142+ // Sync after broadcasting the `send_tx`. Ensure that we detect and receive the `send_tx`.
143+ let send_txid = env. rpc_client ( ) . send_raw_transaction ( send_tx. raw_hex ( ) ) ?;
144+ env. wait_until_electrum_sees_txid ( send_txid, Duration :: from_secs ( 6 ) ) ?;
145+ let sync_request = SyncRequest :: builder ( )
146+ . chain_tip ( chain. tip ( ) )
147+ . spks_with_indexes ( graph. index . all_spks ( ) . clone ( ) )
148+ . expected_spk_txids ( graph. list_expected_spk_txids ( & chain, chain. tip ( ) . block_id ( ) , ..) ) ;
149+ let sync_response = client. sync ( sync_request, BATCH_SIZE , true ) ?;
150+ assert ! (
151+ sync_response
152+ . tx_update
153+ . txs
154+ . iter( )
155+ . any( |tx| tx. compute_txid( ) == send_txid) ,
156+ "sync response must include the send_tx"
157+ ) ;
158+ let changeset = graph. apply_update ( sync_response. tx_update . clone ( ) ) ;
159+ assert ! (
160+ changeset. tx_graph. txs. contains( & send_tx) ,
161+ "tx graph must deem send_tx relevant and include it"
162+ ) ;
163+
164+ // Sync after broadcasting the `undo_send_tx`. Verify that `send_tx` is now missing from the
165+ // mempool.
166+ let undo_send_txid = env
167+ . rpc_client ( )
168+ . send_raw_transaction ( undo_send_tx. raw_hex ( ) ) ?;
169+ env. wait_until_electrum_sees_txid ( undo_send_txid, Duration :: from_secs ( 6 ) ) ?;
170+ let sync_request = SyncRequest :: builder ( )
171+ . chain_tip ( chain. tip ( ) )
172+ . spks_with_indexes ( graph. index . all_spks ( ) . clone ( ) )
173+ . expected_spk_txids ( graph. list_expected_spk_txids ( & chain, chain. tip ( ) . block_id ( ) , ..) ) ;
174+ let sync_response = client. sync ( sync_request, BATCH_SIZE , true ) ?;
175+ assert ! (
176+ sync_response
177+ . tx_update
178+ . evicted_ats
179+ . iter( )
180+ . any( |( txid, _) | * txid == send_txid) ,
181+ "sync response must track send_tx as missing from mempool"
182+ ) ;
183+ let changeset = graph. apply_update ( sync_response. tx_update . clone ( ) ) ;
184+ assert ! (
185+ changeset. tx_graph. last_evicted. contains_key( & send_txid) ,
186+ "tx graph must track send_tx as missing"
187+ ) ;
188+
189+ Ok ( ( ) )
190+ }
191+
63192/// If an spk history contains a tx that spends another unconfirmed tx (chained mempool history),
64193/// the Electrum API will return the tx with a negative height. This should succeed and not panic.
65194#[ test]
0 commit comments