Skip to content

Commit 1789dcc

Browse files
committed
Add multiple_miners_with_custom_chain_id test to test chain id in the signer in an integration test
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 609ad13 commit 1789dcc

File tree

3 files changed

+284
-1
lines changed

3 files changed

+284
-1
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ jobs:
118118
- tests::signer::v0::mine_2_nakamoto_reward_cycles
119119
- tests::signer::v0::signer_set_rollover
120120
- tests::signer::v0::signing_in_0th_tenure_of_reward_cycle
121+
- tests::signer::v0::multiple_miners_with_custom_chain_id
121122
- tests::nakamoto_integrations::burn_ops_integration_test
122123
- tests::nakamoto_integrations::check_block_heights
123124
- tests::nakamoto_integrations::clarity_burn_state

stacks-signer/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ pub struct GlobalConfig {
156156
/// How much time to wait for a miner to propose a block following a sortition
157157
pub block_proposal_timeout: Duration,
158158
/// An optional custom Chain ID
159-
chain_id: Option<u32>,
159+
pub chain_id: Option<u32>,
160160
}
161161

162162
/// Internal struct for loading up the config file

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

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5170,3 +5170,285 @@ fn signing_in_0th_tenure_of_reward_cycle() {
51705170
}
51715171
assert_eq!(signer_test.get_current_reward_cycle(), next_reward_cycle);
51725172
}
5173+
5174+
/// This test involves two miners with a custom chain id, each mining tenures with 6 blocks each.
5175+
/// Half of the signers are attached to each miner, so the test also verifies that
5176+
/// the signers' messages successfully make their way to the active miner.
5177+
#[test]
5178+
#[ignore]
5179+
fn multiple_miners_with_custom_chain_id() {
5180+
let num_signers = 5;
5181+
let max_nakamoto_tenures = 20;
5182+
let inter_blocks_per_tenure = 5;
5183+
5184+
// setup sender + recipient for a test stx transfer
5185+
let sender_sk = Secp256k1PrivateKey::new();
5186+
let sender_addr = tests::to_addr(&sender_sk);
5187+
let send_amt = 1000;
5188+
let send_fee = 180;
5189+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
5190+
5191+
let btc_miner_1_seed = vec![1, 1, 1, 1];
5192+
let btc_miner_2_seed = vec![2, 2, 2, 2];
5193+
let btc_miner_1_pk = Keychain::default(btc_miner_1_seed.clone()).get_pub_key();
5194+
let btc_miner_2_pk = Keychain::default(btc_miner_2_seed.clone()).get_pub_key();
5195+
5196+
let node_1_rpc = 51024;
5197+
let node_1_p2p = 51023;
5198+
let node_2_rpc = 51026;
5199+
let node_2_p2p = 51025;
5200+
5201+
let localhost = "127.0.0.1";
5202+
let node_1_rpc_bind = format!("{localhost}:{node_1_rpc}");
5203+
let node_2_rpc_bind = format!("{localhost}:{node_2_rpc}");
5204+
let mut node_2_listeners = Vec::new();
5205+
let chain_id = 0x87654321;
5206+
// partition the signer set so that ~half are listening and using node 1 for RPC and events,
5207+
// and the rest are using node 2
5208+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
5209+
num_signers,
5210+
vec![(
5211+
sender_addr.clone(),
5212+
(send_amt + send_fee) * max_nakamoto_tenures * inter_blocks_per_tenure,
5213+
)],
5214+
|signer_config| {
5215+
let node_host = if signer_config.endpoint.port() % 2 == 0 {
5216+
&node_1_rpc_bind
5217+
} else {
5218+
&node_2_rpc_bind
5219+
};
5220+
signer_config.node_host = node_host.to_string();
5221+
signer_config.chain_id = Some(chain_id)
5222+
},
5223+
|config| {
5224+
config.node.rpc_bind = format!("{localhost}:{node_1_rpc}");
5225+
config.node.p2p_bind = format!("{localhost}:{node_1_p2p}");
5226+
config.node.data_url = format!("http://{localhost}:{node_1_rpc}");
5227+
config.node.p2p_address = format!("{localhost}:{node_1_p2p}");
5228+
config.miner.wait_on_interim_blocks = Duration::from_secs(5);
5229+
config.node.pox_sync_sample_secs = 30;
5230+
config.burnchain.chain_id = chain_id;
5231+
5232+
config.node.seed = btc_miner_1_seed.clone();
5233+
config.node.local_peer_seed = btc_miner_1_seed.clone();
5234+
config.burnchain.local_mining_public_key = Some(btc_miner_1_pk.to_hex());
5235+
config.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[1]));
5236+
5237+
config.events_observers.retain(|listener| {
5238+
let Ok(addr) = std::net::SocketAddr::from_str(&listener.endpoint) else {
5239+
warn!(
5240+
"Cannot parse {} to a socket, assuming it isn't a signer-listener binding",
5241+
listener.endpoint
5242+
);
5243+
return true;
5244+
};
5245+
if addr.port() % 2 == 0 || addr.port() == test_observer::EVENT_OBSERVER_PORT {
5246+
return true;
5247+
}
5248+
node_2_listeners.push(listener.clone());
5249+
false
5250+
})
5251+
},
5252+
Some(vec![btc_miner_1_pk.clone(), btc_miner_2_pk.clone()]),
5253+
None,
5254+
);
5255+
let blocks_mined1 = signer_test.running_nodes.nakamoto_blocks_mined.clone();
5256+
5257+
let conf = signer_test.running_nodes.conf.clone();
5258+
let mut conf_node_2 = conf.clone();
5259+
conf_node_2.node.rpc_bind = format!("{localhost}:{node_2_rpc}");
5260+
conf_node_2.node.p2p_bind = format!("{localhost}:{node_2_p2p}");
5261+
conf_node_2.node.data_url = format!("http://{localhost}:{node_2_rpc}");
5262+
conf_node_2.node.p2p_address = format!("{localhost}:{node_2_p2p}");
5263+
conf_node_2.node.seed = btc_miner_2_seed.clone();
5264+
conf_node_2.burnchain.local_mining_public_key = Some(btc_miner_2_pk.to_hex());
5265+
conf_node_2.node.local_peer_seed = btc_miner_2_seed.clone();
5266+
conf_node_2.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[2]));
5267+
conf_node_2.node.miner = true;
5268+
conf_node_2.events_observers.clear();
5269+
conf_node_2.events_observers.extend(node_2_listeners);
5270+
5271+
assert!(!conf_node_2.events_observers.is_empty());
5272+
5273+
let node_1_sk = Secp256k1PrivateKey::from_seed(&conf.node.local_peer_seed);
5274+
let node_1_pk = StacksPublicKey::from_private(&node_1_sk);
5275+
5276+
conf_node_2.node.working_dir = format!("{}-{}", conf_node_2.node.working_dir, "1");
5277+
5278+
conf_node_2.node.set_bootstrap_nodes(
5279+
format!("{}@{}", &node_1_pk.to_hex(), conf.node.p2p_bind),
5280+
conf.burnchain.chain_id,
5281+
conf.burnchain.peer_version,
5282+
);
5283+
5284+
let http_origin = format!("http://{}", &conf.node.rpc_bind);
5285+
5286+
let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap();
5287+
let run_loop_stopper_2 = run_loop_2.get_termination_switch();
5288+
let rl2_coord_channels = run_loop_2.coordinator_channels();
5289+
let Counters {
5290+
naka_submitted_commits: rl2_commits,
5291+
naka_mined_blocks: blocks_mined2,
5292+
..
5293+
} = run_loop_2.counters();
5294+
let run_loop_2_thread = thread::Builder::new()
5295+
.name("run_loop_2".into())
5296+
.spawn(move || run_loop_2.start(None, 0))
5297+
.unwrap();
5298+
5299+
signer_test.boot_to_epoch_3();
5300+
5301+
wait_for(120, || {
5302+
let Some(node_1_info) = get_chain_info_opt(&conf) else {
5303+
return Ok(false);
5304+
};
5305+
let Some(node_2_info) = get_chain_info_opt(&conf_node_2) else {
5306+
return Ok(false);
5307+
};
5308+
Ok(node_1_info.stacks_tip_height == node_2_info.stacks_tip_height)
5309+
})
5310+
.expect("Timed out waiting for follower to catch up to the miner");
5311+
5312+
let pre_nakamoto_peer_1_height = get_chain_info(&conf).stacks_tip_height;
5313+
5314+
info!("------------------------- Reached Epoch 3.0 -------------------------");
5315+
5316+
// due to the random nature of mining sortitions, the way this test is structured
5317+
// is that we keep track of how many tenures each miner produced, and once enough sortitions
5318+
// have been produced such that each miner has produced 3 tenures, we stop and check the
5319+
// results at the end
5320+
let rl1_coord_channels = signer_test.running_nodes.coord_channel.clone();
5321+
let rl1_commits = signer_test.running_nodes.commits_submitted.clone();
5322+
5323+
let miner_1_pk = StacksPublicKey::from_private(conf.miner.mining_key.as_ref().unwrap());
5324+
let miner_2_pk = StacksPublicKey::from_private(conf_node_2.miner.mining_key.as_ref().unwrap());
5325+
let mut btc_blocks_mined = 1;
5326+
let mut miner_1_tenures = 0;
5327+
let mut miner_2_tenures = 0;
5328+
let mut sender_nonce = 0;
5329+
while !(miner_1_tenures >= 3 && miner_2_tenures >= 3) {
5330+
if btc_blocks_mined > max_nakamoto_tenures {
5331+
panic!("Produced {btc_blocks_mined} sortitions, but didn't cover the test scenarios, aborting");
5332+
}
5333+
let blocks_processed_before =
5334+
blocks_mined1.load(Ordering::SeqCst) + blocks_mined2.load(Ordering::SeqCst);
5335+
signer_test.mine_block_wait_on_processing(
5336+
&[&rl1_coord_channels, &rl2_coord_channels],
5337+
&[&rl1_commits, &rl2_commits],
5338+
Duration::from_secs(30),
5339+
);
5340+
btc_blocks_mined += 1;
5341+
5342+
// wait for the new block to be processed
5343+
wait_for(60, || {
5344+
let blocks_processed =
5345+
blocks_mined1.load(Ordering::SeqCst) + blocks_mined2.load(Ordering::SeqCst);
5346+
Ok(blocks_processed > blocks_processed_before)
5347+
})
5348+
.unwrap();
5349+
5350+
info!(
5351+
"Nakamoto blocks mined: {}",
5352+
blocks_mined1.load(Ordering::SeqCst) + blocks_mined2.load(Ordering::SeqCst)
5353+
);
5354+
5355+
// mine the interim blocks
5356+
info!("Mining interim blocks");
5357+
for interim_block_ix in 0..inter_blocks_per_tenure {
5358+
let blocks_processed_before =
5359+
blocks_mined1.load(Ordering::SeqCst) + blocks_mined2.load(Ordering::SeqCst);
5360+
// submit a tx so that the miner will mine an extra block
5361+
let transfer_tx = make_stacks_transfer(
5362+
&sender_sk,
5363+
sender_nonce,
5364+
send_fee,
5365+
signer_test.running_nodes.conf.burnchain.chain_id,
5366+
&recipient,
5367+
send_amt,
5368+
);
5369+
sender_nonce += 1;
5370+
submit_tx(&http_origin, &transfer_tx);
5371+
5372+
wait_for(60, || {
5373+
let blocks_processed =
5374+
blocks_mined1.load(Ordering::SeqCst) + blocks_mined2.load(Ordering::SeqCst);
5375+
Ok(blocks_processed > blocks_processed_before)
5376+
})
5377+
.unwrap();
5378+
info!(
5379+
"Mined interim block {}:{}",
5380+
btc_blocks_mined, interim_block_ix
5381+
);
5382+
}
5383+
5384+
let blocks = get_nakamoto_headers(&conf);
5385+
let mut seen_burn_hashes = HashSet::new();
5386+
miner_1_tenures = 0;
5387+
miner_2_tenures = 0;
5388+
for header in blocks.iter() {
5389+
if seen_burn_hashes.contains(&header.burn_header_hash) {
5390+
continue;
5391+
}
5392+
seen_burn_hashes.insert(header.burn_header_hash.clone());
5393+
5394+
let header = header.anchored_header.as_stacks_nakamoto().unwrap();
5395+
if miner_1_pk
5396+
.verify(
5397+
header.miner_signature_hash().as_bytes(),
5398+
&header.miner_signature,
5399+
)
5400+
.unwrap()
5401+
{
5402+
miner_1_tenures += 1;
5403+
}
5404+
if miner_2_pk
5405+
.verify(
5406+
header.miner_signature_hash().as_bytes(),
5407+
&header.miner_signature,
5408+
)
5409+
.unwrap()
5410+
{
5411+
miner_2_tenures += 1;
5412+
}
5413+
}
5414+
info!(
5415+
"Miner 1 tenures: {}, Miner 2 tenures: {}",
5416+
miner_1_tenures, miner_2_tenures
5417+
);
5418+
}
5419+
5420+
info!(
5421+
"New chain info 1: {:?}",
5422+
get_chain_info(&signer_test.running_nodes.conf)
5423+
);
5424+
5425+
info!("New chain info 2: {:?}", get_chain_info(&conf_node_2));
5426+
5427+
let peer_1_height = get_chain_info(&conf).stacks_tip_height;
5428+
let peer_2_height = get_chain_info(&conf_node_2).stacks_tip_height;
5429+
info!("Peer height information"; "peer_1" => peer_1_height, "peer_2" => peer_2_height, "pre_naka_height" => pre_nakamoto_peer_1_height);
5430+
assert_eq!(peer_1_height, peer_2_height);
5431+
assert_eq!(
5432+
peer_1_height,
5433+
pre_nakamoto_peer_1_height + (btc_blocks_mined - 1) * (inter_blocks_per_tenure + 1)
5434+
);
5435+
assert_eq!(
5436+
btc_blocks_mined,
5437+
u64::try_from(miner_1_tenures + miner_2_tenures).unwrap()
5438+
);
5439+
5440+
// Verify both nodes have the correct chain id
5441+
let miner1_info = get_chain_info(&signer_test.running_nodes.conf);
5442+
assert_eq!(miner1_info.network_id, chain_id);
5443+
5444+
let miner2_info = get_chain_info(&conf_node_2);
5445+
assert_eq!(miner2_info.network_id, chain_id);
5446+
5447+
rl2_coord_channels
5448+
.lock()
5449+
.expect("Mutex poisoned")
5450+
.stop_chains_coordinator();
5451+
run_loop_stopper_2.store(false, Ordering::SeqCst);
5452+
run_loop_2_thread.join().unwrap();
5453+
signer_test.shutdown();
5454+
}

0 commit comments

Comments
 (0)