@@ -5,7 +5,10 @@ use bdk_chain::{
5
5
spk_txout:: SpkTxOutIndex ,
6
6
Balance , ConfirmationBlockTime , IndexedTxGraph , Indexer , Merge , TxGraph ,
7
7
} ;
8
- use bdk_core:: bitcoin:: Network ;
8
+ use bdk_core:: bitcoin:: {
9
+ key:: { Secp256k1 , UntweakedPublicKey } ,
10
+ Network ,
11
+ } ;
9
12
use bdk_electrum:: BdkElectrumClient ;
10
13
use bdk_testenv:: {
11
14
anyhow,
@@ -14,12 +17,22 @@ use bdk_testenv::{
14
17
} ;
15
18
use core:: time:: Duration ;
16
19
use electrum_client:: ElectrumApi ;
17
- use std:: collections:: { BTreeSet , HashSet } ;
20
+ use std:: collections:: { BTreeSet , HashMap , HashSet } ;
18
21
use std:: str:: FromStr ;
19
22
20
23
// Batch size for `sync_with_electrum`.
21
24
const BATCH_SIZE : usize = 5 ;
22
25
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
+
23
36
fn get_balance (
24
37
recv_chain : & LocalChain ,
25
38
recv_graph : & IndexedTxGraph < ConfirmationBlockTime , SpkTxOutIndex < ( ) > > ,
@@ -60,6 +73,122 @@ where
60
73
Ok ( update)
61
74
}
62
75
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
+
63
192
/// If an spk history contains a tx that spends another unconfirmed tx (chained mempool history),
64
193
/// the Electrum API will return the tx with a negative height. This should succeed and not panic.
65
194
#[ test]
0 commit comments