Skip to content

Commit

Permalink
test(electrum): detect receive tx cancel WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
LagginTimes committed Jan 17, 2025
1 parent 58a6704 commit 27cccd7
Showing 1 changed file with 181 additions and 11 deletions.
192 changes: 181 additions & 11 deletions crates/electrum/tests/test_electrum.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
use bdk_chain::{
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
bitcoin::{
consensus::encode, hashes::Hash, secp256k1::Secp256k1, transaction::Version, Address,
Amount, OutPoint, PrivateKey, Psbt, PublicKey, ScriptBuf, Transaction, TxIn, TxOut,
WScriptHash,
},
indexer::keychain_txout::KeychainTxOutIndex,
local_chain::LocalChain,
miniscript::{
descriptor::{DescriptorSecretKey, SinglePubKey},
Descriptor, DescriptorPublicKey,
},
spk_client::{FullScanRequest, SyncRequest, SyncResponse},
spk_txout::SpkTxOutIndex,
Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph,
};
use bdk_electrum::BdkElectrumClient;
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, utils::new_tx, TestEnv};
use core::time::Duration;
use std::collections::{BTreeSet, HashSet};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::str::FromStr;

// Batch size for `sync_with_electrum`.
Expand All @@ -26,7 +35,7 @@ fn get_balance(
Ok(balance)
}

fn sync_with_electrum<I, Spks>(
fn sync_with_spks<I, Spks>(
client: &BdkElectrumClient<electrum_client::Client>,
spks: Spks,
chain: &mut LocalChain,
Expand Down Expand Up @@ -54,6 +63,167 @@ where
Ok(update)
}

fn sync_with_keychain(
client: &BdkElectrumClient<electrum_client::Client>,
chain: &mut LocalChain,
graph: &mut IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<()>>,
) -> anyhow::Result<SyncResponse> {
use bdk_chain::keychain_txout::SyncRequestBuilderExt;

let update = client.sync(
SyncRequest::builder()
.chain_tip(chain.tip())
.revealed_spks_from_indexer(&graph.index, ..)
.unconfirmed_outpoints(
graph.graph().canonical_iter(chain, chain.tip().block_id()),
&graph.index,
),
BATCH_SIZE,
true,
)?;

if let Some(chain_update) = update.chain_update.clone() {
let _ = chain
.apply_update(chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
}
let _ = graph.apply_update(update.tx_update.clone());

Ok(update)
}

#[test]
pub fn detect_receive_tx_cancel() -> anyhow::Result<()> {
let env = TestEnv::new()?;
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
let client = BdkElectrumClient::new(electrum_client);

let secp = Secp256k1::new();
let (descriptor, keymap) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)")
.expect("must be valid");
let receiver_spk = descriptor.at_derivation_index(9).unwrap().script_pubkey();

let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
KeychainTxOutIndex::new(10),
);
let _ = graph
.index
.insert_descriptor((), descriptor.clone())
.unwrap();
let (mut chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);

env.mine_blocks(101, None)?;

let funding_txid = env.bitcoind.client.send_to_address(
&Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked(),
Amount::from_sat(10_000),
None,
None,
None,
None,
Some(1),
None,
)?;

env.mine_blocks(1, None)?;
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;

let funding_tx = env
.rpc_client()
.get_transaction(&funding_txid, None)?
.transaction()?;

let funding_amt = funding_tx
.output
.iter()
.map(|o| o.value.to_sat())
.sum::<u64>();

let mut send_psbt = Psbt::from_unsigned_tx(Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(funding_txid, 0),
..Default::default()
}],
output: vec![
TxOut {
value: Amount::from_sat(10_000),
script_pubkey: receiver_spk.clone(),
},
TxOut {
value: Amount::from_sat(funding_amt - 10_500),
script_pubkey: funding_tx.output[0].script_pubkey.clone(),
},
],
version: Version::TWO,
..new_tx(1)
})?;
send_psbt.inputs[0].non_witness_utxo = Some(funding_tx.clone());

let mut undo_send_psbt = Psbt::from_unsigned_tx(Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(funding_txid, 0),
..Default::default()
}],
output: vec![
TxOut {
value: Amount::from_sat(5_000),
script_pubkey: receiver_spk,
},
TxOut {
value: Amount::from_sat(funding_amt - 5_500),
script_pubkey: funding_tx.output[0].script_pubkey.clone(),
},
],
version: Version::TWO,
..new_tx(2)
})?;
undo_send_psbt.inputs[0].non_witness_utxo = Some(funding_tx.clone());

let _ = match keymap.iter().next().expect("keymap not empty") {
(DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
let pk = match single_pub.key {
SinglePubKey::FullKey(pk) => pk,
SinglePubKey::XOnly(_) => unimplemented!("single xonly pubkey"),
};
let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
send_psbt.sign(&keys, &secp).unwrap();
undo_send_psbt.sign(&keys, &secp).unwrap();
}
(_, DescriptorSecretKey::XPrv(k)) => {
send_psbt.sign(&k.xkey, &secp).unwrap();
undo_send_psbt.sign(&k.xkey, &secp).unwrap();
}
_ => unimplemented!("multi xkey signer"),
};

// Broadcast the send transaction.
let send_txid = env
.rpc_client()
.send_raw_transaction(&encode::serialize(&send_psbt.extract_tx()?))?;

// Broadcast the cancel transaction to create a conflict.
let undo_send_txid = env
.rpc_client()
.send_raw_transaction(&encode::serialize(&undo_send_psbt.extract_tx()?))?;

// Sync and check for conflicts.
let sync_result = sync_with_keychain(&client, &mut chain, &mut graph)?;
assert!(sync_result
.tx_update
.txs
.iter()
.any(|tx| tx.compute_txid() == send_txid));
assert!(sync_result
.tx_update
.txs
.iter()
.any(|tx| tx.compute_txid() == undo_send_txid));

println!("Detected transactions: {:?}", sync_result.tx_update.txs);

Ok(())
}

#[test]
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
let env = TestEnv::new()?;
Expand Down Expand Up @@ -322,7 +492,7 @@ fn test_sync() -> anyhow::Result<()> {
let txid = env.send(&addr_to_track, SEND_AMOUNT)?;
env.wait_until_electrum_sees_txid(txid, Duration::from_secs(6))?;

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

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

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

let _ = sync_with_electrum(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;
let _ = sync_with_spks(&client, [spk_to_track], &mut recv_chain, &mut recv_graph)?;

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

// Sync up to tip.
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
let update = sync_with_electrum(
let update = sync_with_spks(
&client,
[spk_to_track.clone()],
&mut recv_chain,
Expand Down Expand Up @@ -501,7 +671,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
env.reorg_empty_blocks(depth)?;

env.wait_until_electrum_sees_block(Duration::from_secs(6))?;
let update = sync_with_electrum(
let update = sync_with_spks(
&client,
[spk_to_track.clone()],
&mut recv_chain,
Expand Down Expand Up @@ -549,7 +719,7 @@ fn test_sync_with_coinbase() -> anyhow::Result<()> {
env.wait_until_electrum_sees_block(Duration::from_secs(6))?;

// Check to see if electrum syncs properly.
assert!(sync_with_electrum(
assert!(sync_with_spks(
&client,
[spk_to_track.clone()],
&mut recv_chain,
Expand Down

0 comments on commit 27cccd7

Please sign in to comment.