Skip to content

Commit b473984

Browse files
committed
Add global_acceptance_depends_on_block_announcement
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent d4860f8 commit b473984

File tree

4 files changed

+299
-6
lines changed

4 files changed

+299
-6
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ jobs:
127127
- tests::signer::v0::continue_after_fast_block_no_sortition
128128
- tests::signer::v0::block_validation_response_timeout
129129
- tests::signer::v0::tenure_extend_after_bad_commit
130+
- tests::signer::v0::global_acceptance_depends_on_block_announcement
130131
- tests::nakamoto_integrations::burn_ops_integration_test
131132
- tests::nakamoto_integrations::check_block_heights
132133
- tests::nakamoto_integrations::clarity_burn_state

stacks-signer/src/v0/signer.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ pub static TEST_PAUSE_BLOCK_BROADCAST: std::sync::Mutex<Option<bool>> = std::syn
6464
/// Skip broadcasting the block to the network
6565
pub static TEST_SKIP_BLOCK_BROADCAST: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None);
6666

67+
#[cfg(any(test, feature = "testing"))]
68+
/// Skip any block responses from other signers
69+
pub static TEST_IGNORE_BLOCK_RESPONSES: std::sync::Mutex<Option<bool>> =
70+
std::sync::Mutex::new(None);
71+
6772
/// The stacks signer registered for the reward cycle
6873
#[derive(Debug)]
6974
pub struct Signer {
@@ -533,6 +538,10 @@ impl Signer {
533538
stacks_client: &StacksClient,
534539
block_response: &BlockResponse,
535540
) {
541+
#[cfg(any(test, feature = "testing"))]
542+
if self.test_ignore_block_responses(block_response) {
543+
return;
544+
}
536545
match block_response {
537546
BlockResponse::Accepted(accepted) => {
538547
self.handle_block_signature(stacks_client, accepted);
@@ -1121,6 +1130,18 @@ impl Signer {
11211130
}
11221131
}
11231132

1133+
#[cfg(any(test, feature = "testing"))]
1134+
fn test_ignore_block_responses(&self, block_response: &BlockResponse) -> bool {
1135+
if *TEST_IGNORE_BLOCK_RESPONSES.lock().unwrap() == Some(true) {
1136+
warn!(
1137+
"{self}: Ignoring block response due to testing directive";
1138+
"block_response" => %block_response
1139+
);
1140+
return true;
1141+
}
1142+
false
1143+
}
1144+
11241145
#[cfg(any(test, feature = "testing"))]
11251146
fn test_pause_block_broadcast(&self, block_info: &BlockInfo) {
11261147
if *TEST_PAUSE_BLOCK_BROADCAST.lock().unwrap() == Some(true) {

testnet/stacks-node/src/event_dispatcher.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ use url::Url;
7070

7171
use super::config::{EventKeyType, EventObserverConfig};
7272

73+
#[cfg(any(test, feature = "testing"))]
74+
pub static TEST_SKIP_BLOCK_ANNOUNCEMENT: std::sync::Mutex<Option<bool>> =
75+
std::sync::Mutex::new(None);
76+
7377
#[derive(Debug, Clone)]
7478
struct EventObserver {
7579
/// Path to the database where pending payloads are stored. If `None`, then
@@ -1299,6 +1303,11 @@ impl EventDispatcher {
12991303

13001304
let mature_rewards = serde_json::Value::Array(mature_rewards_vec);
13011305

1306+
#[cfg(any(test, feature = "testing"))]
1307+
if test_skip_block_announcement(&block) {
1308+
return;
1309+
}
1310+
13021311
for (observer_id, filtered_events_ids) in dispatch_matrix.iter().enumerate() {
13031312
let filtered_events: Vec<_> = filtered_events_ids
13041313
.iter()
@@ -1695,6 +1704,18 @@ impl EventDispatcher {
16951704
}
16961705
}
16971706

1707+
#[cfg(any(test, feature = "testing"))]
1708+
fn test_skip_block_announcement(block: &StacksBlockEventData) -> bool {
1709+
if *TEST_SKIP_BLOCK_ANNOUNCEMENT.lock().unwrap() == Some(true) {
1710+
warn!(
1711+
"Skipping new block announcement due to testing directive";
1712+
"block_hash" => %block.block_hash
1713+
);
1714+
return true;
1715+
}
1716+
false
1717+
}
1718+
16981719
#[cfg(test)]
16991720
mod test {
17001721
use std::net::TcpListener;

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

Lines changed: 256 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ use stacks::net::api::postblock_proposal::{ValidateRejectCode, TEST_VALIDATE_STA
4343
use stacks::net::relay::fault_injection::set_ignore_block;
4444
use stacks::types::chainstate::{StacksAddress, StacksBlockId, StacksPrivateKey, StacksPublicKey};
4545
use stacks::types::PublicKey;
46-
use stacks::util::hash::{hex_bytes, Hash160, MerkleHashFunc};
46+
use stacks::util::hash::{hex_bytes, Hash160, MerkleHashFunc, MerkleTree, Sha512Trunc256Sum};
4747
use stacks::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey};
4848
use stacks::util_lib::boot::boot_code_id;
4949
use stacks::util_lib::signed_structured_data::pox4::{
@@ -56,16 +56,16 @@ use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView};
5656
use stacks_signer::client::{SignerSlotID, StackerDB};
5757
use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network};
5858
use stacks_signer::v0::signer::{
59-
TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_PAUSE_BLOCK_BROADCAST, TEST_REJECT_ALL_BLOCK_PROPOSAL,
60-
TEST_SKIP_BLOCK_BROADCAST,
59+
TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_IGNORE_BLOCK_RESPONSES, TEST_PAUSE_BLOCK_BROADCAST,
60+
TEST_REJECT_ALL_BLOCK_PROPOSAL, TEST_SKIP_BLOCK_BROADCAST,
6161
};
6262
use stacks_signer::v0::SpawnedSigner;
6363
use tracing_subscriber::prelude::*;
6464
use tracing_subscriber::{fmt, EnvFilter};
6565

6666
use super::SignerTest;
6767
use crate::config::{EventKeyType, EventObserverConfig};
68-
use crate::event_dispatcher::MinedNakamotoBlockEvent;
68+
use crate::event_dispatcher::{MinedNakamotoBlockEvent, TEST_SKIP_BLOCK_ANNOUNCEMENT};
6969
use crate::nakamoto_node::miner::{
7070
TEST_BLOCK_ANNOUNCE_STALL, TEST_BROADCAST_STALL, TEST_MINE_STALL,
7171
};
@@ -375,7 +375,7 @@ impl SignerTest<SpawnedSigner> {
375375
}
376376
}
377377

378-
/// Propose an invalid block to the signers
378+
/// Propose a block to the signers
379379
fn propose_block(&mut self, block: NakamotoBlock, timeout: Duration) {
380380
let miners_contract_id = boot_code_id(MINERS_NAME, false);
381381
let mut session =
@@ -385,6 +385,7 @@ impl SignerTest<SpawnedSigner> {
385385
.btc_regtest_controller
386386
.get_headers_height();
387387
let reward_cycle = self.get_current_reward_cycle();
388+
let signer_signature_hash = block.header.signer_signature_hash();
388389
let message = SignerMessage::BlockProposal(BlockProposal {
389390
block,
390391
burn_height,
@@ -401,7 +402,7 @@ impl SignerTest<SpawnedSigner> {
401402
let mut version = 0;
402403
let slot_id = MinerSlotID::BlockProposal.to_u8() as u32;
403404
let start = Instant::now();
404-
debug!("Proposing invalid block to signers");
405+
debug!("Proposing block to signers: {signer_signature_hash}");
405406
while !accepted {
406407
let mut chunk =
407408
StackerDBChunkData::new(slot_id * 2, version, message.serialize_to_vec());
@@ -8557,3 +8558,252 @@ fn tenure_extend_after_2_bad_commits() {
85578558
run_loop_2_thread.join().unwrap();
85588559
signer_test.shutdown();
85598560
}
8561+
8562+
#[test]
8563+
#[ignore]
8564+
/// Test that signers that reject a block locally, but that was accepted globally will accept
8565+
/// only accept a block built upon it when they receive the new block event confirming their prior
8566+
/// rejected block.
8567+
///
8568+
/// Test Setup:
8569+
/// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind.
8570+
/// The stacks node is then advanced to Epoch 3.0 boundary to allow block signing.
8571+
///
8572+
/// Test Execution:
8573+
/// The node mines 1 stacks block N (all signers sign it). <30% of signers are configured to auto reject
8574+
/// any block proposals, announcement of new blocks are skipped, and signatures ignored by signers.
8575+
/// The subsequent block N+1 is proposed, triggering one of the <30% signers submit the block to the node
8576+
/// for validation. The node will fail due to a bad block header hash mismatch (passes height checks)
8577+
///
8578+
/// Test Assertion:
8579+
/// - All signers accepted block N.
8580+
/// - Less than 30% of the signers rejected block N+1.
8581+
/// - The 30% of signers that rejected block N+1, will submit the block for validation
8582+
/// as it passes preliminary checks (even though its a sister block, it is a sister block to a locally rejected block)
8583+
fn global_acceptance_depends_on_block_announcement() {
8584+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
8585+
return;
8586+
}
8587+
8588+
tracing_subscriber::registry()
8589+
.with(fmt::layer())
8590+
.with(EnvFilter::from_default_env())
8591+
.init();
8592+
8593+
info!("------------------------- Test Setup -------------------------");
8594+
let num_signers = 5;
8595+
let sender_sk = Secp256k1PrivateKey::new();
8596+
let sender_addr = tests::to_addr(&sender_sk);
8597+
let send_amt = 100;
8598+
let send_fee = 180;
8599+
let nmb_txs = 4;
8600+
8601+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
8602+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new(
8603+
num_signers,
8604+
vec![(sender_addr, (send_amt + send_fee) * nmb_txs)],
8605+
);
8606+
8607+
let all_signers: Vec<_> = signer_test
8608+
.signer_stacks_private_keys
8609+
.iter()
8610+
.map(StacksPublicKey::from_private)
8611+
.collect();
8612+
8613+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
8614+
let short_timeout = 30;
8615+
signer_test.boot_to_epoch_3();
8616+
8617+
info!("------------------------- Test Mine Nakamoto Block N -------------------------");
8618+
let info_before = signer_test
8619+
.stacks_client
8620+
.get_peer_info()
8621+
.expect("Failed to get peer info");
8622+
8623+
test_observer::clear();
8624+
// submit a tx so that the miner will mine a stacks block N
8625+
let mut sender_nonce = 0;
8626+
let transfer_tx = make_stacks_transfer(
8627+
&sender_sk,
8628+
sender_nonce,
8629+
send_fee,
8630+
signer_test.running_nodes.conf.burnchain.chain_id,
8631+
&recipient,
8632+
send_amt,
8633+
);
8634+
let tx = submit_tx(&http_origin, &transfer_tx);
8635+
sender_nonce += 1;
8636+
info!("Submitted tx {tx} in to mine block N");
8637+
8638+
wait_for(short_timeout, || {
8639+
Ok(signer_test
8640+
.stacks_client
8641+
.get_peer_info()
8642+
.expect("Failed to get peer info")
8643+
.stacks_tip_height
8644+
> info_before.stacks_tip_height)
8645+
})
8646+
.expect("Timed out waiting for N to be mined and processed");
8647+
8648+
let info_after = signer_test
8649+
.stacks_client
8650+
.get_peer_info()
8651+
.expect("Failed to get peer info");
8652+
assert_eq!(
8653+
info_before.stacks_tip_height + 1,
8654+
info_after.stacks_tip_height
8655+
);
8656+
8657+
// Ensure that the block was accepted globally so the stacks tip has advanced to N
8658+
let nakamoto_blocks = test_observer::get_mined_nakamoto_blocks();
8659+
let block_n = nakamoto_blocks.last().unwrap();
8660+
assert_eq!(info_after.stacks_tip.to_string(), block_n.block_hash);
8661+
8662+
// Make sure that ALL signers accepted the block proposal
8663+
signer_test
8664+
.wait_for_block_acceptance(short_timeout, &block_n.signer_signature_hash, &all_signers)
8665+
.expect("Timed out waiting for block acceptance of N");
8666+
8667+
info!("------------------------- Mine Nakamoto Block N+1 -------------------------");
8668+
// Make less than 30% of the signers reject the block and ensure it is accepted by the node, but not announced.
8669+
let rejecting_signers: Vec<_> = all_signers
8670+
.iter()
8671+
.cloned()
8672+
.take(num_signers * 3 / 10)
8673+
.collect();
8674+
let non_rejecting_signers = all_signers[num_signers * 3 / 10..].to_vec();
8675+
TEST_REJECT_ALL_BLOCK_PROPOSAL
8676+
.lock()
8677+
.unwrap()
8678+
.replace(rejecting_signers.clone());
8679+
TEST_SKIP_BLOCK_ANNOUNCEMENT.lock().unwrap().replace(true);
8680+
TEST_IGNORE_BLOCK_RESPONSES.lock().unwrap().replace(true);
8681+
TEST_IGNORE_SIGNERS.lock().unwrap().replace(true);
8682+
test_observer::clear();
8683+
8684+
// submit a tx so that the miner will mine a stacks block N+1
8685+
let info_before = signer_test
8686+
.stacks_client
8687+
.get_peer_info()
8688+
.expect("Failed to get peer info");
8689+
let transfer_tx = make_stacks_transfer(
8690+
&sender_sk,
8691+
sender_nonce,
8692+
send_fee,
8693+
signer_test.running_nodes.conf.burnchain.chain_id,
8694+
&recipient,
8695+
send_amt,
8696+
);
8697+
let tx = submit_tx(&http_origin, &transfer_tx);
8698+
info!("Submitted tx {tx} in to mine block N+1");
8699+
8700+
let mut proposed_block = None;
8701+
let start_time = Instant::now();
8702+
while proposed_block.is_none() && start_time.elapsed() < Duration::from_secs(30) {
8703+
proposed_block = test_observer::get_stackerdb_chunks()
8704+
.into_iter()
8705+
.flat_map(|chunk| chunk.modified_slots)
8706+
.find_map(|chunk| {
8707+
let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
8708+
.expect("Failed to deserialize SignerMessage");
8709+
match message {
8710+
SignerMessage::BlockProposal(proposal) => {
8711+
if proposal.block.header.consensus_hash
8712+
== info_before.stacks_tip_consensus_hash
8713+
{
8714+
Some(proposal.block)
8715+
} else {
8716+
None
8717+
}
8718+
}
8719+
_ => None,
8720+
}
8721+
});
8722+
}
8723+
let proposed_block = proposed_block.expect("Failed to find proposed block within 30s");
8724+
8725+
signer_test
8726+
.wait_for_block_acceptance(
8727+
short_timeout,
8728+
&proposed_block.header.signer_signature_hash(),
8729+
&non_rejecting_signers,
8730+
)
8731+
.expect("Timed out waiting for block acceptance of N+1 by non rejecting signers");
8732+
8733+
signer_test
8734+
.wait_for_block_rejections(short_timeout, &rejecting_signers)
8735+
.expect("Timed out waiting for block rejection of N+1' from rejecting signers");
8736+
8737+
info!(
8738+
"------------------------- Attempt to Mine Nakamoto Block N+1' -------------------------"
8739+
);
8740+
TEST_REJECT_ALL_BLOCK_PROPOSAL
8741+
.lock()
8742+
.unwrap()
8743+
.replace(Vec::new());
8744+
test_observer::clear();
8745+
8746+
let mut sister_block = proposed_block;
8747+
8748+
let transfer_tx_bytes = make_stacks_transfer(
8749+
&sender_sk,
8750+
sender_nonce,
8751+
send_fee,
8752+
signer_test.running_nodes.conf.burnchain.chain_id,
8753+
&recipient,
8754+
send_amt * 2,
8755+
);
8756+
let tx = StacksTransaction::consensus_deserialize(&mut &transfer_tx_bytes[..]).unwrap();
8757+
let txs = vec![tx];
8758+
let txid_vecs = txs.iter().map(|tx| tx.txid().as_bytes().to_vec()).collect();
8759+
8760+
let merkle_tree = MerkleTree::<Sha512Trunc256Sum>::new(&txid_vecs);
8761+
let tx_merkle_root = merkle_tree.root();
8762+
sister_block.txs = txs;
8763+
sister_block.header.tx_merkle_root = tx_merkle_root;
8764+
sister_block
8765+
.header
8766+
.sign_miner(&signer_test.running_nodes.conf.miner.mining_key.unwrap())
8767+
.unwrap();
8768+
signer_test.propose_block(sister_block.clone(), Duration::from_secs(30));
8769+
8770+
wait_for(30, || {
8771+
let stackerdb_events = test_observer::get_stackerdb_chunks();
8772+
let block_rejections: HashSet<_> = stackerdb_events
8773+
.into_iter()
8774+
.flat_map(|chunk| chunk.modified_slots)
8775+
.filter_map(|chunk| {
8776+
let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice())
8777+
.expect("Failed to deserialize SignerMessage");
8778+
match message {
8779+
SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) => {
8780+
let rejected_pubkey = rejection
8781+
.recover_public_key()
8782+
.expect("Failed to recover public key from rejection");
8783+
// Proves that one of the rejecting signers actually submitted the block for validation as it passed its preliminary checks about chain length
8784+
assert_eq!(
8785+
rejection.reason_code,
8786+
RejectCode::ValidationFailed(ValidateRejectCode::BadBlockHash)
8787+
);
8788+
Some(rejected_pubkey)
8789+
}
8790+
_ => None,
8791+
}
8792+
})
8793+
.collect::<HashSet<_>>();
8794+
Ok(block_rejections.len() == all_signers.len())
8795+
})
8796+
.expect("Timed out waiting for block rejections for N+1'");
8797+
// Assert the block was NOT mined and the tip has not changed.
8798+
let info_after = signer_test
8799+
.stacks_client
8800+
.get_peer_info()
8801+
.expect("Failed to get peer info");
8802+
assert_eq!(
8803+
info_after,
8804+
signer_test
8805+
.stacks_client
8806+
.get_peer_info()
8807+
.expect("Failed to get peer info")
8808+
);
8809+
}

0 commit comments

Comments
 (0)