Skip to content

Commit 1cfc225

Browse files
committed
test: add signer set test for a flash block with miner changeover
1 parent 273156e commit 1cfc225

File tree

2 files changed

+299
-3
lines changed

2 files changed

+299
-3
lines changed

testnet/stacks-node/src/tests/neon_integrations.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use stacks::net::api::getaccount::AccountEntryResponse;
5353
use stacks::net::api::getcontractsrc::ContractSrcResponse;
5454
use stacks::net::api::getinfo::RPCPeerInfoData;
5555
use stacks::net::api::getpoxinfo::RPCPoxInfoData;
56+
use stacks::net::api::getsortition::SortitionInfo;
5657
use stacks::net::api::gettransaction_unconfirmed::UnconfirmedTransactionResponse;
5758
use stacks::net::api::postblock::StacksBlockAcceptedData;
5859
use stacks::net::api::postfeerate::RPCFeeEstimateResponse;
@@ -1351,7 +1352,7 @@ pub fn get_account_result<F: std::fmt::Display>(
13511352
let client = reqwest::blocking::Client::new();
13521353
let path = format!("{http_origin}/v2/accounts/{account}?proof=0");
13531354
let res = client.get(&path).send()?.json::<AccountEntryResponse>()?;
1354-
info!("Account response: {res:#?}");
1355+
debug!("Account response: {res:#?}");
13551356
Ok(Account {
13561357
balance: u128::from_str_radix(&res.balance[2..], 16).unwrap(),
13571358
locked: u128::from_str_radix(&res.locked[2..], 16).unwrap(),
@@ -1363,6 +1364,22 @@ pub fn get_account<F: std::fmt::Display>(http_origin: &str, account: &F) -> Acco
13631364
get_account_result(http_origin, account).unwrap()
13641365
}
13651366

1367+
pub fn get_sortition_info(conf: &Config) -> SortitionInfo {
1368+
let client = reqwest::blocking::Client::new();
1369+
let http_origin = format!("http://{}", &conf.node.rpc_bind);
1370+
let path = format!("{http_origin}/v3/sortitions");
1371+
let mut resp: Vec<_> = client.get(&path).send().unwrap().json().unwrap();
1372+
resp.pop().unwrap()
1373+
}
1374+
1375+
pub fn get_sortition_info_ch(conf: &Config, ch: &ConsensusHash) -> SortitionInfo {
1376+
let client = reqwest::blocking::Client::new();
1377+
let http_origin = format!("http://{}", &conf.node.rpc_bind);
1378+
let path = format!("{http_origin}/v3/sortitions/consensus/{ch}");
1379+
let mut resp: Vec<_> = client.get(&path).send().unwrap().json().unwrap();
1380+
resp.pop().unwrap()
1381+
}
1382+
13661383
pub fn get_neighbors(conf: &Config) -> Option<serde_json::Value> {
13671384
let client = reqwest::blocking::Client::new();
13681385
let http_origin = format!("http://{}", &conf.node.rpc_bind);

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

Lines changed: 281 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,7 @@ use crate::tests::nakamoto_integrations::{
8484
POX_4_DEFAULT_STACKER_BALANCE, POX_4_DEFAULT_STACKER_STX_AMT,
8585
};
8686
use crate::tests::neon_integrations::{
87-
get_account, get_chain_info, get_chain_info_opt, next_block_and_wait,
88-
run_until_burnchain_height, submit_tx, submit_tx_fallible, test_observer,
87+
get_account, get_chain_info, get_chain_info_opt, get_sortition_info, get_sortition_info_ch, next_block_and_wait, run_until_burnchain_height, submit_tx, submit_tx_fallible, test_observer
8988
};
9089
use crate::tests::{
9190
self, gen_random_port, make_contract_call, make_contract_publish, make_stacks_transfer,
@@ -11251,3 +11250,283 @@ fn fast_sortition() {
1125111250
info!("------------------------- Shutdown -------------------------");
1125211251
signer_test.shutdown();
1125311252
}
11253+
11254+
#[test]
11255+
#[ignore]
11256+
/// This test spins up two nakamoto nodes, both configured to mine.
11257+
/// After Nakamoto blocks are mined, it waits for a normal tenure, then issues
11258+
/// two bitcoin blocks in quick succession -- the first will contain block commits,
11259+
/// and the second "flash block" will contain no block commits.
11260+
/// The test checks if the winner of the first block is different than the previous tenure.
11261+
/// If so, it performs the actual test: asserting that the miner wakes up and produces valid blocks.
11262+
/// This test uses the burn-block-height to ensure consistent calculation of the burn view between
11263+
/// the miner thread and the block processor
11264+
11265+
fn multiple_miners_empty_sortition() {
11266+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
11267+
return;
11268+
}
11269+
let num_signers = 5;
11270+
let sender_sk = Secp256k1PrivateKey::new();
11271+
let sender_addr = tests::to_addr(&sender_sk);
11272+
let send_fee = 180;
11273+
11274+
let btc_miner_1_seed = vec![1, 1, 1, 1];
11275+
let btc_miner_2_seed = vec![2, 2, 2, 2];
11276+
let btc_miner_1_pk = Keychain::default(btc_miner_1_seed.clone()).get_pub_key();
11277+
let btc_miner_2_pk = Keychain::default(btc_miner_2_seed.clone()).get_pub_key();
11278+
11279+
let node_1_rpc = gen_random_port();
11280+
let node_1_p2p = gen_random_port();
11281+
let node_2_rpc = gen_random_port();
11282+
let node_2_p2p = gen_random_port();
11283+
11284+
let localhost = "127.0.0.1";
11285+
let node_1_rpc_bind = format!("{localhost}:{node_1_rpc}");
11286+
let node_2_rpc_bind = format!("{localhost}:{node_2_rpc}");
11287+
let mut node_2_listeners = Vec::new();
11288+
11289+
let max_nakamoto_tenures = 30;
11290+
// partition the signer set so that ~half are listening and using node 1 for RPC and events,
11291+
// and the rest are using node 2
11292+
11293+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
11294+
num_signers,
11295+
vec![(sender_addr, send_fee * 2 * 60 + 1000)],
11296+
|signer_config| {
11297+
let node_host = if signer_config.endpoint.port() % 2 == 0 {
11298+
&node_1_rpc_bind
11299+
} else {
11300+
&node_2_rpc_bind
11301+
};
11302+
signer_config.node_host = node_host.to_string();
11303+
},
11304+
|config| {
11305+
config.node.rpc_bind = format!("{localhost}:{node_1_rpc}");
11306+
config.node.p2p_bind = format!("{localhost}:{node_1_p2p}");
11307+
config.node.data_url = format!("http://{localhost}:{node_1_rpc}");
11308+
config.node.p2p_address = format!("{localhost}:{node_1_p2p}");
11309+
config.miner.wait_on_interim_blocks = Duration::from_secs(5);
11310+
config.node.pox_sync_sample_secs = 30;
11311+
config.burnchain.pox_reward_length = Some(max_nakamoto_tenures);
11312+
11313+
config.node.seed = btc_miner_1_seed.clone();
11314+
config.node.local_peer_seed = btc_miner_1_seed.clone();
11315+
config.burnchain.local_mining_public_key = Some(btc_miner_1_pk.to_hex());
11316+
config.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[1]));
11317+
11318+
config.events_observers.retain(|listener| {
11319+
let Ok(addr) = std::net::SocketAddr::from_str(&listener.endpoint) else {
11320+
warn!(
11321+
"Cannot parse {} to a socket, assuming it isn't a signer-listener binding",
11322+
listener.endpoint
11323+
);
11324+
return true;
11325+
};
11326+
if addr.port() % 2 == 0 || addr.port() == test_observer::EVENT_OBSERVER_PORT {
11327+
return true;
11328+
}
11329+
node_2_listeners.push(listener.clone());
11330+
false
11331+
})
11332+
},
11333+
Some(vec![btc_miner_1_pk, btc_miner_2_pk]),
11334+
None,
11335+
);
11336+
let conf = signer_test.running_nodes.conf.clone();
11337+
let mut conf_node_2 = conf.clone();
11338+
conf_node_2.node.rpc_bind = format!("{localhost}:{node_2_rpc}");
11339+
conf_node_2.node.p2p_bind = format!("{localhost}:{node_2_p2p}");
11340+
conf_node_2.node.data_url = format!("http://{localhost}:{node_2_rpc}");
11341+
conf_node_2.node.p2p_address = format!("{localhost}:{node_2_p2p}");
11342+
conf_node_2.node.seed = btc_miner_2_seed.clone();
11343+
conf_node_2.burnchain.local_mining_public_key = Some(btc_miner_2_pk.to_hex());
11344+
conf_node_2.node.local_peer_seed = btc_miner_2_seed.clone();
11345+
conf_node_2.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[2]));
11346+
conf_node_2.node.miner = true;
11347+
conf_node_2.events_observers.clear();
11348+
conf_node_2.events_observers.extend(node_2_listeners);
11349+
assert!(!conf_node_2.events_observers.is_empty());
11350+
11351+
let node_1_sk = Secp256k1PrivateKey::from_seed(&conf.node.local_peer_seed);
11352+
let node_1_pk = StacksPublicKey::from_private(&node_1_sk);
11353+
11354+
conf_node_2.node.working_dir = format!("{}-1", conf_node_2.node.working_dir);
11355+
11356+
conf_node_2.node.set_bootstrap_nodes(
11357+
format!("{}@{}", &node_1_pk.to_hex(), conf.node.p2p_bind),
11358+
conf.burnchain.chain_id,
11359+
conf.burnchain.peer_version,
11360+
);
11361+
11362+
let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap();
11363+
let run_loop_stopper_2 = run_loop_2.get_termination_switch();
11364+
let rl2_coord_channels = run_loop_2.coordinator_channels();
11365+
let Counters {
11366+
naka_submitted_commits: rl2_commits,
11367+
..
11368+
} = run_loop_2.counters();
11369+
let run_loop_2_thread = thread::Builder::new()
11370+
.name("run_loop_2".into())
11371+
.spawn(move || run_loop_2.start(None, 0))
11372+
.unwrap();
11373+
11374+
signer_test.boot_to_epoch_3();
11375+
11376+
wait_for(120, || {
11377+
let Some(node_1_info) = get_chain_info_opt(&conf) else {
11378+
return Ok(false);
11379+
};
11380+
let Some(node_2_info) = get_chain_info_opt(&conf_node_2) else {
11381+
return Ok(false);
11382+
};
11383+
Ok(node_1_info.stacks_tip_height == node_2_info.stacks_tip_height)
11384+
})
11385+
.expect("Timed out waiting for boostrapped node to catch up to the miner");
11386+
11387+
let pre_nakamoto_peer_1_height = get_chain_info(&conf).stacks_tip_height;
11388+
11389+
info!("------------------------- Reached Epoch 3.0 -------------------------");
11390+
11391+
let burn_height_contract = "
11392+
(define-data-var local-burn-block-ht uint u0)
11393+
(define-public (run-update)
11394+
(ok (var-set local-burn-block-ht burn-block-height)))
11395+
";
11396+
11397+
let contract_tx = make_contract_publish(
11398+
&sender_sk,
11399+
0,
11400+
1000,
11401+
conf.burnchain.chain_id,
11402+
"burn-height-local",
11403+
burn_height_contract,
11404+
);
11405+
submit_tx(&conf.node.data_url, &contract_tx);
11406+
11407+
let rl1_coord_channels = signer_test.running_nodes.coord_channel.clone();
11408+
let rl1_commits = signer_test.running_nodes.commits_submitted.clone();
11409+
11410+
let last_sender_nonce = loop {
11411+
// Mine 1 nakamoto tenures
11412+
info!("Mining tenure...");
11413+
let rl2_commits_before = rl2_commits.load(Ordering::SeqCst);
11414+
let rl1_commits_before = rl1_commits.load(Ordering::SeqCst);
11415+
11416+
signer_test.mine_block_wait_on_processing(
11417+
&[&rl1_coord_channels, &rl2_coord_channels],
11418+
&[&rl1_commits, &rl2_commits],
11419+
Duration::from_secs(30),
11420+
);
11421+
11422+
// mine the interim blocks
11423+
for _ in 0..2 {
11424+
let sender_nonce = get_account(&conf.node.data_url, &sender_addr).nonce;
11425+
// check if the burn contract is already produced, if not wait for it to be included in
11426+
// an interim block
11427+
if sender_nonce >= 1 {
11428+
let contract_call_tx = make_contract_call(
11429+
&sender_sk,
11430+
sender_nonce,
11431+
send_fee,
11432+
conf.burnchain.chain_id,
11433+
&sender_addr,
11434+
"burn-height-local",
11435+
"run-update",
11436+
&[],
11437+
);
11438+
submit_tx(&conf.node.data_url, &contract_call_tx);
11439+
}
11440+
11441+
// make sure the sender's tx gets included (whether it was the contract publish or call)
11442+
wait_for(60, || {
11443+
let next_sender_nonce = get_account(&conf.node.data_url, &sender_addr).nonce;
11444+
Ok(next_sender_nonce > sender_nonce)
11445+
})
11446+
.unwrap();
11447+
}
11448+
11449+
11450+
let last_active_sortition = get_sortition_info(&conf);
11451+
assert!(last_active_sortition.was_sortition);
11452+
11453+
// lets mine a btc flash block
11454+
let rl2_commits_before = rl2_commits.load(Ordering::SeqCst);
11455+
let rl1_commits_before = rl1_commits.load(Ordering::SeqCst);
11456+
signer_test.running_nodes.btc_regtest_controller.build_next_block(2);
11457+
11458+
wait_for(60, || {
11459+
Ok(rl2_commits.load(Ordering::SeqCst) > rl2_commits_before &&
11460+
rl1_commits.load(Ordering::SeqCst) > rl1_commits_before)
11461+
})
11462+
.unwrap();
11463+
11464+
let cur_empty_sortition = get_sortition_info(&conf);
11465+
assert!(!cur_empty_sortition.was_sortition);
11466+
let inactive_sortition = get_sortition_info_ch(
11467+
&conf,
11468+
cur_empty_sortition.last_sortition_ch.as_ref().unwrap(),
11469+
);
11470+
assert!(inactive_sortition.was_sortition);
11471+
assert_eq!(
11472+
inactive_sortition.burn_block_height,
11473+
last_active_sortition.burn_block_height + 1
11474+
);
11475+
11476+
info!("==================== Mined a flash block ====================");
11477+
info!("Flash block sortition info";
11478+
"last_active_winner" => ?last_active_sortition.miner_pk_hash160,
11479+
"last_winner" => ?inactive_sortition.miner_pk_hash160,
11480+
"last_active_ch" => %last_active_sortition.consensus_hash,
11481+
"last_winner_ch" => %inactive_sortition.consensus_hash,
11482+
"cur_empty_sortition" => %cur_empty_sortition.consensus_hash,
11483+
);
11484+
11485+
if last_active_sortition.miner_pk_hash160 != inactive_sortition.miner_pk_hash160 {
11486+
info!(
11487+
"==================== Mined a flash block with changed miners ===================="
11488+
);
11489+
break get_account(&conf.node.data_url, &sender_addr).nonce;
11490+
}
11491+
};
11492+
11493+
// after the flash block, make sure we get block processing without a new bitcoin block
11494+
// being mined.
11495+
11496+
for _ in 0..2 {
11497+
let sender_nonce = get_account(&conf.node.data_url, &sender_addr).nonce;
11498+
let contract_call_tx = make_contract_call(
11499+
&sender_sk,
11500+
sender_nonce,
11501+
send_fee,
11502+
conf.burnchain.chain_id,
11503+
&sender_addr,
11504+
"burn-height-local",
11505+
"run-update",
11506+
&[],
11507+
);
11508+
submit_tx(&conf.node.data_url, &contract_call_tx);
11509+
11510+
wait_for(60, || {
11511+
let next_sender_nonce = get_account(&conf.node.data_url, &sender_addr).nonce;
11512+
Ok(next_sender_nonce > sender_nonce)
11513+
})
11514+
.unwrap();
11515+
}
11516+
11517+
assert_eq!(
11518+
get_account(&conf.node.data_url, &sender_addr).nonce,
11519+
last_sender_nonce + 2,
11520+
"The last two transactions after the flash block must be included in a block"
11521+
);
11522+
11523+
11524+
rl2_coord_channels
11525+
.lock()
11526+
.expect("Mutex poisoned")
11527+
.stop_chains_coordinator();
11528+
run_loop_stopper_2.store(false, Ordering::SeqCst);
11529+
run_loop_2_thread.join().unwrap();
11530+
signer_test.shutdown();
11531+
11532+
}

0 commit comments

Comments
 (0)