Skip to content

Commit 27cccd7

Browse files
committed
test(electrum): detect receive tx cancel WIP
1 parent 58a6704 commit 27cccd7

File tree

1 file changed

+181
-11
lines changed

1 file changed

+181
-11
lines changed

crates/electrum/tests/test_electrum.rs

Lines changed: 181 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
use bdk_chain::{
2-
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
2+
bitcoin::{
3+
consensus::encode, hashes::Hash, secp256k1::Secp256k1, transaction::Version, Address,
4+
Amount, OutPoint, PrivateKey, Psbt, PublicKey, ScriptBuf, Transaction, TxIn, TxOut,
5+
WScriptHash,
6+
},
7+
indexer::keychain_txout::KeychainTxOutIndex,
38
local_chain::LocalChain,
9+
miniscript::{
10+
descriptor::{DescriptorSecretKey, SinglePubKey},
11+
Descriptor, DescriptorPublicKey,
12+
},
413
spk_client::{FullScanRequest, SyncRequest, SyncResponse},
514
spk_txout::SpkTxOutIndex,
615
Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph,
716
};
817
use bdk_electrum::BdkElectrumClient;
9-
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
18+
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, utils::new_tx, TestEnv};
1019
use core::time::Duration;
11-
use std::collections::{BTreeSet, HashSet};
20+
use std::collections::{BTreeSet, HashMap, HashSet};
1221
use std::str::FromStr;
1322

1423
// Batch size for `sync_with_electrum`.
@@ -26,7 +35,7 @@ fn get_balance(
2635
Ok(balance)
2736
}
2837

29-
fn sync_with_electrum<I, Spks>(
38+
fn sync_with_spks<I, Spks>(
3039
client: &BdkElectrumClient<electrum_client::Client>,
3140
spks: Spks,
3241
chain: &mut LocalChain,
@@ -54,6 +63,167 @@ where
5463
Ok(update)
5564
}
5665

66+
fn sync_with_keychain(
67+
client: &BdkElectrumClient<electrum_client::Client>,
68+
chain: &mut LocalChain,
69+
graph: &mut IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<()>>,
70+
) -> anyhow::Result<SyncResponse> {
71+
use bdk_chain::keychain_txout::SyncRequestBuilderExt;
72+
73+
let update = client.sync(
74+
SyncRequest::builder()
75+
.chain_tip(chain.tip())
76+
.revealed_spks_from_indexer(&graph.index, ..)
77+
.unconfirmed_outpoints(
78+
graph.graph().canonical_iter(chain, chain.tip().block_id()),
79+
&graph.index,
80+
),
81+
BATCH_SIZE,
82+
true,
83+
)?;
84+
85+
if let Some(chain_update) = update.chain_update.clone() {
86+
let _ = chain
87+
.apply_update(chain_update)
88+
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
89+
}
90+
let _ = graph.apply_update(update.tx_update.clone());
91+
92+
Ok(update)
93+
}
94+
95+
#[test]
96+
pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
97+
let env = TestEnv::new()?;
98+
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
99+
let client = BdkElectrumClient::new(electrum_client);
100+
101+
let secp = Secp256k1::new();
102+
let (descriptor, keymap) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)")
103+
.expect("must be valid");
104+
let receiver_spk = descriptor.at_derivation_index(9).unwrap().script_pubkey();
105+
106+
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
107+
KeychainTxOutIndex::new(10),
108+
);
109+
let _ = graph
110+
.index
111+
.insert_descriptor((), descriptor.clone())
112+
.unwrap();
113+
let (mut chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
114+
115+
env.mine_blocks(101, None)?;
116+
117+
let funding_txid = env.bitcoind.client.send_to_address(
118+
&Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked(),
119+
Amount::from_sat(10_000),
120+
None,
121+
None,
122+
None,
123+
None,
124+
Some(1),
125+
None,
126+
)?;
127+
128+
env.mine_blocks(1, None)?;
129+
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
130+
131+
let funding_tx = env
132+
.rpc_client()
133+
.get_transaction(&funding_txid, None)?
134+
.transaction()?;
135+
136+
let funding_amt = funding_tx
137+
.output
138+
.iter()
139+
.map(|o| o.value.to_sat())
140+
.sum::<u64>();
141+
142+
let mut send_psbt = Psbt::from_unsigned_tx(Transaction {
143+
input: vec![TxIn {
144+
previous_output: OutPoint::new(funding_txid, 0),
145+
..Default::default()
146+
}],
147+
output: vec![
148+
TxOut {
149+
value: Amount::from_sat(10_000),
150+
script_pubkey: receiver_spk.clone(),
151+
},
152+
TxOut {
153+
value: Amount::from_sat(funding_amt - 10_500),
154+
script_pubkey: funding_tx.output[0].script_pubkey.clone(),
155+
},
156+
],
157+
version: Version::TWO,
158+
..new_tx(1)
159+
})?;
160+
send_psbt.inputs[0].non_witness_utxo = Some(funding_tx.clone());
161+
162+
let mut undo_send_psbt = Psbt::from_unsigned_tx(Transaction {
163+
input: vec![TxIn {
164+
previous_output: OutPoint::new(funding_txid, 0),
165+
..Default::default()
166+
}],
167+
output: vec![
168+
TxOut {
169+
value: Amount::from_sat(5_000),
170+
script_pubkey: receiver_spk,
171+
},
172+
TxOut {
173+
value: Amount::from_sat(funding_amt - 5_500),
174+
script_pubkey: funding_tx.output[0].script_pubkey.clone(),
175+
},
176+
],
177+
version: Version::TWO,
178+
..new_tx(2)
179+
})?;
180+
undo_send_psbt.inputs[0].non_witness_utxo = Some(funding_tx.clone());
181+
182+
let _ = match keymap.iter().next().expect("keymap not empty") {
183+
(DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
184+
let pk = match single_pub.key {
185+
SinglePubKey::FullKey(pk) => pk,
186+
SinglePubKey::XOnly(_) => unimplemented!("single xonly pubkey"),
187+
};
188+
let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
189+
send_psbt.sign(&keys, &secp).unwrap();
190+
undo_send_psbt.sign(&keys, &secp).unwrap();
191+
}
192+
(_, DescriptorSecretKey::XPrv(k)) => {
193+
send_psbt.sign(&k.xkey, &secp).unwrap();
194+
undo_send_psbt.sign(&k.xkey, &secp).unwrap();
195+
}
196+
_ => unimplemented!("multi xkey signer"),
197+
};
198+
199+
// Broadcast the send transaction.
200+
let send_txid = env
201+
.rpc_client()
202+
.send_raw_transaction(&encode::serialize(&send_psbt.extract_tx()?))?;
203+
204+
// Broadcast the cancel transaction to create a conflict.
205+
let undo_send_txid = env
206+
.rpc_client()
207+
.send_raw_transaction(&encode::serialize(&undo_send_psbt.extract_tx()?))?;
208+
209+
// Sync and check for conflicts.
210+
let sync_result = sync_with_keychain(&client, &mut chain, &mut graph)?;
211+
assert!(sync_result
212+
.tx_update
213+
.txs
214+
.iter()
215+
.any(|tx| tx.compute_txid() == send_txid));
216+
assert!(sync_result
217+
.tx_update
218+
.txs
219+
.iter()
220+
.any(|tx| tx.compute_txid() == undo_send_txid));
221+
222+
println!("Detected transactions: {:?}", sync_result.tx_update.txs);
223+
224+
Ok(())
225+
}
226+
57227
#[test]
58228
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
59229
let env = TestEnv::new()?;
@@ -322,7 +492,7 @@ fn test_sync() -> anyhow::Result<()> {
322492
let txid = env.send(&addr_to_track, SEND_AMOUNT)?;
323493
env.wait_until_electrum_sees_txid(txid, Duration::from_secs(6))?;
324494

325-
let _ = sync_with_electrum(
495+
let _ = sync_with_spks(
326496
&client,
327497
[spk_to_track.clone()],
328498
&mut recv_chain,
@@ -343,7 +513,7 @@ fn test_sync() -> anyhow::Result<()> {
343513
env.mine_blocks(1, None)?;
344514
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
345515

346-
let _ = sync_with_electrum(
516+
let _ = sync_with_spks(
347517
&client,
348518
[spk_to_track.clone()],
349519
&mut recv_chain,
@@ -364,7 +534,7 @@ fn test_sync() -> anyhow::Result<()> {
364534
env.reorg_empty_blocks(1)?;
365535
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
366536

367-
let _ = sync_with_electrum(
537+
let _ = sync_with_spks(
368538
&client,
369539
[spk_to_track.clone()],
370540
&mut recv_chain,
@@ -384,7 +554,7 @@ fn test_sync() -> anyhow::Result<()> {
384554
env.mine_blocks(1, None)?;
385555
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
386556

387-
let _ = sync_with_electrum(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;
557+
let _ = sync_with_spks(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;
388558

389559
// Check if balance is correct once transaction is confirmed again.
390560
assert_eq!(
@@ -470,7 +640,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
470640

471641
// Sync up to tip.
472642
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
473-
let update = sync_with_electrum(
643+
let update = sync_with_spks(
474644
&client,
475645
[spk_to_track.clone()],
476646
&mut recv_chain,
@@ -501,7 +671,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
501671
env.reorg_empty_blocks(depth)?;
502672

503673
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
504-
let update = sync_with_electrum(
674+
let update = sync_with_spks(
505675
&client,
506676
[spk_to_track.clone()],
507677
&mut recv_chain,
@@ -549,7 +719,7 @@ fn test_sync_with_coinbase() -> anyhow::Result<()> {
549719
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
550720

551721
// Check to see if electrum syncs properly.
552-
assert!(sync_with_electrum(
722+
assert!(sync_with_spks(
553723
&client,
554724
[spk_to_track.clone()],
555725
&mut recv_chain,

0 commit comments

Comments
 (0)