Skip to content

Commit ba24d00

Browse files
committed
Add an integration test to ensure an empty sortition can time out
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 5b6eae4 commit ba24d00

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ jobs:
8989
- tests::signer::v0::end_of_tenure
9090
- tests::signer::v0::forked_tenure_okay
9191
- tests::signer::v0::forked_tenure_invalid
92+
- tests::signer::v0::empty_sortition
9293
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
9394
- tests::nakamoto_integrations::check_block_heights
9495
- tests::nakamoto_integrations::clarity_burn_state

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// You should have received a copy of the GNU General Public License
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515

16+
use std::ops::Add;
1617
use std::sync::atomic::Ordering;
1718
use std::time::{Duration, Instant};
1819
use std::{env, thread};
@@ -1068,3 +1069,158 @@ fn retry_on_timeout() {
10681069

10691070
signer_test.shutdown();
10701071
}
1072+
1073+
#[test]
1074+
#[ignore]
1075+
/// This test checks the behaviour of signers when a sortition is empty. Specifically:
1076+
/// - An empty sortition will cause the signers to mark a miner as misbehaving once a timeout is exceeded.
1077+
/// - The empty sortition will trigger the miner to attempt a tenure extend.
1078+
/// - Signers will accept the tenure extend and sign subsequent blocks built off the old sortition
1079+
fn empty_sortition() {
1080+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
1081+
return;
1082+
}
1083+
1084+
tracing_subscriber::registry()
1085+
.with(fmt::layer())
1086+
.with(EnvFilter::from_default_env())
1087+
.init();
1088+
1089+
info!("------------------------- Test Setup -------------------------");
1090+
let num_signers = 5;
1091+
let sender_sk = Secp256k1PrivateKey::new();
1092+
let sender_addr = tests::to_addr(&sender_sk);
1093+
let send_amt = 100;
1094+
let send_fee = 180;
1095+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
1096+
let block_proposal_timeout = Duration::from_secs(5);
1097+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
1098+
num_signers,
1099+
vec![(sender_addr.clone(), send_amt + send_fee)],
1100+
Some(Duration::from_secs(5)),
1101+
|config| {
1102+
// make the duration long enough that the miner will be marked as malicious
1103+
config.block_proposal_timeout = block_proposal_timeout;
1104+
},
1105+
);
1106+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
1107+
let short_timeout = Duration::from_secs(20);
1108+
1109+
signer_test.boot_to_epoch_3();
1110+
1111+
TEST_BROADCAST_STALL.lock().unwrap().replace(true);
1112+
1113+
info!("------------------------- Test Mine Regular Tenure A -------------------------");
1114+
let commits_before = signer_test
1115+
.running_nodes
1116+
.commits_submitted
1117+
.load(Ordering::SeqCst);
1118+
// Mine a regular tenure
1119+
next_block_and(
1120+
&mut signer_test.running_nodes.btc_regtest_controller,
1121+
60,
1122+
|| {
1123+
let commits_count = signer_test
1124+
.running_nodes
1125+
.commits_submitted
1126+
.load(Ordering::SeqCst);
1127+
Ok(commits_count > commits_before)
1128+
},
1129+
)
1130+
.unwrap();
1131+
1132+
info!("------------------------- Test Mine Empty Tenure B -------------------------");
1133+
info!("Pausing stacks block mining to trigger an empty sortition.");
1134+
let blocks_before = signer_test
1135+
.running_nodes
1136+
.nakamoto_blocks_mined
1137+
.load(Ordering::SeqCst);
1138+
let commits_before = signer_test
1139+
.running_nodes
1140+
.commits_submitted
1141+
.load(Ordering::SeqCst);
1142+
// Start new Tenure B
1143+
// In the next block, the miner should win the tenure
1144+
next_block_and(
1145+
&mut signer_test.running_nodes.btc_regtest_controller,
1146+
60,
1147+
|| {
1148+
let commits_count = signer_test
1149+
.running_nodes
1150+
.commits_submitted
1151+
.load(Ordering::SeqCst);
1152+
Ok(commits_count > commits_before)
1153+
},
1154+
)
1155+
.unwrap();
1156+
1157+
info!("Pausing stacks block proposal to force an empty tenure");
1158+
TEST_BROADCAST_STALL.lock().unwrap().replace(true);
1159+
1160+
info!("Pausing commit op to prevent tenure C from starting...");
1161+
TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true);
1162+
1163+
let blocks_after = signer_test
1164+
.running_nodes
1165+
.nakamoto_blocks_mined
1166+
.load(Ordering::SeqCst);
1167+
assert_eq!(blocks_after, blocks_before);
1168+
1169+
// submit a tx so that the miner will mine an extra block
1170+
let sender_nonce = 0;
1171+
let transfer_tx =
1172+
make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt);
1173+
submit_tx(&http_origin, &transfer_tx);
1174+
1175+
std::thread::sleep(block_proposal_timeout.add(Duration::from_secs(1)));
1176+
1177+
TEST_BROADCAST_STALL.lock().unwrap().replace(false);
1178+
1179+
info!("------------------------- Test Delayed Block is Rejected -------------------------");
1180+
let reward_cycle = signer_test.get_current_reward_cycle();
1181+
let mut stackerdb = StackerDB::new(
1182+
&signer_test.running_nodes.conf.node.rpc_bind,
1183+
StacksPrivateKey::new(), // We are just reading so don't care what the key is
1184+
false,
1185+
reward_cycle,
1186+
SignerSlotID(0), // We are just reading so again, don't care about index.
1187+
);
1188+
1189+
let signer_slot_ids: Vec<_> = signer_test
1190+
.get_signer_indices(reward_cycle)
1191+
.iter()
1192+
.map(|id| id.0)
1193+
.collect();
1194+
assert_eq!(signer_slot_ids.len(), num_signers);
1195+
1196+
// The miner's proposed block should get rejected by the signers
1197+
let start_polling = Instant::now();
1198+
let mut found_rejection = false;
1199+
while !found_rejection {
1200+
std::thread::sleep(Duration::from_secs(1));
1201+
let messages: Vec<SignerMessage> = StackerDB::get_messages(
1202+
stackerdb
1203+
.get_session_mut(&MessageSlotID::BlockResponse)
1204+
.expect("Failed to get BlockResponse stackerdb session"),
1205+
&signer_slot_ids,
1206+
)
1207+
.expect("Failed to get message from stackerdb");
1208+
for message in messages {
1209+
if let SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection {
1210+
reason_code,
1211+
..
1212+
})) = message
1213+
{
1214+
assert!(matches!(reason_code, RejectCode::SortitionViewMismatch));
1215+
found_rejection = true;
1216+
} else {
1217+
panic!("Unexpected message type");
1218+
}
1219+
}
1220+
assert!(
1221+
start_polling.elapsed() <= short_timeout,
1222+
"Timed out after waiting for response from signer"
1223+
);
1224+
}
1225+
signer_test.shutdown();
1226+
}

0 commit comments

Comments
 (0)