From f75b91678391062c1f055ebc7458a394a44070ea Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 27 May 2025 15:43:14 -0400 Subject: [PATCH 01/18] fix: do not break UTXO chain Bad timing between the Stacks node's processing and the Bitcoin network including a block commit can cause a miner to submit a block commit which breaks the UTXO chain, severly hurting its chances of winning blocks. It is better to just miss this commit than to break the UTXO chain. --- CHANGELOG.md | 6 ++ .../burnchains/bitcoin_regtest_controller.rs | 61 ++++++++----------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed937e7ed..66bcd94891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## Unreleased + +### Changed + +- When a previous block commit is unable to be RBFed, the miner will now just wait for it to be confirmed instead of submitting a new block commit which breaks the miner's UTXO chain. + ## [3.1.0.0.11] - Hotfix for p2p stack misbehavior in mempool syncing conditions diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 7fa78d55d0..fbbd63352c 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -1539,6 +1539,12 @@ impl BitcoinRegtestController { Ok(true) ); if ongoing_tx_confirmed { + if ongoing_op.payload == payload { + info!("Abort attempt to re-submit confirmed LeaderBlockCommit"); + self.ongoing_block_commit = Some(ongoing_op); + return Err(BurnchainControllerError::IdenticalOperation); + } + debug!("Was able to retrieve confirmation of ongoing burnchain TXID - {txid}"); let res = self.send_block_commit_operation( epoch_id, @@ -1600,49 +1606,30 @@ impl BitcoinRegtestController { } // An ongoing operation is in the mempool and we received a new block. The desired behaviour is the following: - // 1) If the ongoing and the incoming operation are **strictly** identical, we will be idempotent and discard the incoming. - // 2) If the 2 operations are different, we will try to avoid wasting UTXOs, and attempt to RBF the outgoing transaction: - // i) If UTXOs are insufficient, - // a) If no other UTXOs, we'll have to wait on the ongoing operation to be mined before resuming operation. - // b) If we have some other UTXOs, drop the ongoing operation, and track the new one. - // ii) If UTXOs initially used are sufficient for paying for a fee bump, then RBF - - // Let's start by early returning 1) + // (1) If the ongoing and the incoming operation are **strictly** identical, we will be idempotent and discard the incoming. + // (2) If the 2 operations are different, attempt to RBF the outgoing transaction: + + // Let's start by early returning (1) if payload == ongoing_op.payload { info!("Abort attempt to re-submit identical LeaderBlockCommit"); self.ongoing_block_commit = Some(ongoing_op); return Err(BurnchainControllerError::IdenticalOperation); } - // Let's proceed and early return 2) i) - let res = if ongoing_op.fees.estimated_amount_required() > ongoing_op.sum_utxos() { - // Try to build and submit op, excluding UTXOs currently used - info!("Attempt to submit another leader_block_commit, despite an ongoing (outdated) commit"); - self.send_block_commit_operation( - epoch_id, - payload, - signer, - None, - Some(ongoing_op.utxos.clone()), - None, - &[], - ) - } else { - // Case 2) ii): Attempt to RBF - info!( - "Attempt to replace by fee an outdated leader block commit"; - "ongoing_txids" => ?ongoing_op.txids - ); - self.send_block_commit_operation( - epoch_id, - payload, - signer, - Some(ongoing_op.utxos.clone()), - None, - Some(ongoing_op.fees.clone()), - &ongoing_op.txids, - ) - }; + // If we reach this point, we are attempting to RBF the ongoing operation (2) + info!( + "Attempt to replace by fee an outdated leader block commit"; + "ongoing_txids" => ?ongoing_op.txids + ); + let res = self.send_block_commit_operation( + epoch_id, + payload, + signer, + Some(ongoing_op.utxos.clone()), + None, + Some(ongoing_op.fees.clone()), + &ongoing_op.txids, + ); if res.is_err() { self.ongoing_block_commit = Some(ongoing_op); From 52fcf783ab9712fea432164b0cc640371133b2fd Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 29 May 2025 11:40:16 -0400 Subject: [PATCH 02/18] chore: remove unused method --- .../src/burnchains/bitcoin_regtest_controller.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index fbbd63352c..58ac53778e 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -112,12 +112,6 @@ pub struct OngoingBlockCommit { txids: Vec, } -impl OngoingBlockCommit { - fn sum_utxos(&self) -> u64 { - self.utxos.total_available() - } -} - #[derive(Clone)] struct LeaderBlockCommitFees { sunset_fee: u64, From 301de11d68e67a5057b9f4dde294e809d7a1c3d3 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Tue, 24 Jun 2025 16:30:29 +0200 Subject: [PATCH 03/18] test: add tests #6157 --- .../burnchains/bitcoin_regtest_controller.rs | 150 ++++++++++++++++++ testnet/stacks-node/src/tests/mod.rs | 2 +- 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 58ac53778e..909ad3e65e 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2832,6 +2832,9 @@ mod tests { use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::Secp256k1PrivateKey; + use crate::tests::bitcoin_regtest::BitcoinCoreController; + use crate::Keychain; + use super::*; #[test] @@ -3007,4 +3010,151 @@ mod tests { assert_eq!(&SerializedTx::new(block_commit).to_hex(), "0100000002eeda098987728e4a2e21b34b74000dcb0bd0e4d20e55735492ec3cba3afbead3030000006a4730440220558286e20e10ce31537f0625dae5cc62fac7961b9d2cf272c990de96323d7e2502202255adbea3d2e0509b80c5d8a3a4fe6397a87bcf18da1852740d5267d89a0cb20121035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7cfdffffff243b0b329a5889ab8801b315eea19810848d4c2133e0245671cc984a2d2f1301000000006a47304402206d9f8de107f9e1eb15aafac66c2bb34331a7523260b30e18779257e367048d34022013c7dabb32a5c281aa00d405e2ccbd00f34f03a65b2336553a4acd6c52c251ef0121035379aa40c02890d253cfa577964116eb5295570ae9f7287cbae5f2585f5b2c7cfdffffff040000000000000000536a4c5054335be88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32afd5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375000008a300010000059800015a10270000000000001976a914000000000000000000000000000000000000000088ac10270000000000001976a914000000000000000000000000000000000000000088acb3ef0400000000001976a9141dc27eba0247f8cc9575e7d45e50a0bc7e72427d88ac00000000"); } + + #[test] + fn test_create_wallet_if_not_exists() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller.start_bitcoind().expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller + .create_wallet_if_dne() + .expect("Wallet should now exists!"); + } + + #[test] + fn test_get_all_utxos() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller.start_bitcoind().expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); + + let utxos = btc_controller.get_all_utxos(&miner_pubkey); + assert_eq!(1, utxos.len()); + } + + #[test] + //NOTE: STALL if burn block at block_height doesn't exist.... + fn test_get_utxos() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller.start_bitcoind().expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); + + let utxos = + btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 19000, None, 0); + + let uxto_set = utxos.expect("Shouldn't be None!"); + assert_eq!(1, uxto_set.num_utxos()); + assert_eq!(5000000000, uxto_set.total_available()); + let utxo = &uxto_set.utxos[0]; + assert_eq!(101, utxo.confirmations); + assert_eq!(5000000000, utxo.amount); + } + + #[test] + fn test_build_leader_block_commit_tx() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller.start_bitcoind().unwrap(); + + let mut btc_controller = BitcoinRegtestController::new(config, None); + btc_controller.bootstrap_chain(101); + + btc_controller.connect_dbs().expect("Cannot initialize dbs!"); + + let tip = btc_controller + .get_burnchain() + .open_burnchain_db(false) + .unwrap() + .get_canonical_chain_tip(); + info!("{:?}", tip); + + + let mut signer = keychain.generate_op_signer(); + + let commit_op = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_hex( + "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", + ) + .unwrap(), + new_seed: VRFSeed::from_hex( + "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", + ) + .unwrap(), + parent_block_ptr: 2211, // 0x000008a3 + parent_vtxindex: 1, // 0x0001 + key_block_ptr: 1432, // 0x00000598 + key_vtxindex: 1, // 0x0001 + memo: vec![11], // 0x5a >> 3 + + burn_fee: 0, + input: (Txid([0x00; 32]), 0), + burn_parent_modulus: 2, // 0x5a & 0b111 + + apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()), + commit_outs: vec![ + PoxAddress::Standard(StacksAddress::burn_address(false), None), + PoxAddress::Standard(StacksAddress::burn_address(false), None), + ], + + treatment: vec![], + sunset_burn: 0, + + txid: Txid([0x00; 32]), + vtxindex: 0, + block_height: 2212, //FDF + burn_header_hash: BurnchainHeaderHash([0x01; 32]), + }; + + let tx = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) + .expect("Build leader block commit should work"); + + assert_eq!(1, tx.version); + assert_eq!(0, tx.lock_time); + assert_eq!(1, tx.input.len()); + assert_eq!(4, tx.output.len()); + } } diff --git a/testnet/stacks-node/src/tests/mod.rs b/testnet/stacks-node/src/tests/mod.rs index 659f82aaea..e4286e6334 100644 --- a/testnet/stacks-node/src/tests/mod.rs +++ b/testnet/stacks-node/src/tests/mod.rs @@ -44,7 +44,7 @@ use crate::tests::neon_integrations::{get_chain_info, next_block_and_wait}; use crate::BitcoinRegtestController; mod atlas; -mod bitcoin_regtest; +pub mod bitcoin_regtest; mod epoch_205; mod epoch_21; mod epoch_22; From 52363158e5ff56726037d3efd1b6dc8815f5eb61 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Wed, 25 Jun 2025 10:30:13 +0200 Subject: [PATCH 04/18] test: add commit re-submit case --- .../burnchains/bitcoin_regtest_controller.rs | 122 ++++++++++++++---- testnet/stacks-node/src/burnchains/mod.rs | 17 +++ 2 files changed, 116 insertions(+), 23 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 909ad3e65e..819113c9b3 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2832,11 +2832,10 @@ mod tests { use stacks_common::util::hash::to_hex; use stacks_common::util::secp256k1::Secp256k1PrivateKey; + use super::*; use crate::tests::bitcoin_regtest::BitcoinCoreController; use crate::Keychain; - use super::*; - #[test] fn test_get_satoshis_per_byte() { let dir = temp_dir(); @@ -3024,7 +3023,9 @@ mod tests { config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); - btcd_controller.start_bitcoind().expect("bitcoind should be started!"); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller @@ -3045,7 +3046,9 @@ mod tests { config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); - btcd_controller.start_bitcoind().expect("bitcoind should be started!"); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); @@ -3068,13 +3071,14 @@ mod tests { config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); - btcd_controller.start_bitcoind().expect("bitcoind should be started!"); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); - let utxos = - btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 19000, None, 0); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 19000, None, 0); let uxto_set = utxos.expect("Shouldn't be None!"); assert_eq!(1, uxto_set.num_utxos()); @@ -3085,10 +3089,12 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx() { + fn test_build_leader_block_commit_tx_ok_with_new_block_commit() { let miner_seed = vec![1, 1, 1, 1]; let keychain = Keychain::default(miner_seed.clone()); let miner_pubkey = keychain.get_pub_key(); + let mut signer = keychain.generate_op_signer(); + let burn_signer = keychain.get_burnchain_signer(); let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); @@ -3097,22 +3103,15 @@ mod tests { config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); - btcd_controller.start_bitcoind().unwrap(); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); let mut btc_controller = BitcoinRegtestController::new(config, None); btc_controller.bootstrap_chain(101); - - btc_controller.connect_dbs().expect("Cannot initialize dbs!"); - - let tip = btc_controller - .get_burnchain() - .open_burnchain_db(false) - .unwrap() - .get_canonical_chain_tip(); - info!("{:?}", tip); - - - let mut signer = keychain.generate_op_signer(); + btc_controller + .connect_dbs() + .expect("Cannot initialize dbs!"); let commit_op = LeaderBlockCommitOp { block_header_hash: BlockHeaderHash::from_hex( @@ -3133,7 +3132,7 @@ mod tests { input: (Txid([0x00; 32]), 0), burn_parent_modulus: 2, // 0x5a & 0b111 - apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()), + apparent_sender: burn_signer, commit_outs: vec![ PoxAddress::Standard(StacksAddress::burn_address(false), None), PoxAddress::Standard(StacksAddress::burn_address(false), None), @@ -3144,7 +3143,7 @@ mod tests { txid: Txid([0x00; 32]), vtxindex: 0, - block_height: 2212, //FDF + block_height: 2212, burn_header_hash: BurnchainHeaderHash([0x01; 32]), }; @@ -3157,4 +3156,81 @@ mod tests { assert_eq!(1, tx.input.len()); assert_eq!(4, tx.output.len()); } + + #[test] + fn test_build_leader_block_commit_tx_fails_resubmitting_same_block_commit() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + let mut signer = keychain.generate_op_signer(); + let burn_signer = keychain.get_burnchain_signer(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let mut btc_controller = BitcoinRegtestController::new(config, None); + btc_controller.bootstrap_chain(101); + btc_controller + .connect_dbs() + .expect("Cannot initialize dbs!"); + + let commit_op = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_hex( + "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", + ) + .unwrap(), + new_seed: VRFSeed::from_hex( + "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", + ) + .unwrap(), + parent_block_ptr: 2211, // 0x000008a3 + parent_vtxindex: 1, // 0x0001 + key_block_ptr: 1432, // 0x00000598 + key_vtxindex: 1, // 0x0001 + memo: vec![11], // 0x5a >> 3 + + burn_fee: 0, + input: (Txid([0x00; 32]), 0), + burn_parent_modulus: 2, // 0x5a & 0b111 + + apparent_sender: burn_signer, + commit_outs: vec![ + PoxAddress::Standard(StacksAddress::burn_address(false), None), + PoxAddress::Standard(StacksAddress::burn_address(false), None), + ], + + treatment: vec![], + sunset_burn: 0, + + txid: Txid([0x00; 32]), + vtxindex: 0, + block_height: 2212, + burn_header_hash: BurnchainHeaderHash([0x01; 32]), + }; + + let _first_tx_ok = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .expect("Build leader block commit should work"); + + let resubmit = btc_controller.build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op, + &mut signer, + 0, + ); + + assert!(resubmit.is_err()); + assert_eq!( + BurnchainControllerError::IdenticalOperation, + resubmit.unwrap_err() + ); + } } diff --git a/testnet/stacks-node/src/burnchains/mod.rs b/testnet/stacks-node/src/burnchains/mod.rs index 3a25eb747f..11b946b0cc 100644 --- a/testnet/stacks-node/src/burnchains/mod.rs +++ b/testnet/stacks-node/src/burnchains/mod.rs @@ -35,6 +35,23 @@ pub enum Error { SerializerError(CodecError), } +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + use Error::*; + match (self, other) { + (CoordinatorClosed, CoordinatorClosed) + | (IndexerError(_), IndexerError(_)) + | (BurnchainError, BurnchainError) + | (MaxFeeRateExceeded, MaxFeeRateExceeded) + | (IdenticalOperation, IdenticalOperation) + | (NoUTXOs, NoUTXOs) + | (TransactionSubmissionFailed(_), TransactionSubmissionFailed(_)) + | (SerializerError(_), SerializerError(_)) => true, + _ => false, + } + } +} + pub trait BurnchainController { fn start(&mut self, target_block_height_opt: Option) -> Result<(BurnchainTip, u64), Error>; From fa2205b1f5555e577454b77c61ed4b3317defec7 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Wed, 25 Jun 2025 12:55:16 +0200 Subject: [PATCH 05/18] test: add commit rbf case --- .../burnchains/bitcoin_regtest_controller.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 819113c9b3..8fc3008858 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3233,4 +3233,82 @@ mod tests { resubmit.unwrap_err() ); } + + #[test] + fn test_build_leader_block_commit_tx_ok_rbf_block_commit() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + let mut signer = keychain.generate_op_signer(); + let burn_signer = keychain.get_burnchain_signer(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let mut btc_controller = BitcoinRegtestController::new(config, None); + btc_controller.bootstrap_chain(101); + btc_controller + .connect_dbs() + .expect("Cannot initialize dbs!"); + + let mut commit_op = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_hex( + "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", + ) + .unwrap(), + new_seed: VRFSeed::from_hex( + "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", + ) + .unwrap(), + parent_block_ptr: 2211, // 0x000008a3 + parent_vtxindex: 1, // 0x0001 + key_block_ptr: 1432, // 0x00000598 + key_vtxindex: 1, // 0x0001 + memo: vec![11], // 0x5a >> 3 + + burn_fee: 0, + input: (Txid([0x00; 32]), 0), + burn_parent_modulus: 2, // 0x5a & 0b111 + + apparent_sender: burn_signer, + commit_outs: vec![ + PoxAddress::Standard(StacksAddress::burn_address(false), None), + PoxAddress::Standard(StacksAddress::burn_address(false), None), + ], + + treatment: vec![], + sunset_burn: 0, + + txid: Txid([0x00; 32]), + vtxindex: 0, + block_height: 2212, + burn_header_hash: BurnchainHeaderHash([0x01; 32]), + }; + + let _first_tx_ok = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .expect("Build leader block commit should work"); + + //re-gen signer othewise fails because it will be disposed during previous commit tx. + let mut signer = keychain.generate_op_signer(); + //small change to the commit op payload + commit_op.burn_fee = 2; + + let rbf_tx = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) + .expect("Commit tx should be rbf-ed"); + + assert_eq!(1, rbf_tx.version); + assert_eq!(0, rbf_tx.lock_time); + assert_eq!(1, rbf_tx.input.len()); + assert_eq!(4, rbf_tx.output.len()); + } } From 0a58e58c2394e370ce32fd56682583db22dc430c Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Wed, 25 Jun 2025 13:35:00 +0200 Subject: [PATCH 06/18] test: improve utxo tests --- .../burnchains/bitcoin_regtest_controller.rs | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 8fc3008858..0cf819908f 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3034,7 +3034,7 @@ mod tests { } #[test] - fn test_get_all_utxos() { + fn test_get_all_utxos_with_one_confirmed() { let miner_seed = vec![1, 1, 1, 1]; let keychain = Keychain::default(miner_seed.clone()); let miner_pubkey = keychain.get_pub_key(); @@ -3055,11 +3055,13 @@ mod tests { let utxos = btc_controller.get_all_utxos(&miner_pubkey); assert_eq!(1, utxos.len()); + let utxo = &utxos[0]; + assert_eq!(101, utxo.confirmations); + assert_eq!(5000000000, utxo.amount); } #[test] - //NOTE: STALL if burn block at block_height doesn't exist.... - fn test_get_utxos() { + fn test_get_utxos_ok() { let miner_seed = vec![1, 1, 1, 1]; let keychain = Keychain::default(miner_seed.clone()); let miner_pubkey = keychain.get_pub_key(); @@ -3078,7 +3080,7 @@ mod tests { let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 19000, None, 0); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 10000, None, 0); let uxto_set = utxos.expect("Shouldn't be None!"); assert_eq!(1, uxto_set.num_utxos()); @@ -3088,6 +3090,45 @@ mod tests { assert_eq!(5000000000, utxo.amount); } + + #[test] + //NOTE: STALL if burn block at block_height doesn't exist.... + fn test_get_utxos_fails_due_to_filtering() { + let miner_seed = vec![1, 1, 1, 1]; + let keychain = Keychain::default(miner_seed.clone()); + let miner_pubkey = keychain.get_pub_key(); + + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + config.burnchain.username = Some("user".to_owned()); + config.burnchain.password = Some("12345".to_owned()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); + + let too_much_required = 1000000000000000000_u64; + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, too_much_required, None, 0); + assert!(utxos.is_none(), "None because too much required"); + + let other_pubkey = Secp256k1PublicKey::from_hex("01010101010101100101010101").unwrap(); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &other_pubkey, too_much_required, None, 0); + assert!(utxos.is_none(), "None because utxos for other pubkey don't exist"); + + let future_block_height = 1000; + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, too_much_required, None, future_block_height); + assert!(utxos.is_none(), "None because utxos for future block height don't exist"); + + let existent_utxo = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, 0).expect("utxo set should exist"); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, Some(existent_utxo), 0); + assert!(utxos.is_none(), "None because utxos filtering out existent utxo set"); + } + #[test] fn test_build_leader_block_commit_tx_ok_with_new_block_commit() { let miner_seed = vec![1, 1, 1, 1]; From 24f60d8ed061670b3cf7fd0d03a18d65e0b6072a Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Wed, 25 Jun 2025 13:42:41 +0200 Subject: [PATCH 07/18] test: improve tests --- .../burnchains/bitcoin_regtest_controller.rs | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 0cf819908f..44b8938877 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3090,9 +3090,7 @@ mod tests { assert_eq!(5000000000, utxo.amount); } - #[test] - //NOTE: STALL if burn block at block_height doesn't exist.... fn test_get_utxos_fails_due_to_filtering() { let miner_seed = vec![1, 1, 1, 1]; let keychain = Keychain::default(miner_seed.clone()); @@ -3113,20 +3111,49 @@ mod tests { btc_controller.bootstrap_chain(101); let too_much_required = 1000000000000000000_u64; - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, too_much_required, None, 0); + let utxos = btc_controller.get_utxos( + StacksEpochId::Epoch31, + &miner_pubkey, + 0, + None, + 0, + ); assert!(utxos.is_none(), "None because too much required"); - let other_pubkey = Secp256k1PublicKey::from_hex("01010101010101100101010101").unwrap(); - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &other_pubkey, too_much_required, None, 0); - assert!(utxos.is_none(), "None because utxos for other pubkey don't exist"); + let other_pubkey = Secp256k1PublicKey::from_hex("04ee0b1602eb18fef7986887a7e8769a30c9df981d33c8380d255edef003abdcd243a0eb74afdf6740e6c423e62aec631519a24cf5b1d62bf8a3e06ddc695dcb77").unwrap(); + let utxos = btc_controller.get_utxos( + StacksEpochId::Epoch31, + &other_pubkey, + too_much_required, + None, + 0, + ); + assert!( + utxos.is_none(), + "None because utxos for other pubkey don't exist" + ); + + let existent_utxo = btc_controller + .get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, 0) + .expect("utxo set should exist"); + let utxos = btc_controller.get_utxos( + StacksEpochId::Epoch31, + &miner_pubkey, + 0, + Some(existent_utxo), + 0, + ); + assert!( + utxos.is_none(), + "None because filtering exclude existent utxo set" + ); + //NOTE: Operation stall if burn block at block_height doesn't exist + /* let future_block_height = 1000; - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, too_much_required, None, future_block_height); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, future_block_height); assert!(utxos.is_none(), "None because utxos for future block height don't exist"); - - let existent_utxo = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, 0).expect("utxo set should exist"); - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, Some(existent_utxo), 0); - assert!(utxos.is_none(), "None because utxos filtering out existent utxo set"); + */ } #[test] From 29af8dda461b2c9224eb5636bcfbf152e4f8bba4 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Wed, 25 Jun 2025 16:30:33 +0200 Subject: [PATCH 08/18] test: make tests windows compliant --- .../burnchains/bitcoin_regtest_controller.rs | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 44b8938877..0c0d7e6938 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3011,26 +3011,29 @@ mod tests { } #[test] - fn test_create_wallet_if_not_exists() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); - let miner_pubkey = keychain.get_pub_key(); - + fn test_create_wallet_from_empty_name_default() { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() .expect("bitcoind should be started!"); + let wallets = BitcoinRPCRequest::list_wallets(&config).unwrap(); + assert_eq!(0, wallets.len()); + let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller .create_wallet_if_dne() .expect("Wallet should now exists!"); + + let wallets = BitcoinRPCRequest::list_wallets(&config).unwrap(); + assert_eq!(1, wallets.len()); + assert_eq!("".to_owned(), wallets[0]); } #[test] @@ -3042,8 +3045,9 @@ mod tests { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller @@ -3069,8 +3073,9 @@ mod tests { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller @@ -3098,9 +3103,10 @@ mod tests { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller @@ -3166,10 +3172,11 @@ mod tests { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3235,9 +3242,10 @@ mod tests { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller @@ -3312,9 +3320,10 @@ mod tests { let mut config = Config::default(); config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + config.burnchain.peer_host = String::from("127.0.0.1"); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some("user".to_owned()); - config.burnchain.password = Some("12345".to_owned()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller From 952877fc679b9729380c68b083919d7f5366a729 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Fri, 27 Jun 2025 11:38:41 +0200 Subject: [PATCH 09/18] test: add more tests and refactor --- .../burnchains/bitcoin_regtest_controller.rs | 483 +++++++++++------- 1 file changed, 297 insertions(+), 186 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 0c0d7e6938..15d04ab2e0 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -1532,6 +1532,8 @@ impl BitcoinRegtestController { BitcoinRPCRequest::check_transaction_confirmed(&self.config, txid), Ok(true) ); + + test_debug!("Ongoing Tx confirmed: {ongoing_tx_confirmed} - TXID: {txid}"); if ongoing_tx_confirmed { if ongoing_op.payload == payload { info!("Abort attempt to re-submit confirmed LeaderBlockCommit"); @@ -2836,6 +2838,74 @@ mod tests { use crate::tests::bitcoin_regtest::BitcoinCoreController; use crate::Keychain; + mod utils { + use super::*; + + pub fn create_config() -> Config { + let mut config = Config::default(); + config.burnchain.magic_bytes = "T3".as_bytes().into(); + config.burnchain.username = Some(String::from("user")); + config.burnchain.password = Some(String::from("12345")); + // overriding default "0.0.0.0" because doesn't play nicely on Windows. + config.burnchain.peer_host = String::from("127.0.0.1"); + config + } + + pub fn create_keychain() -> Keychain { + create_keychain_with_seed(1) + } + + pub fn create_keychain_with_seed(value: u8) -> Keychain { + let seed = vec![value; 4]; + let keychain = Keychain::default(seed); + keychain + } + + pub fn create_miner1_pubkey() -> Secp256k1PublicKey { + create_keychain_with_seed(1).get_pub_key() + } + + pub fn create_miner2_pubkey() -> Secp256k1PublicKey { + create_keychain_with_seed(2).get_pub_key() + } + + pub fn create_commit_op() -> LeaderBlockCommitOp { + LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_hex( + "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", + ) + .unwrap(), + new_seed: VRFSeed::from_hex( + "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", + ) + .unwrap(), + parent_block_ptr: 2211, // 0x000008a3 + parent_vtxindex: 1, // 0x0001 + key_block_ptr: 1432, // 0x00000598 + key_vtxindex: 1, // 0x0001 + memo: vec![11], // 0x5a >> 3 + + burn_fee: 110_000, //relevant for fee calculation when sending the tx + input: (Txid([0x00; 32]), 0), + burn_parent_modulus: 2, // 0x5a & 0b111 + + apparent_sender: BurnchainSigner("mgbpit8FvkVJ9kuXY8QSM5P7eibnhcEMBk".to_string()), + commit_outs: vec![ + PoxAddress::Standard(StacksAddress::burn_address(false), None), + PoxAddress::Standard(StacksAddress::burn_address(false), None), + ], + + treatment: vec![], + sunset_burn: 5_500, //relevant for fee calculation when sending the tx + + txid: Txid([0x00; 32]), + vtxindex: 0, + block_height: 2212, + burn_header_hash: BurnchainHeaderHash([0x01; 32]), + } + } + } + #[test] fn test_get_satoshis_per_byte() { let dir = temp_dir(); @@ -3011,22 +3081,19 @@ mod tests { } #[test] - fn test_create_wallet_from_empty_name_default() { - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); + fn test_create_wallet_from_default_empty_name() { + let config = utils::create_config(); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() .expect("bitcoind should be started!"); + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + let wallets = BitcoinRPCRequest::list_wallets(&config).unwrap(); assert_eq!(0, wallets.len()); - let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller .create_wallet_if_dne() .expect("Wallet should now exists!"); @@ -3037,45 +3104,85 @@ mod tests { } #[test] - fn test_get_all_utxos_with_one_confirmed() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); - let miner_pubkey = keychain.get_pub_key(); + fn test_create_wallet_from_custom_name() { + let mut config = utils::create_config(); + config.burnchain.wallet_name = String::from("mywallet"); - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + + btc_controller + .create_wallet_if_dne() + .expect("Wallet should now exists!"); + + let wallets = BitcoinRPCRequest::list_wallets(&config).unwrap(); + assert_eq!(1, wallets.len()); + assert_eq!("mywallet".to_owned(), wallets[0]); + } + + #[test] + fn test_get_all_utxos_with_confirmation() { + let miner_pubkey = utils::create_miner1_pubkey(); + + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() .expect("bitcoind should be started!"); let btc_controller = BitcoinRegtestController::new(config.clone(), None); - btc_controller.bootstrap_chain(101); + + btc_controller.bootstrap_chain(100); + let utxos = btc_controller.get_all_utxos(&miner_pubkey); + assert_eq!(0, utxos.len()); + btc_controller.build_next_block(1); let utxos = btc_controller.get_all_utxos(&miner_pubkey); assert_eq!(1, utxos.len()); - let utxo = &utxos[0]; - assert_eq!(101, utxo.confirmations); - assert_eq!(5000000000, utxo.amount); + assert_eq!(101, utxos[0].confirmations); + assert_eq!(5_000_000_000, utxos[0].amount); + + btc_controller.build_next_block(1); + let utxos = btc_controller.get_all_utxos(&miner_pubkey); + assert_eq!(2, utxos.len()); + assert_eq!(101, utxos[0].confirmations); + assert_eq!(5_000_000_000, utxos[0].amount); + assert_eq!(102, utxos[1].confirmations); + assert_eq!(5_000_000_000, utxos[1].amount); + } + + #[test] + fn test_get_all_utxos_empty_for_other_pubkey() { + let miner_pubkey = utils::create_miner1_pubkey(); + let other_pubkey = utils::create_miner2_pubkey(); + + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); // one utxo exists + + let utxos = btc_controller.get_all_utxos(&other_pubkey); + assert_eq!(0, utxos.len()); } #[test] fn test_get_utxos_ok() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); - let miner_pubkey = keychain.get_pub_key(); + let miner_pubkey = utils::create_miner1_pubkey(); - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); + let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller @@ -3085,27 +3192,20 @@ mod tests { let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 10000, None, 0); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 0); let uxto_set = utxos.expect("Shouldn't be None!"); assert_eq!(1, uxto_set.num_utxos()); - assert_eq!(5000000000, uxto_set.total_available()); - let utxo = &uxto_set.utxos[0]; - assert_eq!(101, utxo.confirmations); - assert_eq!(5000000000, utxo.amount); + assert_eq!(5_000_000_000, uxto_set.total_available()); + assert_eq!(101, uxto_set.utxos[0].confirmations); + assert_eq!(5_000_000_000, uxto_set.utxos[0].amount); } #[test] - fn test_get_utxos_fails_due_to_filtering() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); - let miner_pubkey = keychain.get_pub_key(); + fn test_get_utxos_none_due_to_filter_total_required() { + let miner_pubkey = utils::create_miner1_pubkey(); - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); + let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); @@ -3114,23 +3214,39 @@ mod tests { .expect("bitcoind should be started!"); let btc_controller = BitcoinRegtestController::new(config.clone(), None); - btc_controller.bootstrap_chain(101); + btc_controller.bootstrap_chain(101); // one utxo exists - let too_much_required = 1000000000000000000_u64; + let too_much_required = 1_000_000_000_000_000_000; let utxos = btc_controller.get_utxos( StacksEpochId::Epoch31, &miner_pubkey, - 0, + too_much_required, None, 0, ); assert!(utxos.is_none(), "None because too much required"); + } + + #[test] + fn test_get_utxos_none_due_to_filter_pubkey() { + let miner_pubkey = utils::create_miner1_pubkey(); + + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); - let other_pubkey = Secp256k1PublicKey::from_hex("04ee0b1602eb18fef7986887a7e8769a30c9df981d33c8380d255edef003abdcd243a0eb74afdf6740e6c423e62aec631519a24cf5b1d62bf8a3e06ddc695dcb77").unwrap(); + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); // one utxo exists + + let other_pubkey = utils::create_miner2_pubkey(); let utxos = btc_controller.get_utxos( StacksEpochId::Epoch31, &other_pubkey, - too_much_required, + 1, None, 0, ); @@ -3138,6 +3254,22 @@ mod tests { utxos.is_none(), "None because utxos for other pubkey don't exist" ); + } + + #[test] + fn test_get_utxos_none_due_to_filter_utxo_exclusion() { + let miner_pubkey = utils::create_miner1_pubkey(); + + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); // one utxo exists let existent_utxo = btc_controller .get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, 0) @@ -3153,28 +3285,15 @@ mod tests { utxos.is_none(), "None because filtering exclude existent utxo set" ); - - //NOTE: Operation stall if burn block at block_height doesn't exist - /* - let future_block_height = 1000; - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 0, None, future_block_height); - assert!(utxos.is_none(), "None because utxos for future block height don't exist"); - */ } #[test] - fn test_build_leader_block_commit_tx_ok_with_new_block_commit() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); + fn test_build_leader_block_commit_tx_ok_with_new_commit_op() { + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); - let burn_signer = keychain.get_burnchain_signer(); - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); + let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); let mut btcd_controller = BitcoinCoreController::new(config.clone()); @@ -3183,44 +3302,12 @@ mod tests { .expect("bitcoind should be started!"); let mut btc_controller = BitcoinRegtestController::new(config, None); - btc_controller.bootstrap_chain(101); btc_controller .connect_dbs() - .expect("Cannot initialize dbs!"); - - let commit_op = LeaderBlockCommitOp { - block_header_hash: BlockHeaderHash::from_hex( - "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", - ) - .unwrap(), - new_seed: VRFSeed::from_hex( - "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", - ) - .unwrap(), - parent_block_ptr: 2211, // 0x000008a3 - parent_vtxindex: 1, // 0x0001 - key_block_ptr: 1432, // 0x00000598 - key_vtxindex: 1, // 0x0001 - memo: vec![11], // 0x5a >> 3 - - burn_fee: 0, - input: (Txid([0x00; 32]), 0), - burn_parent_modulus: 2, // 0x5a & 0b111 - - apparent_sender: burn_signer, - commit_outs: vec![ - PoxAddress::Standard(StacksAddress::burn_address(false), None), - PoxAddress::Standard(StacksAddress::burn_address(false), None), - ], + .expect("Dbs initialization required!"); + btc_controller.bootstrap_chain(101); - treatment: vec![], - sunset_burn: 0, - - txid: Txid([0x00; 32]), - vtxindex: 0, - block_height: 2212, - burn_header_hash: BurnchainHeaderHash([0x01; 32]), - }; + let commit_op = utils::create_commit_op(); let tx = btc_controller .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) @@ -3233,69 +3320,77 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx_fails_resubmitting_same_block_commit() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); + fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_not_confirmed() { + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); - let burn_signer = keychain.get_burnchain_signer(); - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); + let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() .expect("bitcoind should be started!"); let mut btc_controller = BitcoinRegtestController::new(config, None); - btc_controller.bootstrap_chain(101); btc_controller .connect_dbs() - .expect("Cannot initialize dbs!"); + .expect("Dbs initialization required!"); + btc_controller.bootstrap_chain(101); - let commit_op = LeaderBlockCommitOp { - block_header_hash: BlockHeaderHash::from_hex( - "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", - ) - .unwrap(), - new_seed: VRFSeed::from_hex( - "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", - ) - .unwrap(), - parent_block_ptr: 2211, // 0x000008a3 - parent_vtxindex: 1, // 0x0001 - key_block_ptr: 1432, // 0x00000598 - key_vtxindex: 1, // 0x0001 - memo: vec![11], // 0x5a >> 3 + let commit_op = utils::create_commit_op(); - burn_fee: 0, - input: (Txid([0x00; 32]), 0), - burn_parent_modulus: 2, // 0x5a & 0b111 + let _first_tx_ok = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .expect("At first, building leader block commit should work"); - apparent_sender: burn_signer, - commit_outs: vec![ - PoxAddress::Standard(StacksAddress::burn_address(false), None), - PoxAddress::Standard(StacksAddress::burn_address(false), None), - ], + // re-submitting same commit while previous it not confirmed by the burn + let resubmit = btc_controller.build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op, + &mut signer, + 0, + ); - treatment: vec![], - sunset_burn: 0, + assert!(resubmit.is_err()); + assert_eq!( + BurnchainControllerError::IdenticalOperation, + resubmit.unwrap_err() + ); + } - txid: Txid([0x00; 32]), - vtxindex: 0, - block_height: 2212, - burn_header_hash: BurnchainHeaderHash([0x01; 32]), - }; + #[test] + fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_is_confirmed() { + let keychain = utils::create_keychain(); + let miner_pubkey = keychain.get_pub_key(); + let mut signer = keychain.generate_op_signer(); - let _first_tx_ok = btc_controller + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let mut btc_controller = BitcoinRegtestController::new(config, None); + btc_controller + .connect_dbs() + .expect("Dbs initialization required!"); + btc_controller.bootstrap_chain(101); + + let commit_op = utils::create_commit_op(); + + let first_tx_ok = btc_controller .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) - .expect("Build leader block commit should work"); + .expect("At first, building leader block commit should work"); + + let ser = SerializedTx::new(first_tx_ok); + btc_controller.send_transaction(ser).expect("Tx should be sent to the burnchain!"); + btc_controller.build_next_block(1); // Now tx is confirmed + // re-submitting same commit while previous it is confirmed by the burnchain let resubmit = btc_controller.build_leader_block_commit_tx( StacksEpochId::Epoch31, commit_op, @@ -3311,73 +3406,87 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx_ok_rbf_block_commit() { - let miner_seed = vec![1, 1, 1, 1]; - let keychain = Keychain::default(miner_seed.clone()); + fn test_build_leader_block_commit_tx_ok_rbf_while_prev_is_confirmed() { + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); - let burn_signer = keychain.get_burnchain_signer(); - let mut config = Config::default(); - config.burnchain.magic_bytes = "T3".as_bytes().into(); - config.burnchain.username = Some(String::from("user")); - config.burnchain.password = Some(String::from("12345")); - config.burnchain.peer_host = String::from("127.0.0.1"); + let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() .expect("bitcoind should be started!"); let mut btc_controller = BitcoinRegtestController::new(config, None); - btc_controller.bootstrap_chain(101); btc_controller .connect_dbs() - .expect("Cannot initialize dbs!"); + .expect("Dbs initialization required!"); + btc_controller.bootstrap_chain(101); - let mut commit_op = LeaderBlockCommitOp { - block_header_hash: BlockHeaderHash::from_hex( - "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", - ) - .unwrap(), - new_seed: VRFSeed::from_hex( - "d5b9f21bc1f40f24e2c101ecd13c55b8619e5e03dad81de2c62a1cc1d8c1b375", - ) - .unwrap(), - parent_block_ptr: 2211, // 0x000008a3 - parent_vtxindex: 1, // 0x0001 - key_block_ptr: 1432, // 0x00000598 - key_vtxindex: 1, // 0x0001 - memo: vec![11], // 0x5a >> 3 + let mut commit_op = utils::create_commit_op(); - burn_fee: 0, - input: (Txid([0x00; 32]), 0), - burn_parent_modulus: 2, // 0x5a & 0b111 + let first_tx_ok = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .expect("At first, building leader block commit should work"); - apparent_sender: burn_signer, - commit_outs: vec![ - PoxAddress::Standard(StacksAddress::burn_address(false), None), - PoxAddress::Standard(StacksAddress::burn_address(false), None), - ], + let ser = SerializedTx::new(first_tx_ok); + btc_controller.send_transaction(ser).expect("Tx should be sent to the burnchain!"); + btc_controller.build_next_block(1); // Now tx is confirmed - treatment: vec![], - sunset_burn: 0, + //re-gen signer othewise fails because it will be disposed during previous commit tx. + let mut signer = keychain.generate_op_signer(); + //small change to the commit op payload + commit_op.burn_fee += 10; - txid: Txid([0x00; 32]), - vtxindex: 0, - block_height: 2212, - burn_header_hash: BurnchainHeaderHash([0x01; 32]), - }; + let rbf_tx = btc_controller + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) + .expect("Commit tx should be rbf-ed"); - let _first_tx_ok = btc_controller + assert_eq!(1, rbf_tx.version); + assert_eq!(0, rbf_tx.lock_time); + assert_eq!(1, rbf_tx.input.len()); + assert_eq!(4, rbf_tx.output.len()); + + //let ong= btc_controller.ongoing_block_commit.unwrap(); + //assert_eq!(2, ong.txids.len()); + } + + #[test] + fn test_build_leader_block_commit_tx_ok_rbf_while_prev_not_confirmed() { + let keychain = utils::create_keychain(); + let miner_pubkey = keychain.get_pub_key(); + let mut signer = keychain.generate_op_signer(); + + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let mut btc_controller = BitcoinRegtestController::new(config, None); + btc_controller + .connect_dbs() + .expect("Dbs initialization required!"); + btc_controller.bootstrap_chain(101); + + let mut commit_op = utils::create_commit_op(); + + let first_tx_ok = btc_controller .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) - .expect("Build leader block commit should work"); + .expect("At first, building leader block commit should work"); + + let ser = SerializedTx::new(first_tx_ok); + btc_controller.send_transaction(ser).expect("Tx should be sent to the burnchain!"); + btc_controller.build_next_block(1); // Now tx is confirmed //re-gen signer othewise fails because it will be disposed during previous commit tx. let mut signer = keychain.generate_op_signer(); //small change to the commit op payload - commit_op.burn_fee = 2; + commit_op.burn_fee += 10; let rbf_tx = btc_controller .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) @@ -3388,4 +3497,6 @@ mod tests { assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); } + + } From 8149e9e2494cbfd1c2d3e599fa68363d4051d554 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Fri, 27 Jun 2025 15:22:22 +0200 Subject: [PATCH 10/18] test: make regtest run in parallel --- stacks-common/src/util/mod.rs | 9 ++ .../burnchains/bitcoin_regtest_controller.rs | 83 ++++++++++++------- .../stacks-node/src/tests/bitcoin_regtest.rs | 13 ++- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/stacks-common/src/util/mod.rs b/stacks-common/src/util/mod.rs index 3b3e8324c9..fc41aba379 100644 --- a/stacks-common/src/util/mod.rs +++ b/stacks-common/src/util/mod.rs @@ -77,6 +77,15 @@ pub fn get_epoch_time_ms() -> u128 { since_the_epoch.as_millis() } +#[cfg(any(test, feature = "testing"))] +pub fn get_epoch_time_nanos() -> u128 { + let start = SystemTime::now(); + let since_the_epoch = start + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + since_the_epoch.as_nanos() +} + pub fn sleep_ms(millis: u64) { let t = time::Duration::from_millis(millis); thread::sleep(t); diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 15d04ab2e0..9fa6d31e22 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2839,7 +2839,11 @@ mod tests { use crate::Keychain; mod utils { + use std::net::TcpListener; + use super::*; + use crate::tests::bitcoin_regtest::BURNCHAIN_CONFIG_PEER_PORT_DISABLED; + use crate::util::get_epoch_time_nanos; pub fn create_config() -> Config { let mut config = Config::default(); @@ -2848,6 +2852,22 @@ mod tests { config.burnchain.password = Some(String::from("12345")); // overriding default "0.0.0.0" because doesn't play nicely on Windows. config.burnchain.peer_host = String::from("127.0.0.1"); + // avoiding peer port biding to reduce the number of ports to bind to. + config.burnchain.peer_port = BURNCHAIN_CONFIG_PEER_PORT_DISABLED; + + //Ask the OS for a free port. Not guaranteed to stay free, + //after TcpListner is dropped, but good enough for testing + //and starting bitcoind right after config is created + let tmp_listener = + TcpListener::bind("127.0.0.1:0").expect("Failed to bind to get a free port"); + let port = tmp_listener.local_addr().unwrap().port(); + + config.burnchain.rpc_port = port; + + let now = get_epoch_time_nanos(); + let dir = format!("/tmp/regtest-ctrl-{port}-{now}"); + config.node.working_dir = dir; + config } @@ -2885,7 +2905,7 @@ mod tests { key_vtxindex: 1, // 0x0001 memo: vec![11], // 0x5a >> 3 - burn_fee: 110_000, //relevant for fee calculation when sending the tx + burn_fee: 110_000, //relevant for fee calculation when sending the tx input: (Txid([0x00; 32]), 0), burn_parent_modulus: 2, // 0x5a & 0b111 @@ -2896,7 +2916,7 @@ mod tests { ], treatment: vec![], - sunset_burn: 5_500, //relevant for fee calculation when sending the tx + sunset_burn: 5_500, //relevant for fee calculation when sending the tx txid: Txid([0x00; 32]), vtxindex: 0, @@ -3130,14 +3150,14 @@ mod tests { let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() .expect("bitcoind should be started!"); let btc_controller = BitcoinRegtestController::new(config.clone(), None); - + btc_controller.bootstrap_chain(100); let utxos = btc_controller.get_all_utxos(&miner_pubkey); assert_eq!(0, utxos.len()); @@ -3149,11 +3169,13 @@ mod tests { assert_eq!(5_000_000_000, utxos[0].amount); btc_controller.build_next_block(1); - let utxos = btc_controller.get_all_utxos(&miner_pubkey); + let mut utxos = btc_controller.get_all_utxos(&miner_pubkey); + utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations)); + assert_eq!(2, utxos.len()); - assert_eq!(101, utxos[0].confirmations); + assert_eq!(102, utxos[0].confirmations); assert_eq!(5_000_000_000, utxos[0].amount); - assert_eq!(102, utxos[1].confirmations); + assert_eq!(101, utxos[1].confirmations); assert_eq!(5_000_000_000, utxos[1].amount); } @@ -3164,7 +3186,7 @@ mod tests { let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3172,7 +3194,7 @@ mod tests { let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); // one utxo exists - + let utxos = btc_controller.get_all_utxos(&other_pubkey); assert_eq!(0, utxos.len()); } @@ -3192,7 +3214,7 @@ mod tests { let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 0); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 101); let uxto_set = utxos.expect("Shouldn't be None!"); assert_eq!(1, uxto_set.num_utxos()); @@ -3243,13 +3265,7 @@ mod tests { btc_controller.bootstrap_chain(101); // one utxo exists let other_pubkey = utils::create_miner2_pubkey(); - let utxos = btc_controller.get_utxos( - StacksEpochId::Epoch31, - &other_pubkey, - 1, - None, - 0, - ); + let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &other_pubkey, 1, None, 0); assert!( utxos.is_none(), "None because utxos for other pubkey don't exist" @@ -3295,7 +3311,7 @@ mod tests { let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3320,14 +3336,15 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_not_confirmed() { + fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_not_confirmed( + ) { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3361,14 +3378,15 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_is_confirmed() { + fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_is_confirmed() + { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3387,7 +3405,9 @@ mod tests { .expect("At first, building leader block commit should work"); let ser = SerializedTx::new(first_tx_ok); - btc_controller.send_transaction(ser).expect("Tx should be sent to the burnchain!"); + btc_controller + .send_transaction(ser) + .expect("Tx should be sent to the burnchain!"); btc_controller.build_next_block(1); // Now tx is confirmed // re-submitting same commit while previous it is confirmed by the burnchain @@ -3413,7 +3433,7 @@ mod tests { let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3432,7 +3452,9 @@ mod tests { .expect("At first, building leader block commit should work"); let ser = SerializedTx::new(first_tx_ok); - btc_controller.send_transaction(ser).expect("Tx should be sent to the burnchain!"); + btc_controller + .send_transaction(ser) + .expect("Tx should be sent to the burnchain!"); btc_controller.build_next_block(1); // Now tx is confirmed //re-gen signer othewise fails because it will be disposed during previous commit tx. @@ -3448,9 +3470,6 @@ mod tests { assert_eq!(0, rbf_tx.lock_time); assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); - - //let ong= btc_controller.ongoing_block_commit.unwrap(); - //assert_eq!(2, ong.txids.len()); } #[test] @@ -3461,7 +3480,7 @@ mod tests { let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); - + let mut btcd_controller = BitcoinCoreController::new(config.clone()); btcd_controller .start_bitcoind() @@ -3480,7 +3499,9 @@ mod tests { .expect("At first, building leader block commit should work"); let ser = SerializedTx::new(first_tx_ok); - btc_controller.send_transaction(ser).expect("Tx should be sent to the burnchain!"); + btc_controller + .send_transaction(ser) + .expect("Tx should be sent to the burnchain!"); btc_controller.build_next_block(1); // Now tx is confirmed //re-gen signer othewise fails because it will be disposed during previous commit tx. @@ -3497,6 +3518,4 @@ mod tests { assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); } - - } diff --git a/testnet/stacks-node/src/tests/bitcoin_regtest.rs b/testnet/stacks-node/src/tests/bitcoin_regtest.rs index ef193f56f7..b8d238fe5f 100644 --- a/testnet/stacks-node/src/tests/bitcoin_regtest.rs +++ b/testnet/stacks-node/src/tests/bitcoin_regtest.rs @@ -17,6 +17,9 @@ use crate::helium::RunLoop; use crate::tests::to_addr; use crate::Config; +// Value usable as `BurnchainConfig::peer_port` to avoid bitcoind peer port binding +pub const BURNCHAIN_CONFIG_PEER_PORT_DISABLED: u16 = 0; + #[derive(Debug, thiserror::Error)] pub enum BitcoinCoreError { #[error("bitcoind spawn failed: {0}")] @@ -67,9 +70,17 @@ impl BitcoinCoreController { .arg("-server=1") .arg("-listenonion=0") .arg("-rpcbind=127.0.0.1") - .arg(format!("-port={}", self.config.burnchain.peer_port)) + //.arg(format!("-port={}", self.config.burnchain.peer_port)) .arg(format!("-datadir={}", self.config.get_burnchain_path_str())); + let peer_port = self.config.burnchain.peer_port; + if peer_port == BURNCHAIN_CONFIG_PEER_PORT_DISABLED { + info!("Peer Port is disabled. So `-listen=0` flag will be used"); + command.arg("-listen=0"); + } else { + command.arg(format!("-port={}", peer_port)); + } + self.add_rpc_cli_args(&mut command); eprintln!("bitcoind spawn: {command:?}"); From ee0282ef8210a033b2c8a427863bdb68c7e9a7c3 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Fri, 27 Jun 2025 15:44:36 +0200 Subject: [PATCH 11/18] test: add BITCOIND_TEST env protection --- .../burnchains/bitcoin_regtest_controller.rs | 58 +++++++++++++++++-- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 9fa6d31e22..9ec74544d7 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2823,7 +2823,7 @@ impl BitcoinRPCRequest { #[cfg(test)] mod tests { - use std::env::temp_dir; + use std::env::{self, temp_dir}; use std::fs::File; use std::io::Write; @@ -2855,7 +2855,7 @@ mod tests { // avoiding peer port biding to reduce the number of ports to bind to. config.burnchain.peer_port = BURNCHAIN_CONFIG_PEER_PORT_DISABLED; - //Ask the OS for a free port. Not guaranteed to stay free, + //Ask the OS for a free port. Not guaranteed to stay free, //after TcpListner is dropped, but good enough for testing //and starting bitcoind right after config is created let tmp_listener = @@ -3102,6 +3102,10 @@ mod tests { #[test] fn test_create_wallet_from_default_empty_name() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let config = utils::create_config(); let mut btcd_controller = BitcoinCoreController::new(config.clone()); @@ -3146,6 +3150,10 @@ mod tests { #[test] fn test_get_all_utxos_with_confirmation() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let miner_pubkey = utils::create_miner1_pubkey(); let mut config = utils::create_config(); @@ -3181,6 +3189,10 @@ mod tests { #[test] fn test_get_all_utxos_empty_for_other_pubkey() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let miner_pubkey = utils::create_miner1_pubkey(); let other_pubkey = utils::create_miner2_pubkey(); @@ -3201,6 +3213,10 @@ mod tests { #[test] fn test_get_utxos_ok() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let miner_pubkey = utils::create_miner1_pubkey(); let mut config = utils::create_config(); @@ -3225,6 +3241,10 @@ mod tests { #[test] fn test_get_utxos_none_due_to_filter_total_required() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let miner_pubkey = utils::create_miner1_pubkey(); let mut config = utils::create_config(); @@ -3251,6 +3271,10 @@ mod tests { #[test] fn test_get_utxos_none_due_to_filter_pubkey() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let miner_pubkey = utils::create_miner1_pubkey(); let mut config = utils::create_config(); @@ -3274,6 +3298,10 @@ mod tests { #[test] fn test_get_utxos_none_due_to_filter_utxo_exclusion() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let miner_pubkey = utils::create_miner1_pubkey(); let mut config = utils::create_config(); @@ -3305,6 +3333,10 @@ mod tests { #[test] fn test_build_leader_block_commit_tx_ok_with_new_commit_op() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); @@ -3336,8 +3368,11 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_not_confirmed( - ) { + fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_not_confirmed() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); @@ -3378,8 +3413,11 @@ mod tests { } #[test] - fn test_build_leader_block_commit_tx_fails_resubmitting_same_commit_op_while_prev_is_confirmed() - { + fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_is_confirmed() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); @@ -3427,6 +3465,10 @@ mod tests { #[test] fn test_build_leader_block_commit_tx_ok_rbf_while_prev_is_confirmed() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); @@ -3474,6 +3516,10 @@ mod tests { #[test] fn test_build_leader_block_commit_tx_ok_rbf_while_prev_not_confirmed() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); let mut signer = keychain.generate_op_signer(); From c6e6bcde4baea888c6df4ff68184549b5202f73a Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Fri, 27 Jun 2025 17:03:03 +0200 Subject: [PATCH 12/18] test: remove commented code --- testnet/stacks-node/src/tests/bitcoin_regtest.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/bitcoin_regtest.rs b/testnet/stacks-node/src/tests/bitcoin_regtest.rs index b8d238fe5f..4e02960995 100644 --- a/testnet/stacks-node/src/tests/bitcoin_regtest.rs +++ b/testnet/stacks-node/src/tests/bitcoin_regtest.rs @@ -70,7 +70,6 @@ impl BitcoinCoreController { .arg("-server=1") .arg("-listenonion=0") .arg("-rpcbind=127.0.0.1") - //.arg(format!("-port={}", self.config.burnchain.peer_port)) .arg(format!("-datadir={}", self.config.get_burnchain_path_str())); let peer_port = self.config.burnchain.peer_port; From 12057c5f215994c3fe0a44729d35d17a79f0ea16 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Mon, 30 Jun 2025 15:17:32 +0200 Subject: [PATCH 13/18] test: add assertions for TxOut --- .../burnchains/bitcoin_regtest_controller.rs | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 9ec74544d7..40bf5b525f 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2841,6 +2841,8 @@ mod tests { mod utils { use std::net::TcpListener; + use stacks::burnchains::MagicBytes; + use super::*; use crate::tests::bitcoin_regtest::BURNCHAIN_CONFIG_PEER_PORT_DISABLED; use crate::util::get_epoch_time_nanos; @@ -2924,6 +2926,35 @@ mod tests { burn_header_hash: BurnchainHeaderHash([0x01; 32]), } } + + pub fn txout_opreturn(op: &LeaderBlockCommitOp, magic: &MagicBytes) -> TxOut { + let op_bytes = { + let mut buffer = vec![]; + let mut magic_bytes = magic.as_bytes().to_vec(); + buffer.append(&mut magic_bytes); + op.consensus_serialize(&mut buffer) + .expect("FATAL: invalid operation"); + buffer + }; + + TxOut { + value: op.sunset_burn, + script_pubkey: Builder::new() + .push_opcode(opcodes::All::OP_RETURN) + .push_slice(&op_bytes) + .into_script(), + } + } + + pub fn txout_opdup_commit_to(addr: &PoxAddress, amount: u64) -> TxOut { + addr.to_bitcoin_tx_out(amount) + } + + pub fn txout_opdup_change_legacy(signer: &mut BurnchainOpSigner, amount: u64) -> TxOut { + let public_key = signer.get_public_key(); + let change_address_hash = Hash160::from_data(&public_key.to_bytes()); + LegacyBitcoinAddress::to_p2pkh_tx_out(&change_address_hash, amount) + } } #[test] @@ -3349,7 +3380,7 @@ mod tests { .start_bitcoind() .expect("bitcoind should be started!"); - let mut btc_controller = BitcoinRegtestController::new(config, None); + let mut btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller .connect_dbs() .expect("Dbs initialization required!"); @@ -3358,13 +3389,27 @@ mod tests { let commit_op = utils::create_commit_op(); let tx = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) .expect("Build leader block commit should work"); assert_eq!(1, tx.version); assert_eq!(0, tx.lock_time); assert_eq!(1, tx.input.len()); assert_eq!(4, tx.output.len()); + + //TODO: Try to implement assertion for TxIn + //let utxos = btc_controller.get_all_utxos(&miner_pubkey); + //let input_0 = utils::txin_utxo_legacy(&utxos[0]) + //assert_ne!(tx.input[0], tx.input[0]); + + let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes); + let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_000); + let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_000); + let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_865_300); + assert_eq!(op_return, tx.output[0]); + assert_eq!(op_commit_1, tx.output[1]); + assert_eq!(op_commit_2, tx.output[2]); + assert_eq!(op_change, tx.output[3]); } #[test] @@ -3481,7 +3526,7 @@ mod tests { .start_bitcoind() .expect("bitcoind should be started!"); - let mut btc_controller = BitcoinRegtestController::new(config, None); + let mut btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller .connect_dbs() .expect("Dbs initialization required!"); @@ -3505,13 +3550,22 @@ mod tests { commit_op.burn_fee += 10; let rbf_tx = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) .expect("Commit tx should be rbf-ed"); assert_eq!(1, rbf_tx.version); assert_eq!(0, rbf_tx.lock_time); assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); + + let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes); + let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005); + let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005); + let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_730_590); + assert_eq!(op_return, rbf_tx.output[0]); + assert_eq!(op_commit_1, rbf_tx.output[1]); + assert_eq!(op_commit_2, rbf_tx.output[2]); + assert_eq!(op_change, rbf_tx.output[3]); } #[test] @@ -3532,7 +3586,7 @@ mod tests { .start_bitcoind() .expect("bitcoind should be started!"); - let mut btc_controller = BitcoinRegtestController::new(config, None); + let mut btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller .connect_dbs() .expect("Dbs initialization required!"); @@ -3556,12 +3610,21 @@ mod tests { commit_op.burn_fee += 10; let rbf_tx = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op, &mut signer, 0) + .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) .expect("Commit tx should be rbf-ed"); assert_eq!(1, rbf_tx.version); assert_eq!(0, rbf_tx.lock_time); assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); + + let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes); + let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005); + let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005); + let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_730_590); + assert_eq!(op_return, rbf_tx.output[0]); + assert_eq!(op_commit_1, rbf_tx.output[1]); + assert_eq!(op_commit_2, rbf_tx.output[2]); + assert_eq!(op_change, rbf_tx.output[3]); } } From f889256e066fc179372de70f253206e6eac5b3aa Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Tue, 1 Jul 2025 14:38:40 +0200 Subject: [PATCH 14/18] test: improve asserts on transaction input/output --- .../burnchains/bitcoin_regtest_controller.rs | 255 ++++++++++++++---- testnet/stacks-node/src/operations.rs | 20 ++ 2 files changed, 222 insertions(+), 53 deletions(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index df71374a24..3e8e9619c0 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2902,7 +2902,15 @@ mod tests { create_keychain_with_seed(2).get_pub_key() } - pub fn create_commit_op() -> LeaderBlockCommitOp { + pub fn mine_tx(btc_controller: &BitcoinRegtestController, tx: Transaction) { + let ser = SerializedTx::new(tx); + btc_controller + .send_transaction(ser) + .expect("Tx should be sent to the burnchain!"); + btc_controller.build_next_block(1); // Now tx is confirmed + } + + pub fn create_templated_commit_op() -> LeaderBlockCommitOp { LeaderBlockCommitOp { block_header_hash: BlockHeaderHash::from_hex( "e88c3d30cb59a142f83de3b27f897a43bbb0f13316911bb98a3229973dae32af", @@ -2966,6 +2974,85 @@ mod tests { let change_address_hash = Hash160::from_data(&public_key.to_bytes()); LegacyBitcoinAddress::to_p2pkh_tx_out(&change_address_hash, amount) } + + pub fn txin_at_index( + complete_tx: &Transaction, + signer: &BurnchainOpSigner, + utxos: &[UTXO], + index: usize, + ) -> TxIn { + //Refresh op signer + let mut signer = signer.undisposed(); + let mut public_key = signer.get_public_key(); + + let mut tx = Transaction { + version: complete_tx.version, + lock_time: complete_tx.lock_time, + input: vec![], + output: complete_tx.output.clone(), + }; + + for utxo in utxos.iter() { + let input = TxIn { + previous_output: OutPoint { + txid: utxo.txid, + vout: utxo.vout, + }, + script_sig: Script::new(), + sequence: 0xFFFFFFFD, // allow RBF + witness: vec![], + }; + tx.input.push(input); + } + + for (i, utxo) in utxos.iter().enumerate() { + let script_pub_key = utxo.script_pub_key.clone(); + let sig_hash_all = 0x01; + + let (sig_hash, is_segwit) = if script_pub_key.as_bytes().len() == 22 + && script_pub_key.as_bytes()[0..2] == [0x00, 0x14] + { + // p2wpkh + ( + tx.segwit_signature_hash(i, &script_pub_key, utxo.amount, sig_hash_all), + true, + ) + } else { + // p2pkh + (tx.signature_hash(i, &script_pub_key, sig_hash_all), false) + }; + + let sig1_der = { + let message = signer + .sign_message(sig_hash.as_bytes()) + .expect("Unable to sign message"); + message + .to_secp256k1_recoverable() + .expect("Unable to get recoverable signature") + .to_standard() + .serialize_der() + }; + + if is_segwit { + // segwit + public_key.set_compressed(true); + tx.input[i].script_sig = Script::from(vec![]); + tx.input[i].witness = vec![ + [&*sig1_der, &[sig_hash_all as u8][..]].concat().to_vec(), + public_key.to_bytes(), + ]; + } else { + // legacy scriptSig + tx.input[i].script_sig = Builder::new() + .push_slice(&[&*sig1_der, &[sig_hash_all as u8][..]].concat()) + .push_slice(&public_key.to_bytes()) + .into_script(); + tx.input[i].witness.clear(); + } + } + + tx.input[index].clone() + } } #[test] @@ -3254,7 +3341,7 @@ mod tests { } #[test] - fn test_get_utxos_ok() { + fn test_get_utxos_ok_with_confirmation() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -3272,13 +3359,32 @@ mod tests { let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); - let utxos = btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 101); + let utxos_opt = + btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 101); + let uxto_set = utxos_opt.expect("Shouldn't be None at height 101!"); - let uxto_set = utxos.expect("Shouldn't be None!"); + assert_eq!(btc_controller.get_block_hash(101), uxto_set.bhh); assert_eq!(1, uxto_set.num_utxos()); assert_eq!(5_000_000_000, uxto_set.total_available()); - assert_eq!(101, uxto_set.utxos[0].confirmations); - assert_eq!(5_000_000_000, uxto_set.utxos[0].amount); + let utxos = uxto_set.utxos; + assert_eq!(101, utxos[0].confirmations); + assert_eq!(5_000_000_000, utxos[0].amount); + + btc_controller.build_next_block(1); + + let utxos_opt = + btc_controller.get_utxos(StacksEpochId::Epoch31, &miner_pubkey, 1, None, 102); + let uxto_set = utxos_opt.expect("Shouldn't be None at height 102!"); + + assert_eq!(btc_controller.get_block_hash(102), uxto_set.bhh); + assert_eq!(2, uxto_set.num_utxos()); + assert_eq!(10_000_000_000, uxto_set.total_available()); + let mut utxos = uxto_set.utxos; + utxos.sort_by(|a, b| b.confirmations.cmp(&a.confirmations)); + assert_eq!(102, utxos[0].confirmations); + assert_eq!(5_000_000_000, utxos[0].amount); + assert_eq!(101, utxos[1].confirmations); + assert_eq!(5_000_000_000, utxos[1].amount); } #[test] @@ -3381,7 +3487,7 @@ mod tests { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); - let mut signer = keychain.generate_op_signer(); + let mut op_signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); @@ -3395,28 +3501,37 @@ mod tests { btc_controller .connect_dbs() .expect("Dbs initialization required!"); - btc_controller.bootstrap_chain(101); + btc_controller.bootstrap_chain(101); // now, one utxo exists - let commit_op = utils::create_commit_op(); + let mut commit_op = utils::create_templated_commit_op(); + commit_op.sunset_burn = 5_500; + commit_op.burn_fee = 110_000; let tx = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op.clone(), + &mut op_signer, + 0, + ) .expect("Build leader block commit should work"); + assert!(op_signer.is_disposed()); + assert_eq!(1, tx.version); assert_eq!(0, tx.lock_time); assert_eq!(1, tx.input.len()); assert_eq!(4, tx.output.len()); - //TODO: Try to implement assertion for TxIn - //let utxos = btc_controller.get_all_utxos(&miner_pubkey); - //let input_0 = utils::txin_utxo_legacy(&utxos[0]) - //assert_ne!(tx.input[0], tx.input[0]); + // utxos list contains the only existing utxo + let used_utxos = btc_controller.get_all_utxos(&miner_pubkey); + let input_0 = utils::txin_at_index(&tx, &op_signer, &used_utxos, 0); + assert_eq!(input_0, tx.input[0]); let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes); let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_000); let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_000); - let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_865_300); + let op_change = utils::txout_opdup_change_legacy(&mut op_signer, 4_999_865_300); assert_eq!(op_return, tx.output[0]); assert_eq!(op_commit_1, tx.output[1]); assert_eq!(op_commit_2, tx.output[2]); @@ -3431,7 +3546,7 @@ mod tests { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); - let mut signer = keychain.generate_op_signer(); + let mut op_signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); @@ -3445,19 +3560,24 @@ mod tests { btc_controller .connect_dbs() .expect("Dbs initialization required!"); - btc_controller.bootstrap_chain(101); + btc_controller.bootstrap_chain(101); // now, one utxo exists - let commit_op = utils::create_commit_op(); + let commit_op = utils::create_templated_commit_op(); let _first_tx_ok = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op.clone(), + &mut op_signer, + 0, + ) .expect("At first, building leader block commit should work"); - // re-submitting same commit while previous it not confirmed by the burn + // re-submitting same commit while previous it is not confirmed by the burnchain let resubmit = btc_controller.build_leader_block_commit_tx( StacksEpochId::Epoch31, commit_op, - &mut signer, + &mut op_signer, 0, ); @@ -3476,7 +3596,7 @@ mod tests { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); - let mut signer = keychain.generate_op_signer(); + let mut op_signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); @@ -3490,25 +3610,26 @@ mod tests { btc_controller .connect_dbs() .expect("Dbs initialization required!"); - btc_controller.bootstrap_chain(101); + btc_controller.bootstrap_chain(101); // now, one utxo exists - let commit_op = utils::create_commit_op(); + let commit_op = utils::create_templated_commit_op(); let first_tx_ok = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op.clone(), + &mut op_signer, + 0, + ) .expect("At first, building leader block commit should work"); - let ser = SerializedTx::new(first_tx_ok); - btc_controller - .send_transaction(ser) - .expect("Tx should be sent to the burnchain!"); - btc_controller.build_next_block(1); // Now tx is confirmed + utils::mine_tx(&btc_controller, first_tx_ok); // Now tx is confirmed // re-submitting same commit while previous it is confirmed by the burnchain let resubmit = btc_controller.build_leader_block_commit_tx( StacksEpochId::Epoch31, commit_op, - &mut signer, + &mut op_signer, 0, ); @@ -3527,7 +3648,7 @@ mod tests { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); - let mut signer = keychain.generate_op_signer(); + let mut op_signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); @@ -3541,19 +3662,25 @@ mod tests { btc_controller .connect_dbs() .expect("Dbs initialization required!"); - btc_controller.bootstrap_chain(101); - - let mut commit_op = utils::create_commit_op(); + btc_controller.bootstrap_chain(101); // now, one utxo exists + let mut commit_op = utils::create_templated_commit_op(); + commit_op.sunset_burn = 5_500; + commit_op.burn_fee = 110_000; + let first_tx_ok = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + .build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op.clone(), + &mut op_signer, + 0, + ) .expect("At first, building leader block commit should work"); - let ser = SerializedTx::new(first_tx_ok); - btc_controller - .send_transaction(ser) - .expect("Tx should be sent to the burnchain!"); - btc_controller.build_next_block(1); // Now tx is confirmed + let first_txid = first_tx_ok.txid(); + + // Now tx is confirmed: prev utxo is updated and one more utxo is generated + utils::mine_tx(&btc_controller, first_tx_ok); //re-gen signer othewise fails because it will be disposed during previous commit tx. let mut signer = keychain.generate_op_signer(); @@ -3564,11 +3691,24 @@ mod tests { .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) .expect("Commit tx should be rbf-ed"); + assert!(op_signer.is_disposed()); + assert_eq!(1, rbf_tx.version); assert_eq!(0, rbf_tx.lock_time); assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); + // utxos list contains the sole utxo used by prev commit operation + // because has enough amount to cover the rfb commit + let used_utxos: Vec = btc_controller + .get_all_utxos(&miner_pubkey) + .into_iter() + .filter(|utxo| utxo.txid == first_txid) + .collect(); + + let input_0 = utils::txin_at_index(&rbf_tx, &op_signer, &used_utxos, 0); + assert_eq!(input_0, rbf_tx.input[0]); + let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes); let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005); let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005); @@ -3587,7 +3727,7 @@ mod tests { let keychain = utils::create_keychain(); let miner_pubkey = keychain.get_pub_key(); - let mut signer = keychain.generate_op_signer(); + let mut op_signer = keychain.generate_op_signer(); let mut config = utils::create_config(); config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); @@ -3601,20 +3741,21 @@ mod tests { btc_controller .connect_dbs() .expect("Dbs initialization required!"); - btc_controller.bootstrap_chain(101); + btc_controller.bootstrap_chain(101); // Now, one utxo exists - let mut commit_op = utils::create_commit_op(); + let mut commit_op = utils::create_templated_commit_op(); + commit_op.sunset_burn = 5_500; + commit_op.burn_fee = 110_000; - let first_tx_ok = btc_controller - .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) + let _first_tx_ok = btc_controller + .build_leader_block_commit_tx( + StacksEpochId::Epoch31, + commit_op.clone(), + &mut op_signer, + 0, + ) .expect("At first, building leader block commit should work"); - let ser = SerializedTx::new(first_tx_ok); - btc_controller - .send_transaction(ser) - .expect("Tx should be sent to the burnchain!"); - btc_controller.build_next_block(1); // Now tx is confirmed - //re-gen signer othewise fails because it will be disposed during previous commit tx. let mut signer = keychain.generate_op_signer(); //small change to the commit op payload @@ -3624,15 +3765,23 @@ mod tests { .build_leader_block_commit_tx(StacksEpochId::Epoch31, commit_op.clone(), &mut signer, 0) .expect("Commit tx should be rbf-ed"); + assert!(op_signer.is_disposed()); + assert_eq!(1, rbf_tx.version); assert_eq!(0, rbf_tx.lock_time); assert_eq!(1, rbf_tx.input.len()); assert_eq!(4, rbf_tx.output.len()); + // utxos list contains the only existing utxo + let used_utxos = btc_controller.get_all_utxos(&miner_pubkey); + + let input_0 = utils::txin_at_index(&rbf_tx, &op_signer, &used_utxos, 0); + assert_eq!(input_0, rbf_tx.input[0]); + let op_return = utils::txout_opreturn(&commit_op, &config.burnchain.magic_bytes); let op_commit_1 = utils::txout_opdup_commit_to(&commit_op.commit_outs[0], 55_005); let op_commit_2 = utils::txout_opdup_commit_to(&commit_op.commit_outs[1], 55_005); - let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_730_590); + let op_change = utils::txout_opdup_change_legacy(&mut signer, 4_999_862_985); assert_eq!(op_return, rbf_tx.output[0]); assert_eq!(op_commit_1, rbf_tx.output[1]); assert_eq!(op_commit_2, rbf_tx.output[2]); diff --git a/testnet/stacks-node/src/operations.rs b/testnet/stacks-node/src/operations.rs index 7e26fb42e2..ccd3bddbc5 100644 --- a/testnet/stacks-node/src/operations.rs +++ b/testnet/stacks-node/src/operations.rs @@ -61,6 +61,26 @@ impl BurnchainOpSigner { } } +/// Test-only utilities for `BurnchainOpSigner` +#[cfg(any(test, feature = "testing"))] +impl BurnchainOpSigner { + /// Returns `true` if the signer has been disposed. + /// + /// This is useful in tests to assert that disposal behavior is working as expected. + pub fn is_disposed(&self) -> bool { + self.is_disposed + } + + /// Returns a new `BurnchainOpSigner` instance using the same secret key, + /// but with `is_disposed` set to `false` and `is_one_off` set to `false`. + /// + /// This is useful in testing scenarios where you need a fresh, undisposed copy + /// of a signer without recreating the private key. + pub fn undisposed(&self) -> Self { + Self::new(self.secret_key, false) + } +} + #[cfg(test)] mod test { use stacks_common::util::secp256k1::Secp256k1PrivateKey; From 48186baf0432ca6494ae6091be74e687d67bdc93 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Tue, 1 Jul 2025 14:41:20 +0200 Subject: [PATCH 15/18] test: improve asserts on transaction input/output --- .../src/burnchains/bitcoin_regtest_controller.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 3e8e9619c0..43708ba91b 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3230,6 +3230,7 @@ mod tests { } #[test] + #[ignore] fn test_create_wallet_from_default_empty_name() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3257,6 +3258,7 @@ mod tests { } #[test] + #[ignore] fn test_create_wallet_from_custom_name() { let mut config = utils::create_config(); config.burnchain.wallet_name = String::from("mywallet"); @@ -3278,6 +3280,7 @@ mod tests { } #[test] + #[ignore] fn test_get_all_utxos_with_confirmation() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3317,6 +3320,7 @@ mod tests { } #[test] + #[ignore] fn test_get_all_utxos_empty_for_other_pubkey() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3341,6 +3345,7 @@ mod tests { } #[test] + #[ignore] fn test_get_utxos_ok_with_confirmation() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3388,6 +3393,7 @@ mod tests { } #[test] + #[ignore] fn test_get_utxos_none_due_to_filter_total_required() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3418,6 +3424,7 @@ mod tests { } #[test] + #[ignore] fn test_get_utxos_none_due_to_filter_pubkey() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3445,6 +3452,7 @@ mod tests { } #[test] + #[ignore] fn test_get_utxos_none_due_to_filter_utxo_exclusion() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3480,6 +3488,7 @@ mod tests { } #[test] + #[ignore] fn test_build_leader_block_commit_tx_ok_with_new_commit_op() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3539,6 +3548,7 @@ mod tests { } #[test] + #[ignore] fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_not_confirmed() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3589,6 +3599,7 @@ mod tests { } #[test] + #[ignore] fn test_build_leader_block_commit_tx_fails_resub_same_commit_op_while_prev_is_confirmed() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3641,6 +3652,7 @@ mod tests { } #[test] + #[ignore] fn test_build_leader_block_commit_tx_ok_rbf_while_prev_is_confirmed() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; @@ -3720,6 +3732,7 @@ mod tests { } #[test] + #[ignore] fn test_build_leader_block_commit_tx_ok_rbf_while_prev_not_confirmed() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; From 1b35f008f23751c775376091c727ff3e72430502 Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Tue, 1 Jul 2025 15:25:35 +0200 Subject: [PATCH 16/18] fix: format --- .../stacks-node/src/burnchains/bitcoin_regtest_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 43708ba91b..06a4ef002d 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3679,7 +3679,7 @@ mod tests { let mut commit_op = utils::create_templated_commit_op(); commit_op.sunset_burn = 5_500; commit_op.burn_fee = 110_000; - + let first_tx_ok = btc_controller .build_leader_block_commit_tx( StacksEpochId::Epoch31, From 1fec25eae2bf233555c5911149028090363217ef Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Tue, 1 Jul 2025 16:32:13 +0200 Subject: [PATCH 17/18] test: reduce amount in test --- .../stacks-node/src/burnchains/bitcoin_regtest_controller.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 06a4ef002d..16742f4fb8 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3412,7 +3412,7 @@ mod tests { let btc_controller = BitcoinRegtestController::new(config.clone(), None); btc_controller.bootstrap_chain(101); // one utxo exists - let too_much_required = 1_000_000_000_000_000_000; + let too_much_required = 10_000_000_000; let utxos = btc_controller.get_utxos( StacksEpochId::Epoch31, &miner_pubkey, From 39c6f92154ea2ace5854854b238282929bec695c Mon Sep 17 00:00:00 2001 From: Federico De Felici Date: Wed, 2 Jul 2025 11:27:48 +0200 Subject: [PATCH 18/18] test: reproduce stall behaviour, #6225 --- .../burnchains/bitcoin_regtest_controller.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 16742f4fb8..1ea4a13063 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -3487,6 +3487,41 @@ mod tests { ); } + #[test] + #[ignore] + fn test_get_utxos_none_due_to_filter_block_height() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let miner_pubkey = utils::create_miner1_pubkey(); + + let mut config = utils::create_config(); + config.burnchain.local_mining_public_key = Some(miner_pubkey.to_hex()); + + let mut btcd_controller = BitcoinCoreController::new(config.clone()); + btcd_controller + .start_bitcoind() + .expect("bitcoind should be started!"); + + let btc_controller = BitcoinRegtestController::new(config.clone(), None); + btc_controller.bootstrap_chain(101); // one utxo exists + + //NOTE: Operation stall if burn block at block_height doesn't exist + let future_block_height = 102; + let utxos = btc_controller.get_utxos( + StacksEpochId::Epoch31, + &miner_pubkey, + 1, + None, + future_block_height, + ); + assert!( + utxos.is_none(), + "None because utxos for future block height don't exist" + ); + } + #[test] #[ignore] fn test_build_leader_block_commit_tx_ok_with_new_commit_op() {