diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 722528e0ab..5888864ac8 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -23,15 +23,20 @@ use std::{env, thread}; use clarity::vm::ast::ASTRules; use clarity::vm::costs::ExecutionCost; -use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier}; +use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData}; use clarity::vm::{ClarityName, ClarityVersion, Value}; use http_types::headers::AUTHORIZATION; use lazy_static::lazy_static; use libsigner::v0::messages::SignerMessage as SignerMessageV0; use libsigner::{SignerSession, StackerDBSession}; use rand::RngCore; +use stacks::burnchains::bitcoin::address::{ + BitcoinAddress, LegacyBitcoinAddress, LegacyBitcoinAddressType, +}; +use stacks::burnchains::bitcoin::keys::BitcoinPublicKey; +use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::{MagicBytes, Txid}; -use stacks::chainstate::burn::db::sortdb::SortitionDB; +use stacks::chainstate::burn::db::sortdb::{get_block_commit_by_txid, SortitionDB}; use stacks::chainstate::burn::operations::{ BlockstackOperationType, DelegateStxOp, PreStxOp, StackStxOp, TransferStxOp, VoteForAggregateKeyOp, @@ -71,6 +76,7 @@ use stacks::net::api::getstackers::GetStackersResponse; use stacks::net::api::postblock_proposal::{ BlockValidateReject, BlockValidateResponse, NakamotoBlockProposal, ValidateRejectCode, }; +use stacks::types::{Address, PublicKey}; use stacks::util::hash::hex_bytes; use stacks::util_lib::boot::boot_code_id; use stacks::util_lib::signed_structured_data::pox4::{ @@ -2212,14 +2218,13 @@ fn multiple_miners() { for tenure_ix in 0..tenure_count { info!("Mining tenure {}", tenure_ix); let commits_before = commits_submitted.load(Ordering::SeqCst); + let info_old_tenure = get_chain_info_result(&naka_conf).unwrap(); next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) .unwrap(); - let mut last_tip = BlockHeaderHash([0x00; 32]); - let mut last_tip_height = 0; - // mine the interim blocks for interim_block_ix in 0..inter_blocks_per_tenure { + let info_old_stacks = get_chain_info_result(&naka_conf).unwrap(); let blocks_processed_before = coord_channel .lock() .expect("Mutex poisoned") @@ -2239,12 +2244,26 @@ fn multiple_miners() { }) .unwrap(); - let info = get_chain_info_result(&naka_conf).unwrap(); - assert_ne!(info.stacks_tip, last_tip); - assert_ne!(info.stacks_tip_height, last_tip_height); - - last_tip = info.stacks_tip; - last_tip_height = info.stacks_tip_height; + let info_new_stacks = get_chain_info_result(&naka_conf).unwrap(); + assert_ne!(info_new_stacks.stacks_tip, info_old_stacks.stacks_tip); + assert_eq!( + info_new_stacks.stacks_tip_consensus_hash, + info_old_stacks.stacks_tip_consensus_hash + ); + assert_eq!( + info_new_stacks.stacks_tip_height, + info_old_stacks.stacks_tip_height + 1, + "Stacks block height should increment by 1" + ); + assert_ne!( + info_old_tenure.stacks_tip_consensus_hash, + info_old_stacks.stacks_tip_consensus_hash + ); + assert_eq!( + info_new_stacks.burn_block_height, + info_old_tenure.burn_block_height + 1, + "Tenure block height should increment by 1" + ); } wait_for(20, || { @@ -2253,6 +2272,61 @@ fn multiple_miners() { .unwrap(); } + // check most recently process blocks + // case to check: + // old stacks_tip_consensus_hash != new stacks_tip_consensus_hash and second tx is in the new burn block + let info_node_1_old_block_consensus_hash = get_chain_info(&naka_conf).stacks_tip_consensus_hash; + let info_node_2_old_block_consensus_hash = + get_chain_info(&conf_node_2).stacks_tip_consensus_hash; + + // TODO: submit burn block without waiting for it to be mined + // btc_regtest_controller.build_next_block(1); - flashblock + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + // submit tx 1 + let miner_1_stacks_address = StandardPrincipalData::from(&node_1_sk); + let http_origin = &naka_conf.node.data_url; + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let miner_1_account = get_account(http_origin, &miner_1_stacks_address); + let miner_1_nonce = miner_1_account.nonce; + let transfer_tx = + make_stacks_transfer(&sender_sk, miner_1_nonce, send_fee, &recipient, send_amt); + submit_tx(&http_origin, &transfer_tx); + thread::sleep(Duration::from_secs(15)); + + // submit tx 2 + let transfer_tx = make_stacks_transfer( + &sender_sk, + miner_1_nonce + 1, + send_fee, + &recipient, + send_amt, + ); + submit_tx(&http_origin, &transfer_tx); + + // TODO: wait so the burn block is mined + thread::sleep(Duration::from_secs(15)); + + let info_node_1_second_tx_consensus_hash = get_chain_info(&naka_conf).stacks_tip_consensus_hash; + let info_node_2_second_tx_consensus_hash = + get_chain_info(&conf_node_2).stacks_tip_consensus_hash; + + let info_node_1_new_block_consensus_hash = get_chain_info(&naka_conf).stacks_tip_consensus_hash; + let info_node_2_new_block_consensus_hash = + get_chain_info(&conf_node_2).stacks_tip_consensus_hash; + + assert_eq!( + info_node_1_second_tx_consensus_hash, + info_node_1_new_block_consensus_hash + ); + assert!(info_node_1_old_block_consensus_hash != info_node_1_new_block_consensus_hash); + assert_eq!( + info_node_2_second_tx_consensus_hash, + info_node_2_new_block_consensus_hash + ); + assert!(info_node_2_old_block_consensus_hash != info_node_2_new_block_consensus_hash); + // load the chain tip, and assert that it is a nakamoto block and at least 30 blocks have advanced in epoch 3 let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) .unwrap() @@ -2267,12 +2341,18 @@ fn multiple_miners() { let peer_2_height = get_chain_info(&conf_node_2).stacks_tip_height; info!("Peer height information"; "peer_1" => peer_1_height, "peer_2" => peer_2_height); assert_eq!(peer_1_height, peer_2_height); - + info!( + "Stacks block height {} vs previous formula {}", + tip.stacks_block_height, + block_height_pre_3_0 + ((inter_blocks_per_tenure + 1) * tenure_count) + ); assert!(tip.anchored_header.as_stacks_nakamoto().is_some()); + + // TODO: shouldn't this be + 2 instead of +1 as 2 stx transactions are submitted with time difference, making 2 different stacks blocks for them assert_eq!( tip.stacks_block_height, - block_height_pre_3_0 + ((inter_blocks_per_tenure + 1) * tenure_count), - "Should have mined (1 + interim_blocks_per_tenure) * tenure_count nakamoto blocks" + block_height_pre_3_0 + ((inter_blocks_per_tenure + 1) * tenure_count) + 1, + "Should have mined (1 + interim_blocks_per_tenure) * tenure_count + 1 nakamoto blocks" ); coord_channel diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index 667d91730a..cf24f0f4a4 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -20,15 +20,19 @@ use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; use std::{env, thread}; -use clarity::vm::types::PrincipalData; +use clarity::vm::types::{PrincipalData, StandardPrincipalData}; use clarity::vm::StacksEpoch; use libsigner::v0::messages::{ BlockRejection, BlockResponse, MessageSlotID, MinerSlotID, RejectCode, SignerMessage, }; use libsigner::{BlockProposal, SignerSession, StackerDBSession}; use stacks::address::AddressHashMode; +use stacks::burnchains::bitcoin::address::{ + BitcoinAddress, LegacyBitcoinAddress, LegacyBitcoinAddressType, +}; +use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::Txid; -use stacks::chainstate::burn::db::sortdb::SortitionDB; +use stacks::chainstate::burn::db::sortdb::{self, get_block_commit_by_txid, SortitionDB}; use stacks::chainstate::burn::operations::LeaderBlockCommitOp; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; use stacks::chainstate::stacks::address::PoxAddress; @@ -42,7 +46,7 @@ use stacks::net::api::postblock_proposal::{ValidateRejectCode, TEST_VALIDATE_STA use stacks::net::relay::fault_injection::set_ignore_block; use stacks::types::chainstate::{StacksAddress, StacksBlockId, StacksPrivateKey, StacksPublicKey}; use stacks::types::PublicKey; -use stacks::util::hash::MerkleHashFunc; +use stacks::util::hash::{Hash160, MerkleHashFunc}; use stacks::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey}; use stacks::util_lib::boot::boot_code_id; use stacks::util_lib::signed_structured_data::pox4::{ @@ -1649,6 +1653,623 @@ fn multiple_miners() { signer_test.shutdown(); } +#[test] +#[ignore] +fn new_tenure_stop_fast_blocks() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let num_signers = 5; + let sender_sk = Secp256k1PrivateKey::new(); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + + let btc_miner_1_seed = vec![1, 1, 1, 1]; + let btc_miner_2_seed = vec![2, 2, 2, 2]; + let btc_miner_1_pk = Keychain::default(btc_miner_1_seed.clone()).get_pub_key(); + let btc_miner_2_pk = Keychain::default(btc_miner_2_seed.clone()).get_pub_key(); + + let node_1_rpc = 51024; + let node_1_p2p = 51023; + let node_2_rpc = 51026; + let node_2_p2p = 51025; + + let localhost = "127.0.0.1"; + let node_1_rpc_bind = format!("{localhost}:{node_1_rpc}"); + let node_2_rpc_bind = format!("{localhost}:{node_2_rpc}"); + let mut node_2_listeners = Vec::new(); + + // partition the signer set so that ~half are listening and using node 1 for RPC and events, + // and the rest are using node 2 + + let mut signer_test: SignerTest = SignerTest::new_with_config_modifications( + num_signers, + vec![(sender_addr.clone(), send_amt + send_fee)], + |signer_config| { + let node_host = if signer_config.endpoint.port() % 5 == 0 { + &node_1_rpc_bind + } else { + &node_2_rpc_bind + }; + signer_config.node_host = node_host.to_string(); + }, + |config| { + config.node.rpc_bind = format!("{localhost}:{node_1_rpc}"); + config.node.p2p_bind = format!("{localhost}:{node_1_p2p}"); + config.node.data_url = format!("http://{localhost}:{node_1_rpc}"); + config.node.p2p_address = format!("{localhost}:{node_1_p2p}"); + config.miner.wait_on_interim_blocks = Duration::from_secs(5); + config.node.pox_sync_sample_secs = 5; + + config.node.seed = btc_miner_1_seed.clone(); + config.node.local_peer_seed = btc_miner_1_seed.clone(); + config.burnchain.local_mining_public_key = Some(btc_miner_1_pk.to_hex()); + config.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[1])); + + config.events_observers.retain(|listener| { + let Ok(addr) = std::net::SocketAddr::from_str(&listener.endpoint) else { + warn!( + "Cannot parse {} to a socket, assuming it isn't a signer-listener binding", + listener.endpoint + ); + return true; + }; + if addr.port() % 2 == 0 || addr.port() == test_observer::EVENT_OBSERVER_PORT { + return true; + } + node_2_listeners.push(listener.clone()); + false + }) + }, + Some(vec![btc_miner_1_pk.clone(), btc_miner_2_pk.clone()]), + None, + ); + let conf = signer_test.running_nodes.conf.clone(); + let mut conf_node_2 = conf.clone(); + conf_node_2.node.rpc_bind = format!("{localhost}:{node_2_rpc}"); + conf_node_2.node.p2p_bind = format!("{localhost}:{node_2_p2p}"); + conf_node_2.node.data_url = format!("http://{localhost}:{node_2_rpc}"); + conf_node_2.node.p2p_address = format!("{localhost}:{node_2_p2p}"); + conf_node_2.node.seed = btc_miner_2_seed.clone(); + conf_node_2.burnchain.local_mining_public_key = Some(btc_miner_2_pk.to_hex()); + conf_node_2.node.local_peer_seed = btc_miner_2_seed.clone(); + conf_node_2.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[2])); + conf_node_2.node.miner = true; + conf_node_2.events_observers.clear(); + conf_node_2.events_observers.extend(node_2_listeners); + assert!(!conf_node_2.events_observers.is_empty()); + + let node_1_sk = Secp256k1PrivateKey::from_seed(&conf.node.local_peer_seed); + let node_1_pk = StacksPublicKey::from_private(&node_1_sk); + + conf_node_2.node.working_dir = format!("{}-{}", conf_node_2.node.working_dir, "1"); + + conf_node_2.node.set_bootstrap_nodes( + format!("{}@{}", &node_1_pk.to_hex(), conf.node.p2p_bind), + conf.burnchain.chain_id, + conf.burnchain.peer_version, + ); + + let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap(); + let run_loop_stopper_2 = run_loop_2.get_termination_switch(); + let rl2_coord_channels = run_loop_2.coordinator_channels(); + let Counters { + naka_submitted_commits: rl2_commits, + .. + } = run_loop_2.counters(); + let run_loop_2_thread = thread::Builder::new() + .name("run_loop_2".into()) + .spawn(move || run_loop_2.start(None, 0)) + .unwrap(); + + signer_test.boot_to_epoch_3(); + + wait_for(120, || { + let Some(node_1_info) = get_chain_info_opt(&conf) else { + return Ok(false); + }; + let Some(node_2_info) = get_chain_info_opt(&conf_node_2) else { + return Ok(false); + }; + Ok(node_1_info.stacks_tip_height == node_2_info.stacks_tip_height) + }) + .expect("Timed out waiting for boostrapped node to catch up to the miner"); + + info!("------------------------- Reached Epoch 3.0 -------------------------"); + + let max_nakamoto_tenures = 5; + + // mine tenures until we have miner_b winner of one tenure + // then stop the miner_a + // then try to mine with miner_b and not produce any blocks + + let rl1_coord_channels = signer_test.running_nodes.coord_channel.clone(); + let rl1_commits = signer_test.running_nodes.commits_submitted.clone(); + + let miner_1_stacks_address = StandardPrincipalData::from(&node_1_sk); + let mut btc_blocks_mined = 1; + let mut stopped_miner_2 = false; + let mut miner_1_tries = 0; + + let miner_1_pubkey = conf + .burnchain + .local_mining_public_key + .as_ref() + .map(|pubkey_hex| Secp256k1PublicKey::from_hex(pubkey_hex)) + .expect("Failed to parse 1's public key") + .expect("Invalid A's public key format"); + let miner_2_pubkey = conf_node_2 + .burnchain + .local_mining_public_key + .as_ref() + .map(|pubkey_hex| Secp256k1PublicKey::from_hex(pubkey_hex)) + .expect("Failed to parse miner 2's public key") + .expect("Invalid B's public key format"); + + // Create Bitcoin addresses from public keys + let miner_1_bitcoin_address = BitcoinAddress::from_bytes_legacy( + BitcoinNetworkType::Regtest, + LegacyBitcoinAddressType::PublicKeyHash, + &Hash160::from_data(&miner_1_pubkey.to_bytes()).0, + ) + .expect("Failed to create Bitcoin address for miner 1"); + + let miner_2_bitcoin_address = BitcoinAddress::from_bytes_legacy( + BitcoinNetworkType::Regtest, + LegacyBitcoinAddressType::PublicKeyHash, + &Hash160::from_data(&miner_2_pubkey.to_bytes()).0, + ) + .expect("Failed to create Bitcoin address for miner 2"); + + info!("Miner 1 address: {}", miner_1_bitcoin_address); + info!("Miner 2 address: {}", miner_2_bitcoin_address); + + while !stopped_miner_2 || miner_1_tries < 3 { + assert!( + max_nakamoto_tenures >= btc_blocks_mined, + "Produced {btc_blocks_mined} sortitions, but didn't cover the test scenarios, aborting" + ); + + let info_1 = get_chain_info(&conf); + if !stopped_miner_2 { + let info_2 = get_chain_info(&conf_node_2); + info!( + "Issue next block-build request\ninfo 1: {:?}\ninfo 2: {:?}\n", + &info_1, &info_2 + ); + }; + + info!("Issue next block-build request\ninfo 1: {:?}\n", &info_1); + let info_old_stacks = get_chain_info(&conf); + + if !stopped_miner_2 { + signer_test.mine_block_wait_on_processing( + &[&rl1_coord_channels, &rl2_coord_channels], + &[&rl1_commits, &rl2_commits], + Duration::from_secs(30), + ); + } else { + // create interim block and check that it is not signed and mined + let http_origin = &conf.node.data_url; + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let miner_a_account = get_account(http_origin, &miner_1_stacks_address); + let miner_a_nonce = miner_a_account.nonce; + let transfer_tx = + make_stacks_transfer(&sender_sk, miner_a_nonce, send_fee, &recipient, send_amt); + submit_tx(http_origin, &transfer_tx); + info!("Creating interim block7"); + wait_for(15, || { + let info_new_stacks = get_chain_info(&conf); + info!("Creating interim block8 {}, {}", info_new_stacks.stacks_tip_height, info_old_stacks.stacks_tip_height); + Ok(info_new_stacks.stacks_tip_height <= info_old_stacks.stacks_tip_height) + }).expect("The stacks tip height should not have increased as the signers should not have been able to receive the proposal to sign"); + } + + if stopped_miner_2 { + miner_1_tries += 1; + } else { + btc_blocks_mined += 1; + } + + // Get the latest block from chainstate + let burnchain = conf.get_burnchain(); + let burnchain_header_hash = burnchain + .get_highest_burnchain_block() + .unwrap() + .unwrap() + .block_hash; + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let block_snapshot: stacks::chainstate::burn::BlockSnapshot = + SortitionDB::get_all_snapshots_for_burn_block(sortdb.conn(), &burnchain_header_hash) + .unwrap()[0] + .clone(); + let winning_block_txid: Txid = block_snapshot.winning_block_txid; + let blocks_commits = get_block_commit_by_txid( + sortdb.conn(), + &block_snapshot.sortition_id, + &winning_block_txid, + ) + .unwrap() + .unwrap(); + + let winner_address = blocks_commits.apparent_sender; + let winner_bitcoin_address = BitcoinAddress::Legacy( + LegacyBitcoinAddress::from_b58(winner_address.to_string().as_str()).unwrap(), + ); + + info!("Winner Miner Address: {}", winner_bitcoin_address); + + if !stopped_miner_2 + && winner_bitcoin_address.to_string() == miner_1_bitcoin_address.to_string() + { + info!( + "Miner 1 is the winner for tenure {}. Stopping miner 2", + btc_blocks_mined + ); + rl2_coord_channels + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper_2.store(false, Ordering::SeqCst); + stopped_miner_2 = true; + }; + info!("Miner 1 tries: {}", miner_1_tries); + let blocks = get_nakamoto_headers(&conf); + // for this test, there should be one block per tenure + let consensus_hash_set: HashSet<_> = blocks + .iter() + .map(|header| header.consensus_hash.clone()) + .collect(); + assert_eq!( + consensus_hash_set.len(), + blocks.len(), + "In this test, there should only be one block per tenure" + ); + } + + run_loop_2_thread.join().unwrap(); + signer_test.shutdown(); +} + +// case 2 - stop miner b and continue to mine with miner a. it should continue increasing the number of tenures +// try starting miner b again. it should be able to resync with miner a after some time. make a long wait there and some block mine +#[test] +#[ignore] +fn new_tenure_stop_fast_blocks_case_2() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let num_signers = 5; + let sender_sk = Secp256k1PrivateKey::new(); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + + let btc_miner_1_seed = vec![1, 1, 1, 1]; + let btc_miner_2_seed = vec![2, 2, 2, 2]; + let btc_miner_1_pk = Keychain::default(btc_miner_1_seed.clone()).get_pub_key(); + let btc_miner_2_pk = Keychain::default(btc_miner_2_seed.clone()).get_pub_key(); + + let node_1_rpc = 51024; + let node_1_p2p = 51023; + let node_2_rpc = 51026; + let node_2_p2p = 51025; + + let localhost = "127.0.0.1"; + let node_1_rpc_bind = format!("{localhost}:{node_1_rpc}"); + let node_2_rpc_bind = format!("{localhost}:{node_2_rpc}"); + let mut node_2_listeners = Vec::new(); + + // partition the signer set so that ~half are listening and using node 1 for RPC and events, + // and the rest are using node 2 + + let mut signer_test: SignerTest = SignerTest::new_with_config_modifications( + num_signers, + vec![(sender_addr.clone(), send_amt + send_fee)], + |signer_config| { + let node_host = if signer_config.endpoint.port() % 5 != 0 { + &node_1_rpc_bind + } else { + &node_2_rpc_bind + }; + signer_config.node_host = node_host.to_string(); + }, + |config| { + config.node.rpc_bind = format!("{localhost}:{node_1_rpc}"); + config.node.p2p_bind = format!("{localhost}:{node_1_p2p}"); + config.node.data_url = format!("http://{localhost}:{node_1_rpc}"); + config.node.p2p_address = format!("{localhost}:{node_1_p2p}"); + config.miner.wait_on_interim_blocks = Duration::from_secs(5); + config.node.pox_sync_sample_secs = 5; + + config.node.seed = btc_miner_1_seed.clone(); + config.node.local_peer_seed = btc_miner_1_seed.clone(); + config.burnchain.local_mining_public_key = Some(btc_miner_1_pk.to_hex()); + config.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[1])); + + config.events_observers.retain(|listener| { + let Ok(addr) = std::net::SocketAddr::from_str(&listener.endpoint) else { + warn!( + "Cannot parse {} to a socket, assuming it isn't a signer-listener binding", + listener.endpoint + ); + return true; + }; + if addr.port() % 2 == 0 || addr.port() == test_observer::EVENT_OBSERVER_PORT { + return true; + } + node_2_listeners.push(listener.clone()); + false + }) + }, + Some(vec![btc_miner_1_pk.clone(), btc_miner_2_pk.clone()]), + None, + ); + let conf = signer_test.running_nodes.conf.clone(); + let mut conf_node_2 = conf.clone(); + conf_node_2.node.rpc_bind = format!("{localhost}:{node_2_rpc}"); + conf_node_2.node.p2p_bind = format!("{localhost}:{node_2_p2p}"); + conf_node_2.node.data_url = format!("http://{localhost}:{node_2_rpc}"); + conf_node_2.node.p2p_address = format!("{localhost}:{node_2_p2p}"); + conf_node_2.node.seed = btc_miner_2_seed.clone(); + conf_node_2.burnchain.local_mining_public_key = Some(btc_miner_2_pk.to_hex()); + conf_node_2.node.local_peer_seed = btc_miner_2_seed.clone(); + conf_node_2.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[2])); + conf_node_2.node.miner = true; + conf_node_2.events_observers.clear(); + conf_node_2.events_observers.extend(node_2_listeners); + assert!(!conf_node_2.events_observers.is_empty()); + + let node_1_sk = Secp256k1PrivateKey::from_seed(&conf.node.local_peer_seed); + let node_1_pk = StacksPublicKey::from_private(&node_1_sk); + + conf_node_2.node.working_dir = format!("{}-{}", conf_node_2.node.working_dir, "1"); + + conf_node_2.node.set_bootstrap_nodes( + format!("{}@{}", &node_1_pk.to_hex(), conf.node.p2p_bind), + conf.burnchain.chain_id, + conf.burnchain.peer_version, + ); + + let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap(); + let run_loop_stopper_2 = run_loop_2.get_termination_switch(); + let rl2_coord_channels = run_loop_2.coordinator_channels(); + let Counters { + naka_submitted_commits: rl2_commits, + .. + } = run_loop_2.counters(); + let run_loop_2_thread = thread::Builder::new() + .name("run_loop_2".into()) + .spawn(move || run_loop_2.start(None, 0)) + .unwrap(); + + signer_test.boot_to_epoch_3(); + + wait_for(120, || { + let Some(node_1_info) = get_chain_info_opt(&conf) else { + return Ok(false); + }; + let Some(node_2_info) = get_chain_info_opt(&conf_node_2) else { + return Ok(false); + }; + Ok(node_1_info.stacks_tip_height == node_2_info.stacks_tip_height) + }) + .expect("Timed out waiting for boostrapped node to catch up to the miner"); + + info!("------------------------- Reached Epoch 3.0 -------------------------"); + + let max_nakamoto_tenures = 5; + + // mine tenures until we have miner_b winner of one tenure + // then stop the miner_a + // then try to mine with miner_b and not produce any blocks + + let rl1_coord_channels = signer_test.running_nodes.coord_channel.clone(); + let rl1_commits = signer_test.running_nodes.commits_submitted.clone(); + + let mut btc_blocks_mined = 1; + let mut stopped_miner_2 = false; + + let miner_1_pubkey = conf + .burnchain + .local_mining_public_key + .as_ref() + .map(|pubkey_hex| Secp256k1PublicKey::from_hex(pubkey_hex)) + .expect("Failed to parse miner 1's public key") + .expect("Invalid A's public key format"); + let miner_2_pubkey = conf_node_2 + .burnchain + .local_mining_public_key + .as_ref() + .map(|pubkey_hex| Secp256k1PublicKey::from_hex(pubkey_hex)) + .expect("Failed to parse miner 2's public key") + .expect("Invalid B's public key format"); + + // Create Bitcoin addresses from public keys + let miner_1_bitcoin_address = BitcoinAddress::from_bytes_legacy( + BitcoinNetworkType::Regtest, + LegacyBitcoinAddressType::PublicKeyHash, + &Hash160::from_data(&miner_1_pubkey.to_bytes()).0, + ) + .expect("Failed to create Bitcoin address for miner 1"); + + let miner_2_bitcoin_address = BitcoinAddress::from_bytes_legacy( + BitcoinNetworkType::Regtest, + LegacyBitcoinAddressType::PublicKeyHash, + &Hash160::from_data(&miner_2_pubkey.to_bytes()).0, + ) + .expect("Failed to create Bitcoin address for miner 2"); + + info!("Miner 1 address: {}", miner_1_bitcoin_address); + info!("Miner 2 address: {}", miner_2_bitcoin_address); + + let mut mined_blocks_1_alone = 0; + // TODO: 5 more mined blocks + while !stopped_miner_2 || mined_blocks_1_alone < 5 { + assert!( + max_nakamoto_tenures >= btc_blocks_mined, + "Produced {btc_blocks_mined} sortitions, but didn't cover the test scenarios, aborting" + ); + + let info_1 = get_chain_info(&conf); + if !stopped_miner_2 { + let info_2 = get_chain_info(&conf_node_2); + info!( + "Issue next block-build request\ninfo 1: {:?}\ninfo 2: {:?}\n", + &info_1, &info_2 + ); + }; + + info!("Issue next block-build request\ninfo 1: {:?}\n", &info_1); + let info_old = get_chain_info(&conf); + + // verify advancement of burn chain tip height + if !stopped_miner_2 { + signer_test.mine_block_wait_on_processing( + &[&rl1_coord_channels, &rl2_coord_channels], + &[&rl1_commits, &rl2_commits], + Duration::from_secs(30), + ); + } else { + mined_blocks_1_alone += 1; + + info!("Miner 2 is stopped, mine a block for node 1"); + signer_test.mine_block_wait_on_processing( + &[&rl1_coord_channels, &rl2_coord_channels], + &[&rl1_commits, &rl2_commits], + Duration::from_secs(30), + ); + info!("Miner 2 is stopped, mine a block for node 1 passed"); + // signer_test.mine_nakamoto_block(Duration::from_secs(30)); + } + let info_new = get_chain_info(&conf); + assert_eq!( + info_new.burn_block_height, + info_old.burn_block_height + 1, + "Burn chain tip height should advance" + ); + + if stopped_miner_2 {} + btc_blocks_mined += 1; + + // Get the latest block from chainstate + let burnchain = conf.get_burnchain(); + let burnchain_header_hash = burnchain + .get_highest_burnchain_block() + .unwrap() + .unwrap() + .block_hash; + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let block_snapshot: stacks::chainstate::burn::BlockSnapshot = + SortitionDB::get_all_snapshots_for_burn_block(sortdb.conn(), &burnchain_header_hash) + .unwrap()[0] + .clone(); + let winning_block_txid: Txid = block_snapshot.winning_block_txid; + let blocks_commits = get_block_commit_by_txid( + sortdb.conn(), + &block_snapshot.sortition_id, + &winning_block_txid, + ) + .unwrap() + .unwrap(); + + let winner_address = blocks_commits.apparent_sender; + let winner_bitcoin_address = BitcoinAddress::Legacy( + LegacyBitcoinAddress::from_b58(winner_address.to_string().as_str()).unwrap(), + ); + + info!("Winner Miner Address: {}", winner_bitcoin_address); + + if !stopped_miner_2 + && winner_bitcoin_address.to_string() == miner_1_bitcoin_address.to_string() + { + info!( + "Miner 1 is the winner for tenure {}. Stopping miner 2", + btc_blocks_mined + ); + rl2_coord_channels + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper_2.store(false, Ordering::SeqCst); + stopped_miner_2 = true; + }; + let blocks = get_nakamoto_headers(&conf); + // for this test, there should be one block per tenure + let consensus_hash_set: HashSet<_> = blocks + .iter() + .map(|header| header.consensus_hash.clone()) + .collect(); + assert_eq!( + consensus_hash_set.len(), + blocks.len(), + "In this test, there should only be one block per tenure" + ); + } + // TODO: reopen run_loop_2 + // let run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap(); + // let run_loop_2_thread = thread::Builder::new() + // .name("run_loop_2".into()) + // .spawn(move || run_loop_2.start(None, 0)) + // .unwrap(); + // TODO: wait for node 2 to catch up to node 1 through mining a new block + + // check they are at the same height + let peer_1_height = get_chain_info(&conf).stacks_tip_height; + let peer_2_height = get_chain_info(&conf_node_2).stacks_tip_height; + + assert_eq!(peer_1_height, peer_2_height); + + // check afterwards that node 2 has also got to succesfully mine at least 1 block + + let info_new = get_chain_info(&conf_node_2); + let mut tries = 0; + + loop { + // Get the latest block from chainstate + let burnchain = conf.get_burnchain(); + let burnchain_header_hash = burnchain + .get_highest_burnchain_block() + .unwrap() + .unwrap() + .block_hash; + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let block_snapshot: stacks::chainstate::burn::BlockSnapshot = + SortitionDB::get_all_snapshots_for_burn_block(sortdb.conn(), &burnchain_header_hash) + .unwrap()[0] + .clone(); + let winning_block_txid: Txid = block_snapshot.winning_block_txid; + let blocks_commits = get_block_commit_by_txid( + sortdb.conn(), + &block_snapshot.sortition_id, + &winning_block_txid, + ) + .unwrap() + .unwrap(); + + let winner_address = blocks_commits.apparent_sender; + let winner_bitcoin_address = BitcoinAddress::Legacy( + LegacyBitcoinAddress::from_b58(winner_address.to_string().as_str()).unwrap(), + ); + if winner_bitcoin_address.to_string() == miner_2_bitcoin_address.to_string() { + // TODO: add logic to mine 1 block for node 2 + break; + } else { + // TODO: remove this + tries += 1; + if tries > 7 { + break; + } + } + } + + run_loop_2_thread.join().unwrap(); + signer_test.shutdown(); +} + /// Read processed nakamoto block IDs from the test observer, and use `config` to open /// a chainstate DB and returns their corresponding StacksHeaderInfos fn get_nakamoto_headers(config: &Config) -> Vec {