diff --git a/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/stacks-node/src/burnchains/bitcoin_regtest_controller.rs
index 1e28b1ff1bd..5a54d6121c0 100644
--- a/stacks-node/src/burnchains/bitcoin_regtest_controller.rs
+++ b/stacks-node/src/burnchains/bitcoin_regtest_controller.rs
@@ -14,15 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+use std::cmp;
+use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
-use std::time::{Duration, Instant};
-use std::{cmp, io};
+use std::time::Instant;
-use base64::encode;
-use serde::Serialize;
-use serde_json::json;
-use serde_json::value::RawValue;
use stacks::burnchains::bitcoin::address::{
BitcoinAddress, LegacyBitcoinAddress, LegacyBitcoinAddressType, SegwitBitcoinAddress,
};
@@ -55,9 +52,6 @@ use stacks::config::{
};
use stacks::core::{EpochList, StacksEpochId};
use stacks::monitoring::{increment_btc_blocks_received_counter, increment_btc_ops_sent_counter};
-use stacks::net::http::{HttpRequestContents, HttpResponsePayload};
-use stacks::net::httpcore::{send_http_request, StacksHttpRequest};
-use stacks::net::Error as NetError;
use stacks_common::codec::StacksMessageCodec;
use stacks_common::deps_common::bitcoin::blockdata::opcodes;
use stacks_common::deps_common::bitcoin::blockdata::script::{Builder, Script};
@@ -67,17 +61,16 @@ use stacks_common::deps_common::bitcoin::blockdata::transaction::{
use stacks_common::deps_common::bitcoin::network::serialize::{serialize, serialize_hex};
use stacks_common::deps_common::bitcoin::util::hash::Sha256dHash;
use stacks_common::types::chainstate::BurnchainHeaderHash;
-use stacks_common::types::net::PeerHost;
use stacks_common::util::hash::{hex_bytes, Hash160};
use stacks_common::util::secp256k1::Secp256k1PublicKey;
use stacks_common::util::sleep_ms;
-use url::Url;
use super::super::operations::BurnchainOpSigner;
use super::super::Config;
use super::{BurnchainController, BurnchainTip, Error as BurnchainControllerError};
use crate::burnchains::rpc::bitcoin_rpc_client::{
- BitcoinRpcClient, BitcoinRpcClientError, ImportDescriptorsRequest, Timestamp,
+ BitcoinRpcClient, BitcoinRpcClientError, BitcoinRpcClientResult, ImportDescriptorsRequest,
+ Timestamp,
};
/// The number of bitcoin blocks that can have
@@ -663,97 +656,32 @@ impl BitcoinRegtestController {
}
}
+ /// Retrieves all UTXOs associated with the given public key.
+ ///
+ /// The address to query is computed from the public key,
+ /// disregard the epoch we're in and currently set to [`StacksEpochId::Epoch21`].
+ ///
+ /// Automatically imports descriptors into the wallet for the public_key
#[cfg(test)]
pub fn get_all_utxos(&self, public_key: &Secp256k1PublicKey) -> Vec {
- // Configure UTXO filter, disregard what epoch we're in
- let address = self.get_miner_address(StacksEpochId::Epoch21, public_key);
- let filter_addresses = vec![address.to_string()];
-
- let pubk = if self.config.miner.segwit {
- let mut p = public_key.clone();
- p.set_compressed(true);
- p
- } else {
- public_key.clone()
- };
-
- test_debug!("Import public key '{}'", &pubk.to_hex());
- let result = self.import_public_key(&pubk);
- if let Err(error) = result {
- warn!("Import public key '{}' failed: {error:?}", &pubk.to_hex());
- }
+ const EPOCH: StacksEpochId = StacksEpochId::Epoch21;
+ let address = self.get_miner_address(EPOCH, public_key);
+ let pub_key_rev = self.to_epoch_aware_pubkey(EPOCH, public_key);
+
+ test_debug!("Import public key '{}'", &pub_key_rev.to_hex());
+ self.import_public_key(&pub_key_rev)
+ .unwrap_or_else(|error| {
+ panic!(
+ "Import public key '{}' failed: {error:?}",
+ pub_key_rev.to_hex()
+ )
+ });
sleep_ms(1000);
- let min_conf = 0i64;
- let max_conf = 9999999i64;
- let minimum_amount = ParsedUTXO::sat_to_serialized_btc(1);
-
- test_debug!("List unspent for '{address}' ('{}')", pubk.to_hex());
- let payload = BitcoinRPCRequest {
- method: "listunspent".to_string(),
- params: vec![
- min_conf.into(),
- max_conf.into(),
- filter_addresses.into(),
- true.into(),
- json!({ "minimumAmount": minimum_amount, "maximumCount": self.config.burnchain.max_unspent_utxos }),
- ],
- id: "stacks".to_string(),
- jsonrpc: "2.0".to_string(),
- };
-
- let mut res = BitcoinRPCRequest::send(&self.config, payload).unwrap();
- let mut result_vec = vec![];
-
- if let Some(ref mut object) = res.as_object_mut() {
- match object.get_mut("result") {
- Some(serde_json::Value::Array(entries)) => {
- while let Some(entry) = entries.pop() {
- let parsed_utxo: ParsedUTXO = match serde_json::from_value(entry) {
- Ok(utxo) => utxo,
- Err(err) => {
- warn!("Failed parsing UTXO: {err}");
- continue;
- }
- };
- let amount = match parsed_utxo.get_sat_amount() {
- Some(amount) => amount,
- None => continue,
- };
-
- if amount < 1 {
- continue;
- }
-
- let script_pub_key = match parsed_utxo.get_script_pub_key() {
- Some(script_pub_key) => script_pub_key,
- None => {
- continue;
- }
- };
-
- let txid = match parsed_utxo.get_txid() {
- Some(amount) => amount,
- None => continue,
- };
-
- result_vec.push(UTXO {
- txid,
- vout: parsed_utxo.vout,
- script_pub_key,
- amount,
- confirmations: parsed_utxo.confirmations,
- });
- }
- }
- _ => {
- warn!("Failed to get UTXOs");
- }
- }
- }
-
- result_vec
+ self.retrieve_utxo_set(&address, true, 1, &None, 0)
+ .unwrap_or_log_panic("retrieve all utxos")
+ .utxos
}
/// Retrieve all loaded wallets.
@@ -780,23 +708,15 @@ impl BitcoinRegtestController {
utxos_to_exclude: Option,
block_height: u64,
) -> Option {
- let pubk = if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
- let mut p = public_key.clone();
- p.set_compressed(true);
- p
- } else {
- public_key.clone()
- };
+ let pub_key_rev = self.to_epoch_aware_pubkey(epoch_id, public_key);
// Configure UTXO filter
- let address = self.get_miner_address(epoch_id, &pubk);
- test_debug!("Get UTXOs for {} ({address})", pubk.to_hex());
- let filter_addresses = vec![address.to_string()];
+ let address = self.get_miner_address(epoch_id, &pub_key_rev);
+ test_debug!("Get UTXOs for {} ({address})", pub_key_rev.to_hex());
let mut utxos = loop {
- let result = BitcoinRPCRequest::list_unspent(
- &self.config,
- filter_addresses.clone(),
+ let result = self.retrieve_utxo_set(
+ &address,
false,
total_required,
&utxos_to_exclude,
@@ -824,16 +744,18 @@ impl BitcoinRegtestController {
// Assuming that miners are in charge of correctly operating their bitcoind nodes sounds
// reasonable to me.
// $ bitcoin-cli importaddress mxVFsFW5N4mu1HPkxPttorvocvzeZ7KZyk
- let result = self.import_public_key(&pubk);
+ let result = self.import_public_key(&pub_key_rev);
if let Err(error) = result {
- warn!("Import public key '{}' failed: {error:?}", &pubk.to_hex());
+ warn!(
+ "Import public key '{}' failed: {error:?}",
+ &pub_key_rev.to_hex()
+ );
}
sleep_ms(1000);
}
- let result = BitcoinRPCRequest::list_unspent(
- &self.config,
- filter_addresses.clone(),
+ let result = self.retrieve_utxo_set(
+ &address,
false,
total_required,
&utxos_to_exclude,
@@ -849,7 +771,7 @@ impl BitcoinRegtestController {
}
};
- test_debug!("Unspent for {filter_addresses:?}: {utxos:?}");
+ test_debug!("Unspent for {address:?}: {utxos:?}");
if utxos.is_empty() {
return None;
@@ -858,7 +780,7 @@ impl BitcoinRegtestController {
}
}
} else {
- debug!("Got {} UTXOs for {filter_addresses:?}", utxos.utxos.len(),);
+ debug!("Got {} UTXOs for {address:?}", utxos.utxos.len(),);
utxos
};
@@ -866,7 +788,7 @@ impl BitcoinRegtestController {
if total_unspent < total_required {
warn!(
"Total unspent {total_unspent} < {total_required} for {:?}",
- &pubk.to_hex()
+ &pub_key_rev.to_hex()
);
return None;
}
@@ -2249,6 +2171,97 @@ impl BitcoinRegtestController {
}
Ok(())
}
+
+ /// Returns a copy of the given public key adjusted to the current epoch rules.
+ ///
+ /// In particular:
+ /// - For epochs **before** [`StacksEpochId::Epoch21`], the public key is returned
+ /// unchanged.
+ /// - Starting with [`StacksEpochId::Epoch21`], if **SegWit** is enabled in the miner
+ /// configuration, the key is forced into compressed form.
+ ///
+ /// # Arguments
+ /// * `epoch_id` — The epoch identifier to check against protocol upgrade rules.
+ /// * `public_key` — The original public key to adjust.
+ ///
+ /// # Returns
+ /// A [`Secp256k1PublicKey`] that is either the same as the input or compressed,
+ /// depending on the epoch and miner configuration.
+ fn to_epoch_aware_pubkey(
+ &self,
+ epoch_id: StacksEpochId,
+ public_key: &Secp256k1PublicKey,
+ ) -> Secp256k1PublicKey {
+ let mut reviewed = public_key.clone();
+ if self.config.miner.segwit && epoch_id >= StacksEpochId::Epoch21 {
+ reviewed.set_compressed(true);
+ }
+ return reviewed;
+ }
+
+ /// Retrieves the set of UTXOs for a given address at a specific block height.
+ ///
+ /// This method queries all unspent outputs belonging to the provided address:
+ /// 1. Using a confirmation window of `0..=9_999_999` for the RPC call.
+ /// 2. Filtering out UTXOs that:
+ /// - Are present in the optional exclusion set (matched by transaction ID).
+ /// - Have an amount below the specified `minimum_sum_amount`.
+ ///
+ /// Note: The `block_height` is only used to retrieve the corresponding block hash
+ /// and does not affect which UTXOs are included in the result.
+ ///
+ /// # Arguments
+ /// - `address`: The Bitcoin address whose UTXOs should be retrieved.
+ /// - `include_unsafe`: Whether to include unsafe UTXOs.
+ /// - `minimum_sum_amount`: Minimum amount (in satoshis) that a UTXO must have to be included in the final set.
+ /// - `utxos_to_exclude`: Optional set of UTXOs to exclude from the final result.
+ /// - `block_height`: The block height at which to resolve the block hash used in the result.
+ ///
+ /// # Returns
+ /// A [`UTXOSet`] containing the filtered UTXOs and the block hash corresponding to `block_height`.
+ fn retrieve_utxo_set(
+ &self,
+ address: &BitcoinAddress,
+ include_unsafe: bool,
+ minimum_sum_amount: u64,
+ utxos_to_exclude: &Option,
+ block_height: u64,
+ ) -> BitcoinRpcClientResult {
+ let bhh = self.rpc_client.get_block_hash(block_height)?;
+
+ const MIN_CONFIRMATIONS: u64 = 0;
+ const MAX_CONFIRMATIONS: u64 = 9_999_999;
+ let unspents = self.rpc_client.list_unspent(
+ &self.get_wallet_name(),
+ Some(MIN_CONFIRMATIONS),
+ Some(MAX_CONFIRMATIONS),
+ Some(&[address]),
+ Some(include_unsafe),
+ Some(minimum_sum_amount),
+ self.config.burnchain.max_unspent_utxos.clone(),
+ )?;
+
+ let txids_to_exclude = utxos_to_exclude.as_ref().map_or_else(HashSet::new, |set| {
+ set.utxos
+ .iter()
+ .map(|utxo| Txid::from_bitcoin_tx_hash(&utxo.txid))
+ .collect()
+ });
+
+ let utxos = unspents
+ .into_iter()
+ .filter(|each| !txids_to_exclude.contains(&each.txid))
+ .filter(|each| each.amount >= minimum_sum_amount)
+ .map(|each| UTXO {
+ txid: Txid::to_bitcoin_tx_hash(&each.txid),
+ vout: each.vout,
+ script_pub_key: each.script_pub_key,
+ amount: each.amount,
+ confirmations: each.confirmations,
+ })
+ .collect::>();
+ Ok(UTXOSet { bhh, utxos })
+ }
}
impl BurnchainController for BitcoinRegtestController {
@@ -2393,17 +2406,6 @@ impl UTXOSet {
}
}
-#[derive(Debug, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-#[allow(dead_code)]
-pub struct ParsedUTXO {
- txid: String,
- vout: u32,
- script_pub_key: String,
- amount: Box,
- confirmations: u32,
-}
-
#[derive(Clone, Debug, PartialEq)]
pub struct UTXO {
pub txid: Sha256dHash,
@@ -2413,280 +2415,6 @@ pub struct UTXO {
pub confirmations: u32,
}
-impl ParsedUTXO {
- pub fn get_txid(&self) -> Option {
- match hex_bytes(&self.txid) {
- Ok(ref mut txid) => {
- txid.reverse();
- Some(Sha256dHash::from(&txid[..]))
- }
- Err(err) => {
- warn!("Unable to get txid from UTXO {err}");
- None
- }
- }
- }
-
- pub fn get_sat_amount(&self) -> Option {
- ParsedUTXO::serialized_btc_to_sat(self.amount.get())
- }
-
- pub fn serialized_btc_to_sat(amount: &str) -> Option {
- let comps: Vec<&str> = amount.split('.').collect();
- match comps[..] {
- [lhs, rhs] => {
- if rhs.len() > 8 {
- warn!("Unexpected amount of decimals");
- return None;
- }
-
- match (lhs.parse::(), rhs.parse::()) {
- (Ok(btc), Ok(frac_part)) => {
- let base: u64 = 10;
- let btc_to_sat = base.pow(8);
- let mut amount = btc * btc_to_sat;
- let sat = frac_part * base.pow(8 - rhs.len() as u32);
- amount += sat;
- Some(amount)
- }
- (lhs, rhs) => {
- warn!("Error while converting BTC to sat {lhs:?} - {rhs:?}");
- None
- }
- }
- }
- _ => None,
- }
- }
-
- pub fn sat_to_serialized_btc(amount: u64) -> String {
- let base: u64 = 10;
- let int_part = amount / base.pow(8);
- let frac_part = amount % base.pow(8);
- let amount = format!("{int_part}.{frac_part:08}");
- amount
- }
-
- pub fn get_script_pub_key(&self) -> Option