From 9a2a8da6e034cfbe0fb14d3dd226e427d0087536 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 12 Aug 2025 14:42:14 -0700 Subject: [PATCH 1/3] Add SIGNER_APPROVAL_THRESHOLD env var for lowering threshold on testnet Signed-off-by: Jacinta Ferrant --- Cargo.lock | 1 + libsigner/src/tests/signer_state.rs | 2 +- libsigner/src/v0/signer_state.rs | 28 +++- stacks-common/Cargo.toml | 1 + stacks-common/src/util/mod.rs | 133 ++++++++++++++++++ .../src/nakamoto_node/stackerdb_listener.rs | 14 +- stacks-signer/src/chainstate/tests/v2.rs | 2 +- stacks-signer/src/signerdb.rs | 4 +- stacks-signer/src/tests/signer_state.rs | 4 +- stacks-signer/src/v0/signer.rs | 41 +++--- stacks-signer/src/v0/signer_state.rs | 4 +- stackslib/src/chainstate/nakamoto/mod.rs | 42 ++---- .../src/chainstate/nakamoto/tests/mod.rs | 49 ++----- .../src/chainstate/nakamoto/tests/node.rs | 5 +- stackslib/src/core/mod.rs | 4 - stackslib/src/net/api/tests/gethealth.rs | 13 +- .../nakamoto/download_state_machine.rs | 12 +- stackslib/src/net/download/nakamoto/mod.rs | 11 +- .../download/nakamoto/tenure_downloader.rs | 11 +- .../nakamoto/tenure_downloader_set.rs | 2 + .../nakamoto/tenure_downloader_unconfirmed.rs | 15 +- stackslib/src/net/relay.rs | 5 +- stackslib/src/net/tests/download/nakamoto.rs | 31 ++-- stackslib/src/net/unsolicited.rs | 6 +- 24 files changed, 294 insertions(+), 146 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7c0905f54..242c38111a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3197,6 +3197,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serial_test", "sha2 0.10.8", "sha3", "slog", diff --git a/libsigner/src/tests/signer_state.rs b/libsigner/src/tests/signer_state.rs index a16a789608..2cab3ded43 100644 --- a/libsigner/src/tests/signer_state.rs +++ b/libsigner/src/tests/signer_state.rs @@ -55,7 +55,7 @@ fn generate_global_state_evaluator(num_addresses: u32) -> GlobalStateEvaluator { for address in address_weights.keys() { address_updates.insert(*address, update.clone()); } - GlobalStateEvaluator::new(address_updates, address_weights) + GlobalStateEvaluator::new(address_updates, address_weights, false) } fn generate_random_address_with_equal_weights(num_addresses: u32) -> HashMap { diff --git a/libsigner/src/v0/signer_state.rs b/libsigner/src/v0/signer_state.rs index bac487ced5..cb0b587c24 100644 --- a/libsigner/src/v0/signer_state.rs +++ b/libsigner/src/v0/signer_state.rs @@ -18,6 +18,7 @@ use std::hash::{Hash, Hasher}; use blockstack_lib::chainstate::stacks::StacksTransaction; use clarity::types::chainstate::StacksAddress; +use clarity::util::compute_voting_weight_threshold; use serde::{Deserialize, Serialize}; use stacks_common::types::chainstate::{ConsensusHash, StacksBlockId}; use stacks_common::util::hash::Hash160; @@ -33,6 +34,10 @@ pub struct GlobalStateEvaluator { pub address_updates: HashMap, /// The total weight of all signers pub total_weight: u32, + /// The threshold weight required to reach rejection + pub rejection_weight: u32, + /// The threshold weight required to reach approval + pub approval_weight: u32, } impl GlobalStateEvaluator { @@ -40,14 +45,19 @@ impl GlobalStateEvaluator { pub fn new( address_updates: HashMap, address_weights: HashMap, + mainnet: bool, ) -> Self { let total_weight = address_weights .values() .fold(0u32, |acc, val| acc.saturating_add(*val)); + let approval_weight = compute_voting_weight_threshold(total_weight, mainnet); + let rejection_weight = total_weight.saturating_sub(approval_weight); Self { address_weights, address_updates, total_weight, + approval_weight, + rejection_weight, } } @@ -69,7 +79,7 @@ impl GlobalStateEvaluator { let mut total_weight_support = 0; for (version, weight_support) in protocol_versions.into_iter().rev() { total_weight_support += weight_support; - if total_weight_support >= self.total_weight * 7 / 10 { + if self.reached_approval(total_weight_support) { return Some(version); } } @@ -89,7 +99,7 @@ impl GlobalStateEvaluator { .entry((burn_block, burn_block_height)) .or_insert_with(|| 0); *entry += weight; - if self.reached_agreement(*entry) { + if self.reached_approval(*entry) { return Some((*burn_block, burn_block_height)); } } @@ -124,7 +134,7 @@ impl GlobalStateEvaluator { let entry = state_views.entry(key).or_insert_with(|| 0); *entry += weight; - if self.reached_agreement(*entry) { + if self.reached_approval(*entry) { found_state_view = Some(state_machine); } @@ -133,7 +143,7 @@ impl GlobalStateEvaluator { .or_insert_with(|| 0); *replay_entry += weight; - if self.reached_agreement(*replay_entry) { + if self.reached_approval(*replay_entry) { found_replay_set = Some(tx_replay_set); } if found_replay_set.is_some() && found_state_view.is_some() { @@ -159,8 +169,14 @@ impl GlobalStateEvaluator { /// Check if the supplied vote weight crosses the global agreement threshold. /// Returns true if it has, false otherwise. - pub fn reached_agreement(&self, vote_weight: u32) -> bool { - vote_weight >= self.total_weight * 7 / 10 + pub fn reached_approval(&self, vote_weight: u32) -> bool { + vote_weight >= self.approval_weight + } + + /// Check if the supplied vote weight crosses the global disagreement threshold. + /// Returns true if it has, false otherwise. + pub fn reached_rejection(&self, vote_weight: u32) -> bool { + vote_weight >= self.rejection_weight } /// Get the global transaction replay set. Returns `None` if there diff --git a/stacks-common/Cargo.toml b/stacks-common/Cargo.toml index 4b2c96d3bb..da2cfee693 100644 --- a/stacks-common/Cargo.toml +++ b/stacks-common/Cargo.toml @@ -74,6 +74,7 @@ sha2 = { version = "0.10" } [dev-dependencies] proptest = "1.6.0" +serial_test = "3.2.0" [target.'cfg(windows)'.dev-dependencies] winapi = { version = "0.3", features = ["fileapi", "processenv", "winnt"] } diff --git a/stacks-common/src/util/mod.rs b/stacks-common/src/util/mod.rs index 69e14a4473..edc3a4ea9c 100644 --- a/stacks-common/src/util/mod.rs +++ b/stacks-common/src/util/mod.rs @@ -149,3 +149,136 @@ where } #[cfg(all(feature = "rusqlite", target_family = "wasm"))] compile_error!("The `rusqlite` feature is not supported for wasm targets"); + +// The threshold of weighted votes to reach consensus in Nakamoto. +// This is out of 100, so 70 means "70%". +pub const NAKAMOTO_SIGNER_APPROVAL_THRESHOLD: u32 = 70; + +/// Determines the signer approval threshold percentage for Nakamoto block approval. +/// +/// By default, this uses the constant [`NAKAMOTO_SIGNER_APPROVAL_THRESHOLD`]. +/// If the `SIGNER_APPROVAL_THRESHOLD` environment variable is set, its value (in percent) +/// will be used instead—unless `mainnet` is `true`, in which case overriding via the +/// environment variable is not allowed. +/// +/// The environment variable value must be an integer between 1 and 100 (inclusive). +/// +/// # Panics +/// - If `mainnet` is `true` and `SIGNER_APPROVAL_THRESHOLD` is set. +/// - If `SIGNER_APPROVAL_THRESHOLD` cannot be parsed as a `u32`. +/// - If the parsed threshold is not in the range `1..=100`. +/// +/// # Parameters +/// - `mainnet`: Whether the network is Mainnet. +/// +/// # Returns +/// The approval threshold percentage as a `u32`. +pub fn determine_signer_approval_threshold_percentage(mainnet: bool) -> u32 { + let mut threshold = NAKAMOTO_SIGNER_APPROVAL_THRESHOLD; + if let Ok(env_threshold) = std::env::var("SIGNER_APPROVAL_THRESHOLD") { + assert!( + !mainnet, + "Cannot use SIGNER_APPROVAL_THRESHOLD env variable with Mainnet." + ); + match env_threshold.parse::() { + Ok(env_threshold) => { + assert!( + env_threshold > 0 && env_threshold <= 100, + "Invalid SIGNER_APPROVAL_THRESHOLD. Must be > 0 and <= 100" + ); + threshold = env_threshold; + } + Err(e) => panic!("Failed to parse SIGNER_APPROVAL_THRESHOLD as a u32: {e}"), + } + } + threshold +} + +/// Computes the minimum voting weight required to reach consensus. +/// +/// The threshold is determined as a percentage of `total_weight`, using +/// [`determine_signer_approval_threshold_percentage`]. The percentage is +/// applied, and any remainder from integer division is rounded up by adding 1 +/// if there is a non-zero remainder. +/// +/// # Parameters +/// - `total_weight`: The total combined voting weight of all signers. +/// - `mainnet`: Whether the network is Mainnet (affects threshold calculation). +/// +/// # Returns +/// The minimum voting weight (rounded up) required for consensus. +pub fn compute_voting_weight_threshold(total_weight: u32, mainnet: bool) -> u32 { + let threshold = determine_signer_approval_threshold_percentage(mainnet); + let ceil = if (total_weight * threshold) % 100 == 0 { + 0 + } else { + 1 + }; + (total_weight * threshold / 100).saturating_add(ceil) +} + +#[test] +pub fn test_compute_voting_weight_threshold_no_env() { + // We are purposefully testing ONLY compute_voting_weight_threshold without SIGNER_APPROVAL_THRESHOLD + // see following tests for env SIGNER_APPROVAL_THRESHOLD specific tests. + use crate::util::compute_voting_weight_threshold; + std::env::remove_var("SIGNER_APPROVAL_THRESHOLD"); + assert_eq!(compute_voting_weight_threshold(100_u32, false), 70_u32,); + + assert_eq!(compute_voting_weight_threshold(10_u32, false), 7_u32,); + + assert_eq!(compute_voting_weight_threshold(3000_u32, false), 2100_u32,); + + assert_eq!(compute_voting_weight_threshold(4000_u32, false), 2800_u32,); + + // Round-up check + assert_eq!(compute_voting_weight_threshold(511_u32, false), 358_u32,); +} + +#[test] +#[serial_test::serial] +fn returns_default_when_env_not_set() { + std::env::remove_var("SIGNER_APPROVAL_THRESHOLD"); + let result = determine_signer_approval_threshold_percentage(false); + assert_eq!(result, 70); +} + +#[test] +#[serial_test::serial] +fn uses_env_when_not_mainnet() { + std::env::set_var("SIGNER_APPROVAL_THRESHOLD", "75"); + let result = determine_signer_approval_threshold_percentage(false); + assert_eq!(result, 75); +} + +#[test] +#[serial_test::serial] +#[should_panic(expected = "Cannot use SIGNER_APPROVAL_THRESHOLD env variable with Mainnet")] +fn panics_if_env_set_on_mainnet() { + std::env::set_var("SIGNER_APPROVAL_THRESHOLD", "50"); + let _ = determine_signer_approval_threshold_percentage(true); +} + +#[test] +#[serial_test::serial] +#[should_panic(expected = "Failed to parse SIGNER_APPROVAL_THRESHOLD as a u32")] +fn panics_if_env_not_u32() { + std::env::set_var("SIGNER_APPROVAL_THRESHOLD", "not-a-number"); + let _ = determine_signer_approval_threshold_percentage(false); +} + +#[test] +#[serial_test::serial] +#[should_panic(expected = "Invalid SIGNER_APPROVAL_THRESHOLD. Must be > 0 and <= 100")] +fn panics_if_env_is_zero() { + std::env::set_var("SIGNER_APPROVAL_THRESHOLD", "0"); + let _ = determine_signer_approval_threshold_percentage(false); +} + +#[test] +#[serial_test::serial] +#[should_panic(expected = "Invalid SIGNER_APPROVAL_THRESHOLD. Must be > 0 and <= 100")] +fn panics_if_env_over_100() { + std::env::set_var("SIGNER_APPROVAL_THRESHOLD", "101"); + let _ = determine_signer_approval_threshold_percentage(false); +} diff --git a/stacks-node/src/nakamoto_node/stackerdb_listener.rs b/stacks-node/src/nakamoto_node/stackerdb_listener.rs index cfe23474db..fef3df2141 100644 --- a/stacks-node/src/nakamoto_node/stackerdb_listener.rs +++ b/stacks-node/src/nakamoto_node/stackerdb_listener.rs @@ -140,13 +140,6 @@ impl StackerDBListener { warn!("Replaced the miner/coordinator receiver of a prior thread. Prior thread may have crashed."); } - let total_weight = reward_set.total_signing_weight().map_err(|e| { - warn!("Failed to calculate total weight for the reward set: {e:?}"); - ChainstateError::NoRegisteredSigners(0) - })?; - - let weight_threshold = NakamotoBlockHeader::compute_voting_weight_threshold(total_weight)?; - let reward_cycle_id = burnchain .block_height_to_reward_cycle(burn_tip.block_height) .expect("FATAL: tried to initialize coordinator before first burn block height"); @@ -191,7 +184,8 @@ impl StackerDBListener { .get_latest_chunks(&slot_ids) .inspect_err(|e| warn!("Unable to read the latest signer state from signer db: {e}.")) .unwrap_or_default(); - let mut global_state_evaluator = GlobalStateEvaluator::new(HashMap::new(), address_weights); + let mut global_state_evaluator = + GlobalStateEvaluator::new(HashMap::new(), address_weights, config.is_mainnet()); for (chunk, slot_id) in chunks.into_iter().zip(slot_ids) { let Some(chunk) = chunk else { continue; @@ -216,8 +210,8 @@ impl StackerDBListener { node_keep_running, keep_running, signer_set, - total_weight, - weight_threshold, + total_weight: global_state_evaluator.total_weight, + weight_threshold: global_state_evaluator.approval_weight, signer_entries, blocks: Arc::new((Mutex::new(HashMap::new()), Condvar::new())), signer_idle_timestamps: Arc::new(Mutex::new(HashMap::new())), diff --git a/stacks-signer/src/chainstate/tests/v2.rs b/stacks-signer/src/chainstate/tests/v2.rs index a5aee9c153..4883a8b62f 100644 --- a/stacks-signer/src/chainstate/tests/v2.rs +++ b/stacks-signer/src/chainstate/tests/v2.rs @@ -436,7 +436,7 @@ fn check_sortition_timeout() { let address = StacksAddress::p2pkh(false, &StacksPublicKey::new()); let mut address_weights = HashMap::new(); address_weights.insert(address, 10); - let eval = GlobalStateEvaluator::new(HashMap::new(), address_weights); + let eval = GlobalStateEvaluator::new(HashMap::new(), address_weights, false); // We have not yet timed out assert!(!SortitionState::is_timed_out( &consensus_hash, diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index f43939c670..23087836c8 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -1823,7 +1823,7 @@ impl SignerDb { let weight = eval.address_weights.get(&address).copied().unwrap_or(0); vote_weight = vote_weight.saturating_add(weight); - if eval.reached_agreement(vote_weight) { + if eval.reached_approval(vote_weight) { return Ok(Some(received_time)); } } @@ -3286,7 +3286,7 @@ pub mod tests { address_weights.insert(address_1, 10); address_weights.insert(address_2, 10); address_weights.insert(address_3, 10); - let eval = GlobalStateEvaluator::new(HashMap::new(), address_weights); + let eval = GlobalStateEvaluator::new(HashMap::new(), address_weights, false); assert!(db .get_burn_block_received_time_from_signers(&eval, &burn_block_1, &local_address) diff --git a/stacks-signer/src/tests/signer_state.rs b/stacks-signer/src/tests/signer_state.rs index d338b97d1b..46084efaf3 100644 --- a/stacks-signer/src/tests/signer_state.rs +++ b/stacks-signer/src/tests/signer_state.rs @@ -95,7 +95,7 @@ fn check_capitulate_miner_view() { for address in address_weights.keys() { address_updates.insert(*address, old_update.clone()); } - let mut global_eval = GlobalStateEvaluator::new(address_updates, address_weights); + let mut global_eval = GlobalStateEvaluator::new(address_updates, address_weights, false); let addresses: Vec<_> = global_eval.address_weights.keys().cloned().collect(); // Let's say we are the very first signer in the list @@ -376,7 +376,7 @@ fn check_miner_inactivity_timeout() { let address = *stacks_client.get_signer_address(); address_weights.insert(address, 10_u32); - let eval = GlobalStateEvaluator::new(HashMap::new(), address_weights); + let eval = GlobalStateEvaluator::new(HashMap::new(), address_weights, false); // This local state machine should not change as an uninitialized local state cannot be modified let mut local_state_machine = LocalStateMachine::Uninitialized; local_state_machine diff --git a/stacks-signer/src/v0/signer.rs b/stacks-signer/src/v0/signer.rs index 9646a7f46b..c58bf184aa 100644 --- a/stacks-signer/src/v0/signer.rs +++ b/stacks-signer/src/v0/signer.rs @@ -19,7 +19,7 @@ use std::sync::mpsc::Sender; use std::sync::LazyLock; use std::time::{Duration, Instant, SystemTime}; -use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; +use blockstack_lib::chainstate::nakamoto::NakamotoBlock; use blockstack_lib::net::api::postblock_proposal::{ BlockValidateOk, BlockValidateReject, BlockValidateResponse, ValidateRejectCode, TOO_MANY_REQUESTS_STATUS, @@ -274,6 +274,7 @@ impl SignerTrait for Signer { let global_state_evaluator = GlobalStateEvaluator::new( updates, signer_config.signer_entries.signer_addr_to_weight.clone(), + signer_config.mainnet, ); #[cfg(any(test, feature = "testing"))] let version = signer_config.supported_signer_protocol_version; @@ -1502,13 +1503,6 @@ impl Signer { ) } - /// Compute the total signing weight - fn compute_signature_total_weight(&self) -> u32 { - self.signer_weights - .values() - .fold(0u32, |acc, val| acc.saturating_add(*val)) - } - /// Handle an observed rejection from another signer fn handle_block_rejection( &mut self, @@ -1581,13 +1575,11 @@ impl Signer { }; let total_reject_weight = self.compute_signature_signing_weight(rejection_addrs.iter().map(|(addr, _)| addr)); - let total_weight = self.compute_signature_total_weight(); - - let min_weight = NakamotoBlockHeader::compute_voting_weight_threshold(total_weight) - .unwrap_or_else(|_| { - panic!("{self}: Failed to compute threshold weight for {total_weight}") - }); - if total_reject_weight.saturating_add(min_weight) <= total_weight { + let total_weight = self.global_state_evaluator.total_weight; + if !self + .global_state_evaluator + .reached_rejection(total_reject_weight) + { // Not enough rejection signatures to make a decision info!("{self}: Received block rejection"; "signer_pubkey" => public_key.to_hex(), @@ -1629,13 +1621,16 @@ impl Signer { } // NOTE: This is only used by active signer protocol versions < 2 - // If 30% of the signers have rejected the block due to an invalid + // If a threshold of the signers have rejected the block due to an invalid // reorg, mark the miner as invalid. let total_reorg_reject_weight = self.compute_reject_code_signing_weight( rejection_addrs.iter(), RejectReasonPrefix::ReorgNotAllowed, ); - if total_reorg_reject_weight.saturating_add(min_weight) > total_weight { + if self + .global_state_evaluator + .reached_rejection(total_reorg_reject_weight) + { // Mark the miner as invalid if let Some(sortition_state) = sortition_state { let ch = block_info.block.header.consensus_hash; @@ -1727,14 +1722,12 @@ impl Signer { .collect(); let signature_weight = self.compute_signature_signing_weight(addrs_to_sigs.keys()); - let total_weight = self.compute_signature_total_weight(); - - let min_weight = NakamotoBlockHeader::compute_voting_weight_threshold(total_weight) - .unwrap_or_else(|_| { - panic!("{self}: Failed to compute threshold weight for {total_weight}") - }); + let total_weight = self.global_state_evaluator.total_weight; - if min_weight > signature_weight { + if !self + .global_state_evaluator + .reached_approval(signature_weight) + { info!("{self}: Received block acceptance"; "signer_pubkey" => public_key.to_hex(), "signer_signature_hash" => %block_hash, diff --git a/stacks-signer/src/v0/signer_state.rs b/stacks-signer/src/v0/signer_state.rs index d0183e7529..c946b19ff3 100644 --- a/stacks-signer/src/v0/signer_state.rs +++ b/stacks-signer/src/v0/signer_state.rs @@ -911,7 +911,7 @@ impl LocalStateMachine { let entry = miners.entry(miner_state).or_insert(0); *entry += weight; - if *entry <= eval.total_weight * 3 / 10 { + if !eval.reached_rejection(*entry) { // We don't even see a blocking minority threshold. Ignore. continue; } @@ -919,7 +919,7 @@ impl LocalStateMachine { let nmb_blocks = signerdb .get_globally_accepted_block_count_in_tenure(tenure_id) .unwrap_or(0); - if nmb_blocks == 0 && !eval.reached_agreement(*entry) { + if nmb_blocks == 0 && !eval.reached_approval(*entry) { continue; } diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index e66b0bae59..6d0593e448 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut, Range}; +use clarity::util::compute_voting_weight_threshold; use clarity::util::secp256k1::Secp256k1PublicKey; use clarity::vm::ast::ASTRules; use clarity::vm::costs::ExecutionCost; @@ -85,9 +86,7 @@ use crate::chainstate::stacks::{ use crate::clarity::vm::clarity::TransactionConnection; use crate::clarity_vm::clarity::{ClarityInstance, PreCommitClarityBlock}; use crate::clarity_vm::database::SortitionDBRef; -use crate::core::{ - BOOT_BLOCK_HASH, BURNCHAIN_TX_SEARCH_WINDOW, NAKAMOTO_SIGNER_BLOCK_APPROVAL_THRESHOLD, -}; +use crate::core::{BOOT_BLOCK_HASH, BURNCHAIN_TX_SEARCH_WINDOW}; use crate::monitoring; use crate::net::stackerdb::{StackerDBConfig, MINER_SLOT_COUNT}; use crate::net::Error as net_error; @@ -821,7 +820,11 @@ impl NakamotoBlockHeader { /// Returns the signing weight on success. /// Returns ChainstateError::InvalidStacksBlock on error #[cfg_attr(test, mutants::skip)] - pub fn verify_signer_signatures(&self, reward_set: &RewardSet) -> Result { + pub fn verify_signer_signatures( + &self, + reward_set: &RewardSet, + mainnet: bool, + ) -> Result { let message = self.signer_signature_hash(); let Some(signers) = &reward_set.signers else { return Err(ChainstateError::InvalidStacksBlock( @@ -891,33 +894,15 @@ impl NakamotoBlockHeader { .expect("FATAL: overflow while computing signer set threshold"); } - let threshold = Self::compute_voting_weight_threshold(total_weight)?; + let approval_threshold = compute_voting_weight_threshold(total_weight, mainnet); - if total_weight_signed < threshold { + if total_weight_signed < approval_threshold { return Err(ChainstateError::InvalidStacksBlock(format!( - "Not enough signatures. Needed at least {} but got {} (out of {})", - threshold, total_weight_signed, total_weight, + "Not enough signatures. Needed at least {approval_threshold} but got {total_weight_signed} (out of {total_weight})" ))); } - return Ok(total_weight_signed); - } - - /// Compute the threshold for the minimum number of signers (by weight) required - /// to approve a Nakamoto block. - pub fn compute_voting_weight_threshold(total_weight: u32) -> Result { - let threshold = NAKAMOTO_SIGNER_BLOCK_APPROVAL_THRESHOLD; - let total_weight = u64::from(total_weight); - let ceil = if (total_weight * threshold) % 10 == 0 { - 0 - } else { - 1 - }; - u32::try_from((total_weight * threshold) / 10 + ceil).map_err(|_| { - ChainstateError::InvalidStacksBlock( - "Overflow when computing nakamoto block approval threshold".to_string(), - ) - }) + Ok(total_weight_signed) } /// Make an "empty" header whose block data needs to be filled in. @@ -2546,7 +2531,10 @@ impl NakamotoChainState { return Ok(false); }; - let signing_weight = match block.header.verify_signer_signatures(reward_set) { + let signing_weight = match block + .header + .verify_signer_signatures(reward_set, config.mainnet) + { Ok(x) => x, Err(e) => { warn!("Received block, but the signer signatures are invalid"; diff --git a/stackslib/src/chainstate/nakamoto/tests/mod.rs b/stackslib/src/chainstate/nakamoto/tests/mod.rs index 276de5ccc8..2724392885 100644 --- a/stackslib/src/chainstate/nakamoto/tests/mod.rs +++ b/stackslib/src/chainstate/nakamoto/tests/mod.rs @@ -3032,7 +3032,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; header - .verify_signer_signatures(&reward_set) + .verify_signer_signatures(&reward_set, false) .expect("Failed to verify signatures"); } @@ -3058,7 +3058,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected insufficient signatures to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => { assert!(msg.contains("Not enough signatures")); @@ -3092,7 +3092,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; header - .verify_signer_signatures(&reward_set) + .verify_signer_signatures(&reward_set, false) .expect("Failed to verify signatures"); // assert!(&header.verify_signer_signatures(&reward_set).is_ok()); } @@ -3119,7 +3119,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected out of order signatures to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => { assert!(msg.contains("out of order")); @@ -3150,7 +3150,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected insufficient signatures to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => { assert!(msg.contains("Not enough signatures")); @@ -3184,7 +3184,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; header - .verify_signer_signatures(&reward_set) + .verify_signer_signatures(&reward_set, false) .expect("Failed to verify signatures"); } @@ -3213,7 +3213,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected invalid signature to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => { assert!(msg.contains("not found in the reward set")); @@ -3252,7 +3252,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected duplicate signature to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => { assert!(msg.contains("Signatures are out of order")); @@ -3295,7 +3295,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected invalid message to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => {} _ => panic!("Expected InvalidStacksBlock error"), @@ -3329,7 +3329,7 @@ pub mod nakamoto_block_signatures { header.signer_signature = signer_signature; - match header.verify_signer_signatures(&reward_set) { + match header.verify_signer_signatures(&reward_set, false) { Ok(_) => panic!("Expected invalid message to fail"), Err(ChainstateError::InvalidStacksBlock(msg)) => { if !msg.contains("Unable to recover public key") { @@ -3339,33 +3339,4 @@ pub mod nakamoto_block_signatures { _ => panic!("Expected InvalidStacksBlock error"), } } - - #[test] - pub fn test_compute_voting_weight_threshold() { - assert_eq!( - NakamotoBlockHeader::compute_voting_weight_threshold(100_u32).unwrap(), - 70_u32, - ); - - assert_eq!( - NakamotoBlockHeader::compute_voting_weight_threshold(10_u32).unwrap(), - 7_u32, - ); - - assert_eq!( - NakamotoBlockHeader::compute_voting_weight_threshold(3000_u32).unwrap(), - 2100_u32, - ); - - assert_eq!( - NakamotoBlockHeader::compute_voting_weight_threshold(4000_u32).unwrap(), - 2800_u32, - ); - - // Round-up check - assert_eq!( - NakamotoBlockHeader::compute_voting_weight_threshold(511_u32).unwrap(), - 358_u32, - ); - } } diff --git a/stackslib/src/chainstate/nakamoto/tests/node.rs b/stackslib/src/chainstate/nakamoto/tests/node.rs index 16cacd3530..b14c22daa6 100644 --- a/stackslib/src/chainstate/nakamoto/tests/node.rs +++ b/stackslib/src/chainstate/nakamoto/tests/node.rs @@ -879,7 +879,10 @@ impl TestStacksNode { let mut malleablized_blocks = vec![]; loop { // don't process if we don't have enough signatures - if let Err(e) = block_to_store.header.verify_signer_signatures(&reward_set) { + if let Err(e) = block_to_store + .header + .verify_signer_signatures(&reward_set, chainstate.mainnet) + { info!( "Will stop processing malleablized blocks for {}: {:?}", &block_id, &e diff --git a/stackslib/src/core/mod.rs b/stackslib/src/core/mod.rs index 6c8fbe2e46..d3740efc8b 100644 --- a/stackslib/src/core/mod.rs +++ b/stackslib/src/core/mod.rs @@ -179,10 +179,6 @@ pub const POX_V3_MAINNET_EARLY_UNLOCK_HEIGHT: u32 = pub const POX_V3_TESTNET_EARLY_UNLOCK_HEIGHT: u32 = (BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT as u32) + 1; -// The threshold of weighted votes on a block to approve it in Nakamoto. -// This is out of 10, so 7 means "70%". -pub const NAKAMOTO_SIGNER_BLOCK_APPROVAL_THRESHOLD: u64 = 7; - /// Burn block height at which the ASTRules::PrecheckSize becomes the default behavior on mainnet pub const AST_RULES_PRECHECK_SIZE: u64 = 752000; // on or about Aug 30 2022 diff --git a/stackslib/src/net/api/tests/gethealth.rs b/stackslib/src/net/api/tests/gethealth.rs index 7efcfcce5d..4da6e37573 100644 --- a/stackslib/src/net/api/tests/gethealth.rs +++ b/stackslib/src/net/api/tests/gethealth.rs @@ -120,6 +120,7 @@ fn setup_and_run_nakamoto_health_test( let mut downloader = NakamotoDownloadStateMachine::new( epoch.start_height, rpc_test.peer_1.network.stacks_tip.block_id(), // Initial tip for the downloader state machine + false, ); match nakamoto_download_state { NakamotoDownloadState::Confirmed => { @@ -133,6 +134,7 @@ fn setup_and_run_nakamoto_health_test( RewardSet::empty(), RewardSet::empty(), false, + false, ); let mut header = NakamotoBlockHeader::empty(); @@ -154,6 +156,7 @@ fn setup_and_run_nakamoto_health_test( let mut unconfirmed_tenure = NakamotoUnconfirmedTenureDownloader::new( peer_1_address.clone(), Some(peer_1_tenure_tip.tip_block_id.clone()), + false, ); unconfirmed_tenure.tenure_tip = Some(peer_1_tenure_tip); downloader @@ -316,7 +319,10 @@ fn test_get_health_500_no_initial_neighbors() { let test_observer = TestEventObserver::new(); let mut rpc_test = TestRPC::setup_nakamoto(function_name!(), &test_observer); rpc_test.peer_2.refresh_burnchain_view(); - rpc_test.peer_2.network.init_nakamoto_block_downloader(); + rpc_test + .peer_2 + .network + .init_nakamoto_block_downloader(false); // Mock the PeerDB::get_valid_initial_neighbors to return empty vec by // clearing all peers from the peer DB @@ -408,7 +414,10 @@ fn test_get_health_500_no_download_state() { fn test_get_health_500_no_peers_stats() { let test_observer = TestEventObserver::new(); let mut rpc_test = TestRPC::setup_nakamoto(function_name!(), &test_observer); - rpc_test.peer_2.network.init_nakamoto_block_downloader(); + rpc_test + .peer_2 + .network + .init_nakamoto_block_downloader(false); // --- Invoke the Handler --- let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); let request = StacksHttpRequest::new_gethealth(addr.into(), NeighborsScope::Initial); diff --git a/stackslib/src/net/download/nakamoto/download_state_machine.rs b/stackslib/src/net/download/nakamoto/download_state_machine.rs index 566f17f66e..68d2894ee1 100644 --- a/stackslib/src/net/download/nakamoto/download_state_machine.rs +++ b/stackslib/src/net/download/nakamoto/download_state_machine.rs @@ -97,10 +97,12 @@ pub struct NakamotoDownloadStateMachine { last_unconfirmed_download_check_ms: u128, /// last time an unconfirmed downloader was run last_unconfirmed_download_run_ms: u128, + /// Whether the downloader is operating on mainnet + mainnet: bool, } impl NakamotoDownloadStateMachine { - pub fn new(nakamoto_start_height: u64, nakamoto_tip: StacksBlockId) -> Self { + pub fn new(nakamoto_start_height: u64, nakamoto_tip: StacksBlockId, mainnet: bool) -> Self { Self { nakamoto_start_height, reward_cycle: 0, // will be calculated at runtime @@ -119,6 +121,7 @@ impl NakamotoDownloadStateMachine { fetch_unconfirmed_tenures: false, last_unconfirmed_download_check_ms: 0, last_unconfirmed_download_run_ms: 0, + mainnet, } } @@ -915,6 +918,7 @@ impl NakamotoDownloadStateMachine { &mut self, count: usize, current_reward_sets: &BTreeMap, + mainnet: bool, ) { self.tenure_downloads.make_tenure_downloaders( &mut self.tenure_download_schedule, @@ -922,6 +926,7 @@ impl NakamotoDownloadStateMachine { &self.tenure_block_ids, count, current_reward_sets, + mainnet, ) } @@ -1098,6 +1103,7 @@ impl NakamotoDownloadStateMachine { count: usize, downloaders: &mut HashMap, highest_processed_block_id: Option, + mainnet: bool, ) -> usize { let mut added = 0; schedule.retain(|naddr| { @@ -1111,6 +1117,7 @@ impl NakamotoDownloadStateMachine { let unconfirmed_tenure_download = NakamotoUnconfirmedTenureDownloader::new( naddr.clone(), highest_processed_block_id.clone(), + mainnet, ); debug!("Request unconfirmed tenure state from neighbor {}", &naddr); @@ -1146,6 +1153,7 @@ impl NakamotoDownloadStateMachine { count, &mut self.unconfirmed_tenure_downloads, highest_processed_block_id, + self.mainnet, ); self.last_unconfirmed_download_run_ms = get_epoch_time_ms(); } @@ -1374,7 +1382,7 @@ impl NakamotoDownloadStateMachine { max_count: usize, ) -> HashMap> { // queue up more downloaders - self.update_tenure_downloaders(max_count, &network.current_reward_sets); + self.update_tenure_downloaders(max_count, &network.current_reward_sets, chainstate.mainnet); // run all downloaders let new_blocks = self diff --git a/stackslib/src/net/download/nakamoto/mod.rs b/stackslib/src/net/download/nakamoto/mod.rs index 800c5f4e54..ec0bce9285 100644 --- a/stackslib/src/net/download/nakamoto/mod.rs +++ b/stackslib/src/net/download/nakamoto/mod.rs @@ -140,13 +140,16 @@ pub use crate::net::download::nakamoto::tenure_downloader_unconfirmed::{ impl PeerNetwork { /// Set up the Nakamoto block downloader - pub fn init_nakamoto_block_downloader(&mut self) { + pub fn init_nakamoto_block_downloader(&mut self, mainnet: bool) { if self.block_downloader_nakamoto.is_some() { return; } let epoch = self.get_epoch_by_epoch_id(StacksEpochId::Epoch30); - let downloader = - NakamotoDownloadStateMachine::new(epoch.start_height, self.stacks_tip.block_id()); + let downloader = NakamotoDownloadStateMachine::new( + epoch.start_height, + self.stacks_tip.block_id(), + mainnet, + ); self.block_downloader_nakamoto = Some(downloader); } @@ -159,7 +162,7 @@ impl PeerNetwork { ibd: bool, ) -> Result>, NetError> { if self.block_downloader_nakamoto.is_none() { - self.init_nakamoto_block_downloader(); + self.init_nakamoto_block_downloader(chainstate.mainnet); } let Some(mut block_downloader) = self.block_downloader_nakamoto.take() else { return Ok(HashMap::new()); diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader.rs b/stackslib/src/net/download/nakamoto/tenure_downloader.rs index f15a1a8c89..1242ee4050 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader.rs @@ -117,6 +117,9 @@ pub struct NakamotoTenureDownloader { pub tenure_blocks: Option>, /// Whether this tenure is unconfirmed pub is_tenure_unconfirmed: bool, + + /// Whether this is operating on mainnet + pub mainnet: bool, } impl NakamotoTenureDownloader { @@ -130,6 +133,7 @@ impl NakamotoTenureDownloader { start_signer_keys: RewardSet, end_signer_keys: RewardSet, is_tenure_unconfirmed: bool, + mainnet: bool, ) -> Self { debug!( "Instantiate downloader to {}-{} for tenure {}: {}-{}", @@ -157,6 +161,7 @@ impl NakamotoTenureDownloader { tenure_end_block: None, tenure_blocks: None, is_tenure_unconfirmed, + mainnet, } } @@ -193,7 +198,7 @@ impl NakamotoTenureDownloader { if let Err(e) = tenure_start_block .header - .verify_signer_signatures(&self.start_signer_keys) + .verify_signer_signatures(&self.start_signer_keys, self.mainnet) { // signature verification failed warn!("Invalid tenure-start block: bad signer signature"; @@ -266,7 +271,7 @@ impl NakamotoTenureDownloader { if let Err(e) = tenure_end_block .header - .verify_signer_signatures(&self.end_signer_keys) + .verify_signer_signatures(&self.end_signer_keys, self.mainnet) { // bad signature warn!("Invalid tenure-end block: bad signer signature"; @@ -389,7 +394,7 @@ impl NakamotoTenureDownloader { if let Err(e) = block .header - .verify_signer_signatures(&self.start_signer_keys) + .verify_signer_signatures(&self.start_signer_keys, self.mainnet) { warn!("Invalid block: bad signer signature"; "tenure_id" => %self.tenure_id_consensus_hash, diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs index 674e03f3e9..1b215b7465 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs @@ -358,6 +358,7 @@ impl NakamotoTenureDownloaderSet { tenure_block_ids: &HashMap, count: usize, current_reward_cycles: &BTreeMap, + mainnet: bool, ) { test_debug!("make_tenure_downloaders"; "schedule" => ?schedule, @@ -489,6 +490,7 @@ impl NakamotoTenureDownloaderSet { start_reward_set.clone(), end_reward_set.clone(), false, + mainnet, ); debug!("Request tenure {ch} from neighbor {naddr}"); diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs index b56ffcae61..f381ff1c65 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs @@ -90,12 +90,19 @@ pub struct NakamotoUnconfirmedTenureDownloader { pub unconfirmed_tenure_start_block: Option, /// Unconfirmed tenure blocks obtained pub unconfirmed_tenure_blocks: Option>, + + /// Whether the downloader is operating on mainnet + pub mainnet: bool, } impl NakamotoUnconfirmedTenureDownloader { /// Make a new downloader which will download blocks from the tip back down to the optional /// `highest_processed_block_id` (so we don't re-download the same blocks over and over). - pub fn new(naddr: NeighborAddress, highest_processed_block_id: Option) -> Self { + pub fn new( + naddr: NeighborAddress, + highest_processed_block_id: Option, + mainnet: bool, + ) -> Self { Self { state: NakamotoUnconfirmedDownloadState::GetTenureInfo, naddr, @@ -106,6 +113,7 @@ impl NakamotoUnconfirmedTenureDownloader { tenure_tip: None, unconfirmed_tenure_start_block: None, unconfirmed_tenure_blocks: None, + mainnet, } } @@ -394,7 +402,7 @@ impl NakamotoUnconfirmedTenureDownloader { // stacker signature has to match the current reward set if let Err(e) = unconfirmed_tenure_start_block .header - .verify_signer_signatures(unconfirmed_signer_keys) + .verify_signer_signatures(unconfirmed_signer_keys, self.mainnet) { warn!("Invalid tenure-start block: bad signer signature"; "tenure_start_block.header.consensus_hash" => %unconfirmed_tenure_start_block.header.consensus_hash, @@ -477,7 +485,7 @@ impl NakamotoUnconfirmedTenureDownloader { } if let Err(e) = block .header - .verify_signer_signatures(unconfirmed_signer_keys) + .verify_signer_signatures(unconfirmed_signer_keys, self.mainnet) { warn!("Invalid block: bad signer signature"; "tenure_id" => %tenure_tip.consensus_hash, @@ -719,6 +727,7 @@ impl NakamotoUnconfirmedTenureDownloader { confirmed_signer_keys.clone(), unconfirmed_signer_keys.clone(), true, + self.mainnet, ); Ok(ntd) diff --git a/stackslib/src/net/relay.rs b/stackslib/src/net/relay.rs index 99d87ad642..2f94ff5ca2 100644 --- a/stackslib/src/net/relay.rs +++ b/stackslib/src/net/relay.rs @@ -725,7 +725,10 @@ impl Relayer { return Err(net_error::NoPoXRewardSet(sn_rc)); }; - if let Err(e) = nakamoto_block.header.verify_signer_signatures(reward_set) { + if let Err(e) = nakamoto_block + .header + .verify_signer_signatures(reward_set, chainstate.mainnet) + { warn!( "Signature verification failure for Nakamoto block"; "consensus_hash" => %nakamoto_block.header.consensus_hash, diff --git a/stackslib/src/net/tests/download/nakamoto.rs b/stackslib/src/net/tests/download/nakamoto.rs index 8a74273a3a..7a57baf99d 100644 --- a/stackslib/src/net/tests/download/nakamoto.rs +++ b/stackslib/src/net/tests/download/nakamoto.rs @@ -291,6 +291,7 @@ fn test_nakamoto_tenure_downloader() { reward_set.clone(), reward_set, false, + false, ); // must be first block @@ -520,7 +521,8 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { let mut empty_downloaders = HashMap::new(); let mut full_downloaders = { let mut dl = HashMap::new(); - let utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(tip_block_id)); + let utd = + NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(tip_block_id), false); dl.insert(naddr.clone(), utd); dl }; @@ -529,7 +531,8 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { &mut empty_schedule, 10, &mut empty_downloaders, - None + None, + false ), 0 ); @@ -538,7 +541,8 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { &mut empty_schedule, 10, &mut full_downloaders, - None + None, + false ), 0 ); @@ -547,7 +551,8 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { &mut full_schedule, 10, &mut full_downloaders, - None + None, + false ), 0 ); @@ -557,7 +562,8 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { &mut full_schedule, 10, &mut empty_downloaders, - None + None, + false ), 1 ); @@ -567,7 +573,8 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { // we've processed the tip already, so we transition straight to the Done state { - let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(tip_block_id)); + let mut utd = + NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(tip_block_id), false); assert_eq!(utd.state, NakamotoUnconfirmedDownloadState::GetTenureInfo); utd.confirmed_signer_keys = Some( @@ -642,7 +649,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { let mid_tip_block_id = unconfirmed_tenure.first().as_ref().unwrap().block_id(); let mut utd = - NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(mid_tip_block_id)); + NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(mid_tip_block_id), false); utd.confirmed_signer_keys = Some( current_reward_sets .get(&tip_rc) @@ -741,7 +748,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { let mid_tip_block_id = unconfirmed_tenure.get(5).unwrap().block_id(); let mut utd = - NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(mid_tip_block_id)); + NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), Some(mid_tip_block_id), false); utd.confirmed_signer_keys = Some( current_reward_sets .get(&tip_rc) @@ -839,7 +846,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { // we haven't processed anything yet. // serve all of the unconfirmed blocks in one shot. { - let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), None); + let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), None, false); utd.confirmed_signer_keys = Some( current_reward_sets .get(&tip_rc) @@ -916,7 +923,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { // bad block signature { - let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), None); + let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr.clone(), None, false); utd.confirmed_signer_keys = Some( current_reward_sets .get(&tip_rc) @@ -979,7 +986,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { // Does not consume blocks beyond the highest processed block ID { - let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr, None); + let mut utd = NakamotoUnconfirmedTenureDownloader::new(naddr, None, false); utd.confirmed_signer_keys = Some( current_reward_sets .get(&tip_rc) @@ -2002,6 +2009,7 @@ fn test_make_tenure_downloaders() { &tenure_block_ids, 6, ¤t_reward_sets, + false, ); // made all 6 downloaders @@ -2040,6 +2048,7 @@ fn test_make_tenure_downloaders() { &tenure_block_ids, 12, ¤t_reward_sets, + false, ); // only made 4 downloaders got created diff --git a/stackslib/src/net/unsolicited.rs b/stackslib/src/net/unsolicited.rs index 8f4b1301e3..568c6b3d54 100644 --- a/stackslib/src/net/unsolicited.rs +++ b/stackslib/src/net/unsolicited.rs @@ -16,6 +16,7 @@ use std::collections::HashMap; +use clarity::consts::CHAIN_ID_MAINNET; use stacks_common::types::chainstate::ConsensusHash; use crate::chainstate::burn::db::sortdb::SortitionDB; @@ -824,7 +825,10 @@ impl PeerNetwork { return false; }; - if let Err(e) = nakamoto_block.header.verify_signer_signatures(reward_set) { + if let Err(e) = nakamoto_block.header.verify_signer_signatures( + reward_set, + self.get_local_peer().network_id != CHAIN_ID_MAINNET, + ) { info!( "{:?}: signature verification failure for Nakamoto block {}/{} in reward cycle {}: {:?}", self.get_local_peer(), &nakamoto_block.header.consensus_hash, &nakamoto_block.header.block_hash(), reward_cycle, &e ); From b4c938bc4c50334c1d0e20fb7fc5919ba921babb Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 15 Aug 2025 13:27:23 -0700 Subject: [PATCH 2/3] Add a SIGNER_APPROVAL_THRESHOLD integration test Signed-off-by: Jacinta Ferrant --- stacks-node/src/tests/signer/v0.rs | 84 ++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/stacks-node/src/tests/signer/v0.rs b/stacks-node/src/tests/signer/v0.rs index fac59e3a76..813cff4958 100644 --- a/stacks-node/src/tests/signer/v0.rs +++ b/stacks-node/src/tests/signer/v0.rs @@ -18462,3 +18462,87 @@ fn signer_loads_stackerdb_updates_on_startup() { info!("------------------------- Shutdown -------------------------"); miners.shutdown(); } + +#[test] +#[ignore] +#[serial_test::serial] +/// This test verifies that a a signer will send update messages to stackerdb when it updates its internal state +/// +/// For a new bitcoin block arrival, the signers send a local state update message with this updated block and miner +/// For an inactive miner, the signer sends a local state update message indicating it is reverting to the prior miner +fn custom_signer_approval_threshold_testnet() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + std::env::set_var("SIGNER_APPROVAL_THRESHOLD", "60"); + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("------------------------- Test Setup -------------------------"); + let num_signers = 3; // Let's only set 3 signers to that we can ensure if only 2 approve, we still hit global acceptance + let signer_test: SignerTest = SignerTest::new(num_signers, vec![]); + signer_test.boot_to_epoch_3(); + + let rejecting_signer = + StacksPublicKey::from_private(&signer_test.signer_stacks_private_keys[0]); + info!("------------------------- Make Signer {rejecting_signer:?} Reject all Proposals (33% reject)-------------------------"); + let rejecting_signers = vec![rejecting_signer.clone()]; + TEST_REJECT_ALL_BLOCK_PROPOSAL.set(rejecting_signers.clone()); + + test_observer::clear(); + + info!("------------------------- Ensure Chain Does Not Halt Even with 1 Rejection -------------------------"); + signer_test.mine_nakamoto_block(Duration::from_secs(30), true); + + let mined_block = test_observer::get_mined_nakamoto_blocks().pop().unwrap(); + wait_for_block_rejections_from_signers( + 30, + &mined_block.signer_signature_hash, + &rejecting_signers, + ) + .expect("Expected signer to reject block proposal"); + + let second_rejecting_signer = + StacksPublicKey::from_private(&signer_test.signer_stacks_private_keys[1]); + info!("------------------------- Make Signer {second_rejecting_signer:?} Reject all Proposals (66% reject) -------------------------"); + let rejecting_signers = vec![rejecting_signer.clone(), second_rejecting_signer.clone()]; + TEST_REJECT_ALL_BLOCK_PROPOSAL.set(rejecting_signers.clone()); + + let peer_info = signer_test.get_peer_info(); + let stacks_miner_pk = StacksPublicKey::from_private( + &signer_test + .running_nodes + .conf + .miner + .mining_key + .clone() + .unwrap(), + ); + signer_test.mine_bitcoin_block(); + let rejected_block = + wait_for_block_proposal(30, peer_info.stacks_tip_height + 1, &stacks_miner_pk) + .expect("Failed to get block proposal"); + + wait_for_block_rejections_from_signers( + 30, + &rejected_block.header.signer_signature_hash(), + &rejecting_signers, + ) + .expect("Expected signer to reject block proposal"); + + info!("------------------------- Verify Chain Halts -------------------------"); + assert!( + wait_for(15, || { + Ok(signer_test.get_peer_info().stacks_tip_height > peer_info.stacks_tip_height) + }) + .is_err(), + "Expected the chain to halt" + ); + + info!("------------------------- Shutdown -------------------------"); + signer_test.shutdown(); +} From 76000a115e3b97479ae1a83cf0be3c4c79cce870 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 15 Aug 2025 14:59:15 -0700 Subject: [PATCH 3/3] Hide the shame! accidental != instead of == Signed-off-by: Jacinta Ferrant --- stackslib/src/net/unsolicited.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/net/unsolicited.rs b/stackslib/src/net/unsolicited.rs index 568c6b3d54..97aedfb85a 100644 --- a/stackslib/src/net/unsolicited.rs +++ b/stackslib/src/net/unsolicited.rs @@ -827,7 +827,7 @@ impl PeerNetwork { if let Err(e) = nakamoto_block.header.verify_signer_signatures( reward_set, - self.get_local_peer().network_id != CHAIN_ID_MAINNET, + self.get_local_peer().network_id == CHAIN_ID_MAINNET, ) { info!( "{:?}: signature verification failure for Nakamoto block {}/{} in reward cycle {}: {:?}", self.get_local_peer(), &nakamoto_block.header.consensus_hash, &nakamoto_block.header.block_hash(), reward_cycle, &e