@@ -5170,3 +5170,285 @@ fn signing_in_0th_tenure_of_reward_cycle() {
5170
5170
}
5171
5171
assert_eq ! ( signer_test. get_current_reward_cycle( ) , next_reward_cycle) ;
5172
5172
}
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