1
1
use bdk_chain:: {
2
2
bitcoin:: { hashes:: Hash , Address , Amount , ScriptBuf , WScriptHash } ,
3
+ keychain_txout:: KeychainTxOutIndex ,
3
4
local_chain:: LocalChain ,
5
+ miniscript:: Descriptor ,
4
6
spk_client:: { FullScanRequest , SyncRequest , SyncResponse } ,
5
7
spk_txout:: SpkTxOutIndex ,
6
8
Balance , ConfirmationBlockTime , IndexedTxGraph , Indexer , Merge , TxGraph ,
7
9
} ;
8
- use bdk_core:: bitcoin:: Network ;
10
+ use bdk_core:: bitcoin:: { key :: Secp256k1 , Network } ;
9
11
use bdk_electrum:: BdkElectrumClient ;
10
12
use bdk_testenv:: {
11
13
anyhow,
@@ -14,7 +16,7 @@ use bdk_testenv::{
14
16
} ;
15
17
use core:: time:: Duration ;
16
18
use electrum_client:: ElectrumApi ;
17
- use std:: collections:: { BTreeSet , HashSet } ;
19
+ use std:: collections:: { BTreeSet , HashMap , HashSet } ;
18
20
use std:: str:: FromStr ;
19
21
20
22
// Batch size for `sync_with_electrum`.
@@ -60,6 +62,125 @@ where
60
62
Ok ( update)
61
63
}
62
64
65
+ // Ensure that a wallet can detect a malicious replacement of an incoming transaction.
66
+ //
67
+ // This checks that both the Electrum chain source and the receiving structures properly track the
68
+ // replaced transaction as missing.
69
+ #[ test]
70
+ pub fn detect_receive_tx_cancel ( ) -> anyhow:: Result < ( ) > {
71
+ const SEND_TX_FEE : Amount = Amount :: from_sat ( 1000 ) ;
72
+ const UNDO_SEND_TX_FEE : Amount = Amount :: from_sat ( 2000 ) ;
73
+
74
+ use bdk_chain:: keychain_txout:: SyncRequestBuilderExt ;
75
+ let env = TestEnv :: new ( ) ?;
76
+ let rpc_client = env. rpc_client ( ) ;
77
+ let electrum_client = electrum_client:: Client :: new ( env. electrsd . electrum_url . as_str ( ) ) ?;
78
+ let client = BdkElectrumClient :: new ( electrum_client) ;
79
+
80
+ let ( receiver_desc, _) = Descriptor :: parse_descriptor ( & Secp256k1 :: signing_only ( ) , "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)" )
81
+ . expect ( "must be valid" ) ;
82
+ let mut graph = IndexedTxGraph :: < ConfirmationBlockTime , _ > :: new ( KeychainTxOutIndex :: new ( 0 ) ) ;
83
+ let _ = graph. index . insert_descriptor ( ( ) , receiver_desc. clone ( ) ) ?;
84
+ let ( chain, _) = LocalChain :: from_genesis_hash ( env. bitcoind . client . get_block_hash ( 0 ) ?) ;
85
+
86
+ // Derive the receiving address from the descriptor.
87
+ let ( ( _, receiver_spk) , _) = graph. index . reveal_next_spk ( ( ) ) . unwrap ( ) ;
88
+ let receiver_addr = Address :: from_script ( & receiver_spk, bdk_chain:: bitcoin:: Network :: Regtest ) ?;
89
+
90
+ env. mine_blocks ( 101 , None ) ?;
91
+
92
+ // Select a UTXO to use as an input for constructing our test transactions.
93
+ let selected_utxo = rpc_client
94
+ . list_unspent ( None , None , None , Some ( false ) , None ) ?
95
+ . into_iter ( )
96
+ // Find a block reward tx.
97
+ . find ( |utxo| utxo. amount == Amount :: from_int_btc ( 50 ) )
98
+ . expect ( "Must find a block reward UTXO" ) ;
99
+
100
+ // Derive the sender's address from the selected UTXO.
101
+ let sender_spk = selected_utxo. script_pub_key . clone ( ) ;
102
+ let sender_addr = Address :: from_script ( & sender_spk, bdk_chain:: bitcoin:: Network :: Regtest )
103
+ . expect ( "Failed to derive address from UTXO" ) ;
104
+
105
+ // Setup the common inputs used by both `send_tx` and `undo_send_tx`.
106
+ let inputs = [ CreateRawTransactionInput {
107
+ txid : selected_utxo. txid ,
108
+ vout : selected_utxo. vout ,
109
+ sequence : None ,
110
+ } ] ;
111
+
112
+ // Create and sign the `send_tx` that sends funds to the receiver address.
113
+ let send_tx_outputs = HashMap :: from ( [ (
114
+ receiver_addr. to_string ( ) ,
115
+ selected_utxo. amount - SEND_TX_FEE ,
116
+ ) ] ) ;
117
+ let send_tx = rpc_client. create_raw_transaction ( & inputs, & send_tx_outputs, None , Some ( true ) ) ?;
118
+ let send_tx = rpc_client
119
+ . sign_raw_transaction_with_wallet ( send_tx. raw_hex ( ) , None , None ) ?
120
+ . transaction ( ) ?;
121
+
122
+ // Create and sign the `undo_send_tx` transaction. This redirects funds back to the sender
123
+ // address.
124
+ let undo_send_outputs = HashMap :: from ( [ (
125
+ sender_addr. to_string ( ) ,
126
+ selected_utxo. amount - UNDO_SEND_TX_FEE ,
127
+ ) ] ) ;
128
+ let undo_send_tx =
129
+ rpc_client. create_raw_transaction ( & inputs, & undo_send_outputs, None , Some ( true ) ) ?;
130
+ let undo_send_tx = rpc_client
131
+ . sign_raw_transaction_with_wallet ( undo_send_tx. raw_hex ( ) , None , None ) ?
132
+ . transaction ( ) ?;
133
+
134
+ // Sync after broadcasting the `send_tx`. Ensure that we detect and receive the `send_tx`.
135
+ let send_txid = env. rpc_client ( ) . send_raw_transaction ( send_tx. raw_hex ( ) ) ?;
136
+ env. wait_until_electrum_sees_txid ( send_txid, Duration :: from_secs ( 6 ) ) ?;
137
+ let sync_request = SyncRequest :: builder ( )
138
+ . chain_tip ( chain. tip ( ) )
139
+ . revealed_spks_from_indexer ( & graph. index , ..)
140
+ . expected_spk_txids ( graph. list_expected_spk_txids ( & chain, chain. tip ( ) . block_id ( ) , ..) ) ;
141
+ let sync_response = client. sync ( sync_request, BATCH_SIZE , true ) ?;
142
+ assert ! (
143
+ sync_response
144
+ . tx_update
145
+ . txs
146
+ . iter( )
147
+ . any( |tx| tx. compute_txid( ) == send_txid) ,
148
+ "sync response must include the send_tx"
149
+ ) ;
150
+ let changeset = graph. apply_update ( sync_response. tx_update . clone ( ) ) ;
151
+ assert ! (
152
+ changeset. tx_graph. txs. contains( & send_tx) ,
153
+ "tx graph must deem send_tx relevant and include it"
154
+ ) ;
155
+
156
+ // Sync after broadcasting the `undo_send_tx`. Verify that `send_tx` is now missing from the
157
+ // mempool.
158
+ let undo_send_txid = env
159
+ . rpc_client ( )
160
+ . send_raw_transaction ( undo_send_tx. raw_hex ( ) ) ?;
161
+ env. wait_until_electrum_sees_txid ( undo_send_txid, Duration :: from_secs ( 6 ) ) ?;
162
+ let sync_request = SyncRequest :: builder ( )
163
+ . chain_tip ( chain. tip ( ) )
164
+ . revealed_spks_from_indexer ( & graph. index , ..)
165
+ . expected_spk_txids ( graph. list_expected_spk_txids ( & chain, chain. tip ( ) . block_id ( ) , ..) ) ;
166
+ let sync_response = client. sync ( sync_request, BATCH_SIZE , true ) ?;
167
+ assert ! (
168
+ sync_response
169
+ . tx_update
170
+ . evicted_ats
171
+ . iter( )
172
+ . any( |( txid, _) | * txid == send_txid) ,
173
+ "sync response must track send_tx as missing from mempool"
174
+ ) ;
175
+ let changeset = graph. apply_update ( sync_response. tx_update . clone ( ) ) ;
176
+ assert ! (
177
+ changeset. tx_graph. last_evicted. contains_key( & send_txid) ,
178
+ "tx graph must track send_tx as missing"
179
+ ) ;
180
+
181
+ Ok ( ( ) )
182
+ }
183
+
63
184
/// If an spk history contains a tx that spends another unconfirmed tx (chained mempool history),
64
185
/// the Electrum API will return the tx with a negative height. This should succeed and not panic.
65
186
#[ test]
0 commit comments