@@ -94,7 +94,8 @@ use stacks_signer::v0::signer_state::{
9494use stacks_signer::v0::tests::{
9595 TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_PAUSE_BLOCK_BROADCAST,
9696 TEST_PIN_SUPPORTED_SIGNER_PROTOCOL_VERSION, TEST_REJECT_ALL_BLOCK_PROPOSAL,
97- TEST_SKIP_BLOCK_BROADCAST, TEST_SKIP_SIGNER_CLEANUP, TEST_STALL_BLOCK_VALIDATION_SUBMISSION,
97+ TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST, TEST_SKIP_BLOCK_BROADCAST, TEST_SKIP_SIGNER_CLEANUP,
98+ TEST_STALL_BLOCK_VALIDATION_SUBMISSION,
9899};
99100use stacks_signer::v0::SpawnedSigner;
100101use tracing_subscriber::prelude::*;
@@ -1328,6 +1329,37 @@ pub fn wait_for_block_pushed_by_miner_key(
13281329 block.ok_or_else(|| "Failed to find block pushed".to_string())
13291330}
13301331
1332+ /// Waits for all of the provided signers to send a pre-commit for a block
1333+ /// with the provided signer signature hash
1334+ pub fn wait_for_block_pre_commits_from_signers(
1335+ timeout_secs: u64,
1336+ signer_signature_hash: &Sha512Trunc256Sum,
1337+ expected_signers: &[StacksPublicKey],
1338+ ) -> Result<(), String> {
1339+ wait_for(timeout_secs, || {
1340+ let chunks = test_observer::get_stackerdb_chunks()
1341+ .into_iter()
1342+ .flat_map(|chunk| chunk.modified_slots)
1343+ .filter_map(|chunk| {
1344+ let pk = chunk.recover_pk().expect("Failed to recover pk");
1345+ if !expected_signers.contains(&pk) {
1346+ return None;
1347+ }
1348+ let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
1349+ .expect("Failed to deserialize SignerMessage");
1350+
1351+ if let SignerMessage::BlockPreCommit(hash) = message {
1352+ if hash == *signer_signature_hash {
1353+ return Some(pk);
1354+ }
1355+ }
1356+ None
1357+ })
1358+ .collect::<HashSet<_>>();
1359+ Ok(chunks.len() == expected_signers.len())
1360+ })
1361+ }
1362+
13311363/// Waits for >30% of num_signers block rejection to be observed in the test_observer stackerdb chunks for a block
13321364/// with the provided signer signature hash
13331365fn wait_for_block_global_rejection(
@@ -1862,22 +1894,47 @@ fn miner_gather_signatures() {
18621894 // Test prometheus metrics response
18631895 #[cfg(feature = "monitoring_prom")]
18641896 {
1897+ let min_num_expected = (num_signers * 2) as u64;
18651898 wait_for(30, || {
1899+ use regex::Regex;
1900+
18661901 let metrics_response = signer_test.get_signer_metrics();
1902+ let re_precommits =
1903+ Regex::new(r#"stacks_signer_block_pre_commits_sent (\d+)"#).unwrap();
1904+ let re_proposals =
1905+ Regex::new(r#"stacks_signer_block_proposals_received (\d+)"#).unwrap();
1906+ let re_responses = Regex::new(
1907+ r#"stacks_signer_block_responses_sent\{response_type="accepted"\} (\d+)"#,
1908+ )
1909+ .unwrap();
18671910
1868- // Because 5 signers are running in the same process, the prometheus metrics
1869- // are incremented once for every signer.When booting to Epoch 3.0, the old
1870- // miner will attempt to propose a block before its burnchain tip has updated
1871- // causing an additional block proposal that gets rejected due to consensus hash
1872- // mismatch, hence why we expect 15 rather than just 10 proposals.
1873- let expected_result_1 =
1874- format!("stacks_signer_block_proposals_received {}", num_signers * 2);
1875- let expected_result_2 = format!(
1876- "stacks_signer_block_responses_sent{{response_type=\"accepted\"}} {}",
1877- num_signers * 2
1878- );
1879- Ok(metrics_response.contains(&expected_result_1)
1880- && metrics_response.contains(&expected_result_2))
1911+ let precommits = re_precommits
1912+ .captures(&metrics_response)
1913+ .and_then(|caps| caps.get(1))
1914+ .map(|m| m.as_str().parse::<u64>().ok())
1915+ .flatten();
1916+
1917+ let proposals = re_proposals
1918+ .captures(&metrics_response)
1919+ .and_then(|caps| caps.get(1))
1920+ .map(|m| m.as_str().parse::<u64>().ok())
1921+ .flatten();
1922+
1923+ let responses = re_responses
1924+ .captures(&metrics_response)
1925+ .and_then(|caps| caps.get(1))
1926+ .map(|m| m.as_str().parse::<u64>().ok())
1927+ .flatten();
1928+
1929+ if let (Some(proposals), Some(responses), Some(precommits)) =
1930+ (proposals, responses, precommits)
1931+ {
1932+ Ok(proposals >= min_num_expected
1933+ && responses >= min_num_expected
1934+ && precommits >= min_num_expected)
1935+ } else {
1936+ Ok(false)
1937+ }
18811938 })
18821939 .expect("Failed to advance prometheus metrics");
18831940 }
@@ -9249,7 +9306,7 @@ fn locally_rejected_blocks_overriden_by_global_acceptance() {
92499306/// The stacks node is then advanced to Epoch 3.0 boundary to allow block signing.
92509307///
92519308/// Test Execution:
9252- /// The node mines 1 stacks block N (all signers sign it). The subsequent block N+1 is proposed, but <30% accept it. The remaining signers
9309+ /// The node mines 1 stacks block N (all signers sign it). The subsequent block N+1 is proposed, but <30% pre-commit to it. The remaining signers
92539310/// do not make a decision on the block. A new tenure begins and the miner proposes a new block N+1' which all signers accept.
92549311///
92559312/// Test Assertion:
@@ -9334,7 +9391,7 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() {
93349391 .cloned()
93359392 .skip(num_signers * 7 / 10)
93369393 .collect();
9337- TEST_IGNORE_ALL_BLOCK_PROPOSALS .set(ignoring_signers.clone());
9394+ TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST .set(ignoring_signers.clone());
93389395 // Clear the stackerdb chunks
93399396 test_observer::clear();
93409397
@@ -9354,12 +9411,12 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() {
93549411 wait_for_block_proposal(30, info_before.stacks_tip_height + 1, &miner_pk)
93559412 .expect("Timed out waiting for block N+1 to be proposed");
93569413 // Make sure that the non ignoring signers do actually accept it though
9357- wait_for_block_acceptance_from_signers (
9414+ wait_for_block_pre_commits_from_signers (
93589415 30,
93599416 &block_n_1_proposal.header.signer_signature_hash(),
93609417 &non_ignoring_signers,
93619418 )
9362- .expect("Timed out waiting for block acceptances of N+1");
9419+ .expect("Timed out waiting for block pre-commits of N+1");
93639420 let info_after = signer_test.get_peer_info();
93649421 assert_eq!(info_after, info_before);
93659422 assert_ne!(
@@ -9402,7 +9459,7 @@ fn reorg_locally_accepted_blocks_across_tenures_succeeds() {
94029459 );
94039460 let info_before = signer_test.get_peer_info();
94049461 test_observer::clear();
9405- TEST_IGNORE_ALL_BLOCK_PROPOSALS .set(Vec::new());
9462+ TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST .set(Vec::new());
94069463 TEST_MINE_SKIP.set(false);
94079464
94089465 let block_n_1_prime =
@@ -9546,7 +9603,7 @@ fn reorg_locally_accepted_blocks_across_tenures_fails() {
95469603 .cloned()
95479604 .skip(num_signers * 7 / 10)
95489605 .collect();
9549- TEST_IGNORE_ALL_BLOCK_PROPOSALS .set(ignoring_signers.clone());
9606+ TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST .set(ignoring_signers.clone());
95509607 // Clear the stackerdb chunks
95519608 test_observer::clear();
95529609
@@ -12740,7 +12797,7 @@ fn injected_signatures_are_ignored_across_boundaries() {
1274012797 .collect();
1274112798 assert_eq!(ignoring_signers.len(), 3);
1274212799 assert_eq!(non_ignoring_signers.len(), 2);
12743- TEST_IGNORE_ALL_BLOCK_PROPOSALS .set(ignoring_signers.clone());
12800+ TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST .set(ignoring_signers.clone());
1274412801
1274512802 let info_before = signer_test.get_peer_info();
1274612803 // submit a tx so that the miner will ATTEMPT to mine a stacks block N
@@ -15529,10 +15586,8 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() {
1552915586 .signer_test
1553015587 .check_signer_states_reorg(&approving_signers, &rejecting_signers);
1553115588
15532- info!("------------------------- Wait for 3 acceptances and 2 rejections -------------------------");
1553315589 let signer_signature_hash = block_n_1_prime.header.signer_signature_hash();
15534- wait_for_block_acceptance_from_signers(30, &signer_signature_hash, &approving_signers)
15535- .expect("Timed out waiting for block acceptance from approving signers");
15590+ info!("------------------------- Wait for 3 acceptances and 2 rejections of {signer_signature_hash} -------------------------");
1553615591 let rejections =
1553715592 wait_for_block_rejections_from_signers(30, &signer_signature_hash, &rejecting_signers)
1553815593 .expect("Timed out waiting for block rejection from rejecting signers");
@@ -15543,6 +15598,8 @@ fn mark_miner_as_invalid_if_reorg_is_rejected_v1() {
1554315598 "Reject reason is not ReorgNotAllowed"
1554415599 );
1554515600 }
15601+ wait_for_block_pre_commits_from_signers(30, &signer_signature_hash, &approving_signers)
15602+ .expect("Timed out waiting for block pre-commits from approving signers");
1554615603
1554715604 info!("------------------------- Miner 1 Proposes N+1' Again -------------------------");
1554815605 test_observer::clear();
@@ -18077,3 +18134,74 @@ fn bitcoin_reorg_extended_tenure() {
1807718134
1807818135 miners.shutdown();
1807918136}
18137+
18138+ // Basic test to ensure that signers will not issue a signature over a block proposal unless
18139+ // a threshold number of signers have pre-committed to sign.
18140+ #[test]
18141+ #[ignore]
18142+ fn signers_do_not_commit_unless_threshold_precommitted() {
18143+ if env::var("BITCOIND_TEST") != Ok("1".into()) {
18144+ return;
18145+ }
18146+
18147+ info!("------------------------- Test Setup -------------------------");
18148+ let num_signers = 20;
18149+
18150+ let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new(num_signers, vec![]);
18151+ let miner_sk = signer_test.running_nodes.conf.miner.mining_key.unwrap();
18152+ let miner_pk = StacksPublicKey::from_private(&miner_sk);
18153+ let all_signers = signer_test.signer_test_pks();
18154+
18155+ signer_test.boot_to_epoch_3();
18156+
18157+ // Make sure that more than 30% of signers are set to ignore any incoming proposals so that consensus is not reached
18158+ // on pre-commit round.
18159+ let ignore_signers: Vec<_> = all_signers
18160+ .iter()
18161+ .cloned()
18162+ .take(all_signers.len() / 2)
18163+ .collect();
18164+ let pre_commit_signers: Vec<_> = all_signers
18165+ .iter()
18166+ .cloned()
18167+ .skip(all_signers.len() / 2)
18168+ .collect();
18169+ TEST_IGNORE_ALL_BLOCK_PROPOSALS.set(ignore_signers);
18170+ test_observer::clear();
18171+ let blocks_before = test_observer::get_mined_nakamoto_blocks().len();
18172+ let height_before = signer_test.get_peer_info().stacks_tip_height;
18173+ next_block_and(
18174+ &mut signer_test.running_nodes.btc_regtest_controller,
18175+ 30,
18176+ || Ok(test_observer::get_mined_nakamoto_blocks().len() > blocks_before),
18177+ )
18178+ .unwrap();
18179+
18180+ let proposal = wait_for_block_proposal(30, height_before + 1, &miner_pk)
18181+ .expect("Timed out waiting for block proposal");
18182+ let hash = proposal.header.signer_signature_hash();
18183+ wait_for_block_pre_commits_from_signers(30, &hash, &pre_commit_signers)
18184+ .expect("Timed out waiting for pre-commits");
18185+ assert!(
18186+ wait_for(30, || {
18187+ for chunk in test_observer::get_stackerdb_chunks()
18188+ .into_iter()
18189+ .flat_map(|chunk| chunk.modified_slots)
18190+ {
18191+ let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
18192+ .expect("Failed to deserialize SignerMessage");
18193+ if let SignerMessage::BlockResponse(BlockResponse::Accepted(accepted)) = message {
18194+ if accepted.signer_signature_hash == hash {
18195+ return Ok(true);
18196+ }
18197+ }
18198+ }
18199+ Ok(false)
18200+ })
18201+ .is_err(),
18202+ "Should not have found a single block accept for the block hash {hash}"
18203+ );
18204+
18205+ info!("------------------------- Shutdown -------------------------");
18206+ signer_test.shutdown();
18207+ }
0 commit comments