From: 志宇 Date: Fri, 21 Feb 2025 10:36:17 +0000 (+1100) Subject: feat(esplora): Handle spks with expected txids X-Git-Tag: wallet-1.2.0~7^2~2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/database/scripts/static/struct.BitcoinPeerConfig.html?a=commitdiff_plain;h=f42d5a8549df89452d57f1f9cff8f6e8e8c23dbc;p=bdk feat(esplora): Handle spks with expected txids Also add `detect_receive_tx_cancel` test. Also rm `miniscript` dependency. Update ci to rm `miniscript/no-std` for `bdk_esplora`. Co-authored-by: Wei Chen --- diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index c287b88c..cd45ff29 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -96,7 +96,7 @@ jobs: - name: Check esplora working-directory: ./crates/esplora # TODO "--target thumbv6m-none-eabi" should work but currently does not - run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown + run: cargo check --no-default-features --features bdk_chain/hashbrown check-wasm: needs: prepare @@ -128,7 +128,7 @@ jobs: run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown - name: Check esplora working-directory: ./crates/esplora - run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async + run: cargo check --target wasm32-unknown-unknown --no-default-features --features bdk_core/hashbrown,async fmt: needs: prepare diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 004236cc..85054f2e 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -16,20 +16,19 @@ workspace = true [dependencies] bdk_core = { path = "../core", version = "0.4.1", default-features = false } -esplora-client = { version = "0.11.0", default-features = false } +esplora-client = { version = "0.11.0", default-features = false } async-trait = { version = "0.1.66", optional = true } futures = { version = "0.3.26", optional = true } -miniscript = { version = "12.0.0", optional = true, default-features = false } [dev-dependencies] -esplora-client = { version = "0.11.0" } +esplora-client = { version = "0.11.0" } bdk_chain = { path = "../chain" } bdk_testenv = { path = "../testenv" } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } [features] default = ["std", "async-https", "blocking-https"] -std = ["bdk_chain/std", "miniscript?/std"] +std = ["bdk_core/std"] tokio = ["esplora-client/tokio"] async = ["async-trait", "futures", "esplora-client/async"] async-https = ["async", "esplora-client/async-https"] diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 4cb34ad8..7d8460c5 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -1,8 +1,10 @@ use async_trait::async_trait; use bdk_core::collections::{BTreeMap, BTreeSet, HashSet}; -use bdk_core::spk_client::{FullScanRequest, FullScanResponse, SyncRequest, SyncResponse}; +use bdk_core::spk_client::{ + FullScanRequest, FullScanResponse, SpkWithExpectedTxids, SyncRequest, SyncResponse, +}; use bdk_core::{ - bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, + bitcoin::{BlockHash, OutPoint, Txid}, BlockId, CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate, }; use esplora_client::Sleeper; @@ -62,7 +64,7 @@ where stop_gap: usize, parallel_requests: usize, ) -> Result, Error> { - let mut request = request.into(); + let mut request: FullScanRequest = request.into(); let start_time = request.start_time(); let keychains = request.keychains(); @@ -77,7 +79,9 @@ where let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in keychains { - let keychain_spks = request.iter_spks(keychain.clone()); + let keychain_spks = request + .iter_spks(keychain.clone()) + .map(|(spk_i, spk)| (spk_i, spk.into())); let (update, last_active_index) = fetch_txs_with_keychain_spks( self, start_time, @@ -112,7 +116,7 @@ where request: R, parallel_requests: usize, ) -> Result { - let mut request = request.into(); + let mut request: SyncRequest = request.into(); let start_time = request.start_time(); let chain_tip = request.chain_tip(); @@ -129,7 +133,7 @@ where self, start_time, &mut inserted_txs, - request.iter_spks(), + request.iter_spks_with_expected_txids(), parallel_requests, ) .await?, @@ -291,10 +295,10 @@ async fn fetch_txs_with_keychain_spks( parallel_requests: usize, ) -> Result<(TxUpdate, Option), Error> where - I: Iterator> + Send, + I: Iterator> + Send, S: Sleeper + Clone + Send + Sync, { - type TxsOfSpkIndex = (u32, Vec); + type TxsOfSpkIndex = (u32, Vec, HashSet); let mut update = TxUpdate::::default(); let mut last_index = Option::::None; @@ -306,6 +310,8 @@ where .take(parallel_requests) .map(|(spk_index, spk)| { let client = client.clone(); + let expected_txids = spk.expected_txids; + let spk = spk.spk; async move { let mut last_seen = None; let mut spk_txs = Vec::new(); @@ -315,9 +321,15 @@ where last_seen = txs.last().map(|tx| tx.txid); spk_txs.extend(txs); if tx_count < 25 { - break Result::<_, Error>::Ok((spk_index, spk_txs)); + break; } } + let got_txids = spk_txs.iter().map(|tx| tx.txid).collect::>(); + let evicted_txids = expected_txids + .difference(&got_txids) + .copied() + .collect::>(); + Result::::Ok((spk_index, spk_txs, evicted_txids)) } }) .collect::>(); @@ -326,7 +338,7 @@ where break; } - for (index, txs) in handles.try_collect::>().await? { + for (index, txs, evicted) in handles.try_collect::>().await? { last_index = Some(index); if !txs.is_empty() { last_active_index = Some(index); @@ -338,6 +350,9 @@ where insert_anchor_or_seen_at_from_status(&mut update, start_time, tx.txid, tx.status); insert_prevouts(&mut update, tx.vin); } + update + .evicted_ats + .extend(evicted.into_iter().map(|txid| (txid, start_time))); } let last_index = last_index.expect("Must be set since handles wasn't empty."); @@ -370,7 +385,7 @@ async fn fetch_txs_with_spks( parallel_requests: usize, ) -> Result, Error> where - I: IntoIterator + Send, + I: IntoIterator + Send, I::IntoIter: Send, S: Sleeper + Clone + Send + Sync, { diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 36c97195..bee97fee 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,7 +1,9 @@ use bdk_core::collections::{BTreeMap, BTreeSet, HashSet}; -use bdk_core::spk_client::{FullScanRequest, FullScanResponse, SyncRequest, SyncResponse}; +use bdk_core::spk_client::{ + FullScanRequest, FullScanResponse, SpkWithExpectedTxids, SyncRequest, SyncResponse, +}; use bdk_core::{ - bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, + bitcoin::{BlockHash, OutPoint, Txid}, BlockId, CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate, }; use esplora_client::{OutputStatus, Tx}; @@ -53,7 +55,7 @@ impl EsploraExt for esplora_client::BlockingClient { stop_gap: usize, parallel_requests: usize, ) -> Result, Error> { - let mut request = request.into(); + let mut request: FullScanRequest = request.into(); let start_time = request.start_time(); let chain_tip = request.chain_tip(); @@ -67,7 +69,9 @@ impl EsploraExt for esplora_client::BlockingClient { let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in request.keychains() { - let keychain_spks = request.iter_spks(keychain.clone()); + let keychain_spks = request + .iter_spks(keychain.clone()) + .map(|(spk_i, spk)| (spk_i, spk.into())); let (update, last_active_index) = fetch_txs_with_keychain_spks( self, start_time, @@ -120,7 +124,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, start_time, &mut inserted_txs, - request.iter_spks(), + request.iter_spks_with_expected_txids(), parallel_requests, )?); tx_update.extend(fetch_txs_with_txids( @@ -254,7 +258,7 @@ fn chain_update( Ok(tip) } -fn fetch_txs_with_keychain_spks>>( +fn fetch_txs_with_keychain_spks>>( client: &esplora_client::BlockingClient, start_time: u64, inserted_txs: &mut HashSet, @@ -262,7 +266,7 @@ fn fetch_txs_with_keychain_spks>>( stop_gap: usize, parallel_requests: usize, ) -> Result<(TxUpdate, Option), Error> { - type TxsOfSpkIndex = (u32, Vec); + type TxsOfSpkIndex = (u32, Vec, HashSet); let mut update = TxUpdate::::default(); let mut last_index = Option::::None; @@ -273,21 +277,27 @@ fn fetch_txs_with_keychain_spks>>( .by_ref() .take(parallel_requests) .map(|(spk_index, spk)| { - std::thread::spawn({ - let client = client.clone(); - move || -> Result { - let mut last_seen = None; - let mut spk_txs = Vec::new(); - loop { - let txs = client.scripthash_txs(&spk, last_seen)?; - let tx_count = txs.len(); - last_seen = txs.last().map(|tx| tx.txid); - spk_txs.extend(txs); - if tx_count < 25 { - break Ok((spk_index, spk_txs)); - } + let client = client.clone(); + let expected_txids = spk.expected_txids; + let spk = spk.spk; + std::thread::spawn(move || -> Result { + let mut last_txid = None; + let mut spk_txs = Vec::new(); + loop { + let txs = client.scripthash_txs(&spk, last_txid)?; + let tx_count = txs.len(); + last_txid = txs.last().map(|tx| tx.txid); + spk_txs.extend(txs); + if tx_count < 25 { + break; } } + let got_txids = spk_txs.iter().map(|tx| tx.txid).collect::>(); + let evicted_txids = expected_txids + .difference(&got_txids) + .copied() + .collect::>(); + Ok((spk_index, spk_txs, evicted_txids)) }) }) .collect::>>>(); @@ -297,7 +307,7 @@ fn fetch_txs_with_keychain_spks>>( } for handle in handles { - let (index, txs) = handle.join().expect("thread must not panic")?; + let (index, txs, evicted) = handle.join().expect("thread must not panic")?; last_index = Some(index); if !txs.is_empty() { last_active_index = Some(index); @@ -309,6 +319,9 @@ fn fetch_txs_with_keychain_spks>>( insert_anchor_or_seen_at_from_status(&mut update, start_time, tx.txid, tx.status); insert_prevouts(&mut update, tx.vin); } + update + .evicted_ats + .extend(evicted.into_iter().map(|txid| (txid, start_time))); } let last_index = last_index.expect("Must be set since handles wasn't empty."); @@ -333,7 +346,7 @@ fn fetch_txs_with_keychain_spks>>( /// requests to make in parallel. /// /// Refer to [crate-level docs](crate) for more. -fn fetch_txs_with_spks>( +fn fetch_txs_with_spks>( client: &esplora_client::BlockingClient, start_time: u64, inserted_txs: &mut HashSet, diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index b535d2bf..987f04e4 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -1,15 +1,135 @@ +use bdk_chain::bitcoin::{Address, Amount}; +use bdk_chain::local_chain::LocalChain; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; -use bdk_chain::{ConfirmationBlockTime, TxGraph}; +use bdk_chain::spk_txout::SpkTxOutIndex; +use bdk_chain::{ConfirmationBlockTime, IndexedTxGraph, TxGraph}; use bdk_esplora::EsploraAsyncExt; +use bdk_testenv::bitcoincore_rpc::json::CreateRawTransactionInput; +use bdk_testenv::bitcoincore_rpc::RawTx; +use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use esplora_client::{self, Builder}; -use std::collections::{BTreeSet, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount}; -use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; +mod common; + +// Ensure that a wallet can detect a malicious replacement of an incoming transaction. +// +// This checks that both the Esplora chain source and the receiving structures properly track the +// replaced transaction as missing. +#[tokio::test] +pub async fn detect_receive_tx_cancel() -> anyhow::Result<()> { + const SEND_TX_FEE: Amount = Amount::from_sat(1000); + const UNDO_SEND_TX_FEE: Amount = Amount::from_sat(2000); + + let env = TestEnv::new()?; + let rpc_client = env.rpc_client(); + let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap()); + let client = Builder::new(base_url.as_str()).build_async()?; + + let mut graph = IndexedTxGraph::::new(SpkTxOutIndex::<()>::default()); + let (chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); + + // Get receiving address. + let receiver_spk = common::get_test_spk(); + let receiver_addr = Address::from_script(&receiver_spk, bdk_chain::bitcoin::Network::Regtest)?; + graph.index.insert_spk((), receiver_spk); + + env.mine_blocks(101, None)?; + + // Select a UTXO to use as an input for constructing our test transactions. + let selected_utxo = rpc_client + .list_unspent(None, None, None, Some(false), None)? + .into_iter() + // Find a block reward tx. + .find(|utxo| utxo.amount == Amount::from_int_btc(50)) + .expect("Must find a block reward UTXO"); + + // Derive the sender's address from the selected UTXO. + let sender_spk = selected_utxo.script_pub_key.clone(); + let sender_addr = Address::from_script(&sender_spk, bdk_chain::bitcoin::Network::Regtest) + .expect("Failed to derive address from UTXO"); + + // Setup the common inputs used by both `send_tx` and `undo_send_tx`. + let inputs = [CreateRawTransactionInput { + txid: selected_utxo.txid, + vout: selected_utxo.vout, + sequence: None, + }]; + + // Create and sign the `send_tx` that sends funds to the receiver address. + let send_tx_outputs = HashMap::from([( + receiver_addr.to_string(), + selected_utxo.amount - SEND_TX_FEE, + )]); + let send_tx = rpc_client.create_raw_transaction(&inputs, &send_tx_outputs, None, Some(true))?; + let send_tx = rpc_client + .sign_raw_transaction_with_wallet(send_tx.raw_hex(), None, None)? + .transaction()?; + // Create and sign the `undo_send_tx` transaction. This redirects funds back to the sender + // address. + let undo_send_outputs = HashMap::from([( + sender_addr.to_string(), + selected_utxo.amount - UNDO_SEND_TX_FEE, + )]); + let undo_send_tx = + rpc_client.create_raw_transaction(&inputs, &undo_send_outputs, None, Some(true))?; + let undo_send_tx = rpc_client + .sign_raw_transaction_with_wallet(undo_send_tx.raw_hex(), None, None)? + .transaction()?; + + // Sync after broadcasting the `send_tx`. Ensure that we detect and receive the `send_tx`. + let send_txid = env.rpc_client().send_raw_transaction(send_tx.raw_hex())?; + env.wait_until_electrum_sees_txid(send_txid, Duration::from_secs(6))?; + let sync_request = SyncRequest::builder() + .chain_tip(chain.tip()) + .spks_with_indexes(graph.index.all_spks().clone()) + .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..)); + let sync_response = client.sync(sync_request, 1).await?; + assert!( + sync_response + .tx_update + .txs + .iter() + .any(|tx| tx.compute_txid() == send_txid), + "sync response must include the send_tx" + ); + let changeset = graph.apply_update(sync_response.tx_update.clone()); + assert!( + changeset.tx_graph.txs.contains(&send_tx), + "tx graph must deem send_tx relevant and include it" + ); + + // Sync after broadcasting the `undo_send_tx`. Verify that `send_tx` is now missing from the + // mempool. + let undo_send_txid = env + .rpc_client() + .send_raw_transaction(undo_send_tx.raw_hex())?; + env.wait_until_electrum_sees_txid(undo_send_txid, Duration::from_secs(6))?; + let sync_request = SyncRequest::builder() + .chain_tip(chain.tip()) + .spks_with_indexes(graph.index.all_spks().clone()) + .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..)); + let sync_response = client.sync(sync_request, 1).await?; + assert!( + sync_response + .tx_update + .evicted_ats + .iter() + .any(|(txid, _)| *txid == send_txid), + "sync response must track send_tx as missing from mempool" + ); + let changeset = graph.apply_update(sync_response.tx_update.clone()); + assert!( + changeset.tx_graph.last_evicted.contains_key(&send_txid), + "tx graph must track send_tx as missing" + ); + + Ok(()) +} #[tokio::test] pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { let env = TestEnv::new()?; diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index d4191ceb..d6f8c448 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -1,14 +1,135 @@ +use bdk_chain::bitcoin::{Address, Amount}; +use bdk_chain::local_chain::LocalChain; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; -use bdk_chain::{ConfirmationBlockTime, TxGraph}; +use bdk_chain::spk_txout::SpkTxOutIndex; +use bdk_chain::{ConfirmationBlockTime, IndexedTxGraph, TxGraph}; use bdk_esplora::EsploraExt; +use bdk_testenv::bitcoincore_rpc::json::CreateRawTransactionInput; +use bdk_testenv::bitcoincore_rpc::RawTx; +use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use esplora_client::{self, Builder}; -use std::collections::{BTreeSet, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount}; -use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; +mod common; + +// Ensure that a wallet can detect a malicious replacement of an incoming transaction. +// +// This checks that both the Esplora chain source and the receiving structures properly track the +// replaced transaction as missing. +#[test] +pub fn detect_receive_tx_cancel() -> anyhow::Result<()> { + const SEND_TX_FEE: Amount = Amount::from_sat(1000); + const UNDO_SEND_TX_FEE: Amount = Amount::from_sat(2000); + + let env = TestEnv::new()?; + let rpc_client = env.rpc_client(); + let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap()); + let client = Builder::new(base_url.as_str()).build_blocking(); + + let mut graph = IndexedTxGraph::::new(SpkTxOutIndex::<()>::default()); + let (chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?); + + // Get receiving address. + let receiver_spk = common::get_test_spk(); + let receiver_addr = Address::from_script(&receiver_spk, bdk_chain::bitcoin::Network::Regtest)?; + graph.index.insert_spk((), receiver_spk); + + env.mine_blocks(101, None)?; + + // Select a UTXO to use as an input for constructing our test transactions. + let selected_utxo = rpc_client + .list_unspent(None, None, None, Some(false), None)? + .into_iter() + // Find a block reward tx. + .find(|utxo| utxo.amount == Amount::from_int_btc(50)) + .expect("Must find a block reward UTXO"); + + // Derive the sender's address from the selected UTXO. + let sender_spk = selected_utxo.script_pub_key.clone(); + let sender_addr = Address::from_script(&sender_spk, bdk_chain::bitcoin::Network::Regtest) + .expect("Failed to derive address from UTXO"); + + // Setup the common inputs used by both `send_tx` and `undo_send_tx`. + let inputs = [CreateRawTransactionInput { + txid: selected_utxo.txid, + vout: selected_utxo.vout, + sequence: None, + }]; + + // Create and sign the `send_tx` that sends funds to the receiver address. + let send_tx_outputs = HashMap::from([( + receiver_addr.to_string(), + selected_utxo.amount - SEND_TX_FEE, + )]); + let send_tx = rpc_client.create_raw_transaction(&inputs, &send_tx_outputs, None, Some(true))?; + let send_tx = rpc_client + .sign_raw_transaction_with_wallet(send_tx.raw_hex(), None, None)? + .transaction()?; + + // Create and sign the `undo_send_tx` transaction. This redirects funds back to the sender + // address. + let undo_send_outputs = HashMap::from([( + sender_addr.to_string(), + selected_utxo.amount - UNDO_SEND_TX_FEE, + )]); + let undo_send_tx = + rpc_client.create_raw_transaction(&inputs, &undo_send_outputs, None, Some(true))?; + let undo_send_tx = rpc_client + .sign_raw_transaction_with_wallet(undo_send_tx.raw_hex(), None, None)? + .transaction()?; + + // Sync after broadcasting the `send_tx`. Ensure that we detect and receive the `send_tx`. + let send_txid = env.rpc_client().send_raw_transaction(send_tx.raw_hex())?; + env.wait_until_electrum_sees_txid(send_txid, Duration::from_secs(6))?; + let sync_request = SyncRequest::builder() + .chain_tip(chain.tip()) + .spks_with_indexes(graph.index.all_spks().clone()) + .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..)); + let sync_response = client.sync(sync_request, 1)?; + assert!( + sync_response + .tx_update + .txs + .iter() + .any(|tx| tx.compute_txid() == send_txid), + "sync response must include the send_tx" + ); + let changeset = graph.apply_update(sync_response.tx_update.clone()); + assert!( + changeset.tx_graph.txs.contains(&send_tx), + "tx graph must deem send_tx relevant and include it" + ); + + // Sync after broadcasting the `undo_send_tx`. Verify that `send_tx` is now missing from the + // mempool. + let undo_send_txid = env + .rpc_client() + .send_raw_transaction(undo_send_tx.raw_hex())?; + env.wait_until_electrum_sees_txid(undo_send_txid, Duration::from_secs(6))?; + let sync_request = SyncRequest::builder() + .chain_tip(chain.tip()) + .spks_with_indexes(graph.index.all_spks().clone()) + .expected_spk_txids(graph.list_expected_spk_txids(&chain, chain.tip().block_id(), ..)); + let sync_response = client.sync(sync_request, 1)?; + assert!( + sync_response + .tx_update + .evicted_ats + .iter() + .any(|(txid, _)| *txid == send_txid), + "sync response must track send_tx as missing from mempool" + ); + let changeset = graph.apply_update(sync_response.tx_update.clone()); + assert!( + changeset.tx_graph.last_evicted.contains_key(&send_txid), + "tx graph must track send_tx as missing" + ); + + Ok(()) +} #[test] pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { diff --git a/crates/esplora/tests/common/mod.rs b/crates/esplora/tests/common/mod.rs new file mode 100644 index 00000000..c629c502 --- /dev/null +++ b/crates/esplora/tests/common/mod.rs @@ -0,0 +1,14 @@ +use bdk_core::bitcoin::key::{Secp256k1, UntweakedPublicKey}; +use bdk_core::bitcoin::ScriptBuf; + +const PK_BYTES: &[u8] = &[ + 12, 244, 72, 4, 163, 4, 211, 81, 159, 82, 153, 123, 125, 74, 142, 40, 55, 237, 191, 231, 31, + 114, 89, 165, 83, 141, 8, 203, 93, 240, 53, 101, +]; + +#[allow(dead_code)] +pub fn get_test_spk() -> ScriptBuf { + let secp = Secp256k1::new(); + let pk = UntweakedPublicKey::from_slice(PK_BYTES).expect("Must be valid PK"); + ScriptBuf::new_p2tr(&secp, pk, None) +}