Skip to content

Commit 3e4a0ef

Browse files
committed
Add a test and fix failing tests
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 8f821f2 commit 3e4a0ef

File tree

3 files changed

+184
-30
lines changed

3 files changed

+184
-30
lines changed

stacks-node/src/tests/signer/v0.rs

Lines changed: 152 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ use stacks_signer::v0::signer_state::{
9494
use 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
};
99100
use stacks_signer::v0::SpawnedSigner;
100101
use 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
13331365
fn 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+
}

stacks-signer/src/v0/signer.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,9 @@ impl Signer {
908908

909909
#[cfg(any(test, feature = "testing"))]
910910
fn send_block_response(&mut self, block: &NakamotoBlock, block_response: BlockResponse) {
911+
if self.test_skip_signature_broadcast(&block_response) {
912+
return;
913+
}
911914
const NUM_REPEATS: usize = 1;
912915
let mut count = 0;
913916
let public_keys = TEST_REPEAT_PROPOSAL_RESPONSE.get();
@@ -1062,7 +1065,7 @@ impl Signer {
10621065
let accepted = self.create_block_acceptance(&block_info.block);
10631066
// have to save the signature _after_ the block info
10641067
self.handle_block_signature(stacks_client, &accepted);
1065-
self.impl_send_block_response(&block_info.block, accepted.into());
1068+
self.send_block_response(&block_info.block, accepted.into());
10661069
}
10671070

10681071
/// Handle block proposal messages submitted to signers stackerdb
@@ -1216,7 +1219,7 @@ impl Signer {
12161219
return;
12171220
};
12181221

1219-
self.impl_send_block_response(&block_info.block, block_response);
1222+
self.send_block_response(&block_info.block, block_response);
12201223
}
12211224

12221225
/// Handle block response messages from a signer
@@ -1374,7 +1377,7 @@ impl Signer {
13741377
.insert_block(&block_info)
13751378
.unwrap_or_else(|e| self.handle_insert_block_error(e));
13761379
self.handle_block_rejection(&block_rejection, sortition_state);
1377-
self.impl_send_block_response(&block_info.block, block_rejection.into());
1380+
self.send_block_response(&block_info.block, block_rejection.into());
13781381
} else {
13791382
if let Err(e) = block_info.mark_locally_accepted(false) {
13801383
if !block_info.has_reached_consensus() {
@@ -1448,7 +1451,7 @@ impl Signer {
14481451
.insert_block(&block_info)
14491452
.unwrap_or_else(|e| self.handle_insert_block_error(e));
14501453
self.handle_block_rejection(&block_rejection, sortition_state);
1451-
self.impl_send_block_response(&block_info.block, block_rejection.into());
1454+
self.send_block_response(&block_info.block, block_rejection.into());
14521455
}
14531456

14541457
/// Handle the block validate response returned from our prior calls to submit a block for validation
@@ -1566,7 +1569,7 @@ impl Signer {
15661569
warn!("{self}: Failed to mark block as locally rejected: {e:?}");
15671570
}
15681571
};
1569-
self.impl_send_block_response(&block_info.block, rejection.into());
1572+
self.send_block_response(&block_info.block, rejection.into());
15701573

15711574
self.signer_db
15721575
.insert_block(&block_info)

stacks-signer/src/v0/tests.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::collections::HashMap;
1717
use std::sync::LazyLock;
1818

1919
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
20-
use libsigner::v0::messages::{BlockRejection, RejectReason};
20+
use libsigner::v0::messages::{BlockRejection, BlockResponse, RejectReason};
2121
use libsigner::BlockProposal;
2222
use stacks_common::types::chainstate::StacksPublicKey;
2323
use stacks_common::util::get_epoch_time_secs;
@@ -53,6 +53,10 @@ pub static TEST_STALL_BLOCK_VALIDATION_SUBMISSION: LazyLock<TestFlag<bool>> =
5353
/// A global variable that can be used to prevent signer cleanup
5454
pub static TEST_SKIP_SIGNER_CLEANUP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
5555

56+
/// A global variable that can be used to skip signature broadcast if the signer's public key is in the provided list
57+
pub static TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST: LazyLock<TestFlag<Vec<StacksPublicKey>>> =
58+
LazyLock::new(TestFlag::default);
59+
5660
impl Signer {
5761
/// Skip the block broadcast if the TEST_SKIP_BLOCK_BROADCAST flag is set
5862
pub fn test_skip_block_broadcast(&self, block: &NakamotoBlock) -> bool {
@@ -169,4 +173,23 @@ impl Signer {
169173
}
170174
self.supported_signer_protocol_version
171175
}
176+
177+
/// Skip the block broadcast if the TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST flag is set for the signer
178+
pub fn test_skip_signature_broadcast(&self, block_response: &BlockResponse) -> bool {
179+
if block_response.as_block_accepted().is_none() {
180+
return false;
181+
}
182+
let hash = block_response.get_signer_signature_hash();
183+
let public_keys = TEST_SIGNERS_SKIP_SIGNATURE_BROADCAST.get();
184+
if public_keys.contains(
185+
&stacks_common::types::chainstate::StacksPublicKey::from_private(&self.private_key),
186+
) {
187+
warn!(
188+
"{self}: Skipping signature broadcast due to testing directive";
189+
"signer_signature_hash" => %hash
190+
);
191+
return true;
192+
}
193+
false
194+
}
172195
}

0 commit comments

Comments
 (0)