|
13 | 13 | // You should have received a copy of the GNU General Public License
|
14 | 14 | // along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15 | 15 |
|
| 16 | +use std::ops::Add; |
16 | 17 | use std::sync::atomic::Ordering;
|
17 | 18 | use std::time::{Duration, Instant};
|
18 | 19 | use std::{env, thread};
|
@@ -1068,3 +1069,158 @@ fn retry_on_timeout() {
|
1068 | 1069 |
|
1069 | 1070 | signer_test.shutdown();
|
1070 | 1071 | }
|
| 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