From d96e65def38b92dc7d9b735277fdfcbddce077ae Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 28 May 2024 09:21:21 -0400 Subject: [PATCH 01/14] Add MinerReason to a spawned thread to trigger a tenure extend Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/burn/db/sortdb.rs | 22 +++ .../stacks-node/src/nakamoto_node/miner.rs | 142 ++++++++++++++---- .../stacks-node/src/nakamoto_node/relayer.rs | 90 +++++++++-- 3 files changed, 215 insertions(+), 39 deletions(-) diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index e3802d6ec1..08f78ec6cf 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -2226,6 +2226,28 @@ impl<'a> SortitionHandleConn<'a> { }) } + /// Get the latest block snapshot on this fork where a sortition occured. + pub fn get_last_snapshot_with_sortition_from_tip(&self) -> Result { + let ancestor_hash = + match self.get_indexed(&self.context.chain_tip, &db_keys::last_sortition())? { + Some(hex_str) => BurnchainHeaderHash::from_hex(&hex_str).unwrap_or_else(|_| { + panic!( + "FATAL: corrupt database: failed to parse {} into a hex string", + &hex_str + ) + }), + None => { + // no prior sortitions, so get the first + return self.get_first_block_snapshot(); + } + }; + + self.get_block_snapshot(&ancestor_hash).map(|snapshot_opt| { + snapshot_opt + .unwrap_or_else(|| panic!("FATAL: corrupt index: no snapshot {}", ancestor_hash)) + }) + } + pub fn get_leader_key_at( &self, key_block_height: u64, diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index d6edd79963..d20abe0ea5 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -73,9 +73,13 @@ pub enum MinerDirective { StopTenure, } -struct ParentTenureInfo { - parent_tenure_blocks: u64, - parent_tenure_consensus_hash: ConsensusHash, +#[derive(PartialEq, Debug, Clone)] +/// Tenure info needed to construct a tenure change or tenure extend transaction +pub struct ParentTenureInfo { + /// The number of blocks in the parent tenure + pub parent_tenure_blocks: u64, + /// The consensus hash of the parent tenure + pub parent_tenure_consensus_hash: ConsensusHash, } /// Metadata required for beginning a new tenure @@ -87,6 +91,32 @@ struct ParentStacksBlockInfo { parent_tenure: Option, } +/// The reason the miner thread was spawned +#[derive(PartialEq, Clone, Debug)] +pub enum MinerReason { + /// The miner thread was spawned to begin a new tenure + BlockFound, + /// The miner thread was spawned to extend an existing tenure + Extended { + /// The parent tenure info to extend + parent_tenure_info: ParentTenureInfo, + /// Wether the tenure change transaction was mined + tenure_change_mined: bool, + }, +} + +impl std::fmt::Display for MinerReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MinerReason::BlockFound => write!(f, "BlockFound"), + MinerReason::Extended { parent_tenure_info, tenure_change_mined } => write!( + f, + "Extended: tenure_info = {parent_tenure_info:?}, tenure_change_mined = {tenure_change_mined:?}", + ), + } + } +} + pub struct BlockMinerThread { /// node config struct config: Config, @@ -106,6 +136,8 @@ pub struct BlockMinerThread { parent_tenure_id: StacksBlockId, /// Handle to the node's event dispatcher event_dispatcher: EventDispatcher, + /// The reason the miner thread was spawned + reason: MinerReason, } impl BlockMinerThread { @@ -115,6 +147,7 @@ impl BlockMinerThread { registered_key: RegisteredKey, burn_block: BlockSnapshot, parent_tenure_id: StacksBlockId, + reason: MinerReason, ) -> BlockMinerThread { BlockMinerThread { config: rt.config.clone(), @@ -126,6 +159,7 @@ impl BlockMinerThread { burn_block, event_dispatcher: rt.event_dispatcher.clone(), parent_tenure_id, + reason, } } @@ -146,6 +180,7 @@ impl BlockMinerThread { "had_prior_miner" => prior_miner.is_some(), "parent_tenure_id" => %self.parent_tenure_id, "thread_id" => ?thread::current().id(), + "reason" => %self.reason, ); if let Some(prior_miner) = prior_miner { Self::stop_miner(&self.globals, prior_miner); @@ -207,6 +242,14 @@ impl BlockMinerThread { "consensus_hash" => %new_block.header.consensus_hash, ); self.globals.coord().announce_new_stacks_block(); + if let MinerReason::Extended { + tenure_change_mined, + .. + } = &mut self.reason + { + // We should not issue multiple tenure change transactions for the same tenure + *tenure_change_mined = true; + } } self.globals.counters.bump_naka_mined_blocks(); @@ -500,12 +543,13 @@ impl BlockMinerThread { } fn generate_tenure_change_tx( - &mut self, + &self, nonce: u64, parent_block_id: StacksBlockId, parent_tenure_consensus_hash: ConsensusHash, parent_tenure_blocks: u64, miner_pkh: Hash160, + cause: TenureChangeCause, ) -> Result { let is_mainnet = self.config.is_mainnet(); let chain_id = self.config.burnchain.chain_id; @@ -516,7 +560,7 @@ impl BlockMinerThread { previous_tenure_end: parent_block_id, previous_tenure_blocks: u32::try_from(parent_tenure_blocks) .expect("FATAL: more than u32 blocks in a tenure"), - cause: TenureChangeCause::BlockFound, + cause, pubkey_hash: miner_pkh, }); @@ -541,7 +585,7 @@ impl BlockMinerThread { /// Create a coinbase transaction. fn generate_coinbase_tx( - &mut self, + &self, nonce: u64, epoch_id: StacksEpochId, vrf_proof: VRFProof, @@ -723,28 +767,8 @@ impl BlockMinerThread { } // create our coinbase if this is the first block we've mined this tenure - let tenure_start_info = if let Some(ref par_tenure_info) = parent_block_info.parent_tenure { - let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash(); - let current_miner_nonce = parent_block_info.coinbase_nonce; - let tenure_change_tx = self.generate_tenure_change_tx( - current_miner_nonce, - parent_block_id, - par_tenure_info.parent_tenure_consensus_hash, - par_tenure_info.parent_tenure_blocks, - self.keychain.get_nakamoto_pkh(), - )?; - let coinbase_tx = - self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof); - NakamotoTenureInfo { - coinbase_tx: Some(coinbase_tx), - tenure_change_tx: Some(tenure_change_tx), - } - } else { - NakamotoTenureInfo { - coinbase_tx: None, - tenure_change_tx: None, - } - }; + let tenure_start_info = + self.make_tenure_start_info(&parent_block_info, vrf_proof, target_epoch_id)?; parent_block_info.stacks_parent_header.microblock_tail = None; @@ -816,6 +840,68 @@ impl BlockMinerThread { Ok(block) } + /// Create the tenure start info for the block we're going to build + fn make_tenure_start_info( + &self, + parent_block_info: &ParentStacksBlockInfo, + vrf_proof: VRFProof, + target_epoch_id: StacksEpochId, + ) -> Result { + let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash(); + let current_miner_nonce = parent_block_info.coinbase_nonce; + let (coinbase_tx, tenure_change_tx) = match &self.reason { + MinerReason::BlockFound => { + // create our coinbase if this is the first block we've mined this tenure + if let Some(ref par_tenure_info) = parent_block_info.parent_tenure { + let tenure_change_tx = self.generate_tenure_change_tx( + current_miner_nonce, + parent_block_id, + par_tenure_info.parent_tenure_consensus_hash, + par_tenure_info.parent_tenure_blocks, + self.keychain.get_nakamoto_pkh(), + TenureChangeCause::BlockFound, + )?; + let coinbase_tx = self.generate_coinbase_tx( + current_miner_nonce + 1, + target_epoch_id, + vrf_proof, + ); + (Some(coinbase_tx), Some(tenure_change_tx)) + } else { + (None, None) + } + } + MinerReason::Extended { + parent_tenure_info, + tenure_change_mined, + } => { + if !tenure_change_mined { + let tenure_change_tx = self.generate_tenure_change_tx( + current_miner_nonce, + parent_block_id, + parent_tenure_info.parent_tenure_consensus_hash, + parent_tenure_info.parent_tenure_blocks, + self.keychain.get_nakamoto_pkh(), + TenureChangeCause::Extended, + )?; + let coinbase_tx = self.generate_coinbase_tx( + current_miner_nonce + 1, + target_epoch_id, + vrf_proof, + ); + (Some(coinbase_tx), Some(tenure_change_tx)) + } else { + (None, None) + } + } + }; + + Ok(NakamotoTenureInfo { + coinbase_tx, + tenure_change_tx, + }) + } + /// Check if the tenure needs to change -- if so, return a BurnchainTipChanged error fn check_burn_tip_changed(&self, sortdb: &SortitionDB) -> Result<(), NakamotoNodeError> { let cur_burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index fc4ca1ae0d..be405dda14 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -51,12 +51,13 @@ use stacks_common::util::get_epoch_time_ms; use stacks_common::util::hash::Hash160; use stacks_common::util::vrf::{VRFProof, VRFPublicKey}; +use super::miner::MinerReason; use super::{ BlockCommits, Config, Error as NakamotoNodeError, EventDispatcher, Keychain, BLOCK_PROCESSOR_STACK_SIZE, }; use crate::burnchains::BurnchainController; -use crate::nakamoto_node::miner::{BlockMinerThread, MinerDirective}; +use crate::nakamoto_node::miner::{BlockMinerThread, MinerDirective, ParentTenureInfo}; use crate::neon_node::{ fault_injection_skip_mining, open_chainstate_with_faults, LeaderKeyRegistrationState, }; @@ -162,6 +163,8 @@ pub struct RelayerThread { /// This is the last snapshot in which the relayer committed, and the parent_tenure_id /// which was committed to last_committed: Option<(BlockSnapshot, StacksBlockId)>, + /// The last commit that the relayer submitted which won the sortition + current_mining_commit_tx: Option, } impl RelayerThread { @@ -219,6 +222,7 @@ impl RelayerThread { is_miner, next_initiative: Instant::now() + Duration::from_millis(next_initiative_delay), last_committed: None, + current_mining_commit_tx: None, } } @@ -310,9 +314,7 @@ impl RelayerThread { .expect("FATAL: unknown consensus hash"); self.globals.set_last_sortition(sn.clone()); - let won_sortition = sn.sortition && self.last_commits.remove(&sn.winning_block_txid); - info!( "Relayer: Process sortition"; "sortition_ch" => %consensus_hash, @@ -325,6 +327,7 @@ impl RelayerThread { if won_sortition { increment_stx_blocks_mined_counter(); + self.current_mining_commit_tx = Some(sn.winning_block_txid); } if sn.sortition { @@ -541,6 +544,7 @@ impl RelayerThread { registered_key: RegisteredKey, last_burn_block: BlockSnapshot, parent_tenure_id: StacksBlockId, + reason: MinerReason, ) -> Result { if fault_injection_skip_mining(&self.config.node.rpc_bind, last_burn_block.block_height) { debug!( @@ -570,10 +574,16 @@ impl RelayerThread { "height" => last_burn_block.block_height, "burn_header_hash" => %burn_header_hash, "parent_tenure_id" => %parent_tenure_id, + "reason" => %reason ); - let miner_thread_state = - BlockMinerThread::new(self, registered_key, last_burn_block, parent_tenure_id); + let miner_thread_state = BlockMinerThread::new( + self, + registered_key, + last_burn_block, + parent_tenure_id, + reason, + ); Ok(miner_thread_state) } @@ -581,6 +591,7 @@ impl RelayerThread { &mut self, parent_tenure_start: StacksBlockId, burn_tip: BlockSnapshot, + reason: MinerReason, ) -> Result<(), NakamotoNodeError> { // when starting a new tenure, block the mining thread if its currently running. // the new mining thread will join it (so that the new mining thread stalls, not the relayer) @@ -593,7 +604,8 @@ impl RelayerThread { warn!("Trying to start new tenure, but no VRF key active"); NakamotoNodeError::NoVRFKeyActive })?; - let new_miner_state = self.create_block_miner(vrf_key, burn_tip, parent_tenure_start)?; + let new_miner_state = + self.create_block_miner(vrf_key, burn_tip, parent_tenure_start, reason)?; let new_miner_handle = std::thread::Builder::new() .name(format!("miner.{parent_tenure_start}")) @@ -648,7 +660,11 @@ impl RelayerThread { MinerDirective::BeginTenure { parent_tenure_start, burnchain_tip, - } => match self.start_new_tenure(parent_tenure_start, burnchain_tip) { + } => match self.start_new_tenure( + parent_tenure_start, + burnchain_tip, + MinerReason::BlockFound, + ) { Ok(()) => { debug!("Relayer: successfully started new tenure."); } @@ -656,13 +672,65 @@ impl RelayerThread { error!("Relayer: Failed to start new tenure: {:?}", e); } }, - MinerDirective::ContinueTenure { new_burn_view: _ } => { - // TODO: in this case, we eventually want to undergo a tenure - // change to switch to the new burn view, but right now, we will - // simply end our current tenure if it exists + MinerDirective::ContinueTenure { new_burn_view } => { match self.stop_tenure() { Ok(()) => { debug!("Relayer: successfully stopped tenure."); + // Check if we should undergo a tenure change to switch to the new burn view + let Ok(block_snapshot) = self + .sortdb + .index_handle_at_tip() + .get_last_snapshot_with_sortition_from_tip() + else { + error!("Relayer: failed to get snapshot for current tip"); + return false; + }; + let Ok(Some(block_header)) = + NakamotoChainState::get_block_header_by_consensus_hash( + self.chainstate.db(), + &block_snapshot.consensus_hash, + ) + else { + error!("Relayer: failed to get block header for the last sortition snapshsot"); + return false; + }; + + let Some(current_mining_commit_tx) = self.current_mining_commit_tx else { + error!("Relayer: no current mining commit txid following a ContinueTenure directive. This implies the miner won a sortition without a commit transaction."); + return false; + }; + if block_snapshot.winning_block_txid == current_mining_commit_tx { + let Ok(Some(last_parent_tenure_header)) = + NakamotoChainState::get_nakamoto_tenure_finish_block_header( + self.chainstate.db(), + &block_header.consensus_hash, + ) + else { + warn!("Failed loading last block of parent tenure"; "consensus_hash" => %block_header.consensus_hash); + return false; + }; + let parent_tenure_info = ParentTenureInfo { + parent_tenure_blocks: 1 + last_parent_tenure_header + .stacks_block_height + - block_header.stacks_block_height, + parent_tenure_consensus_hash: new_burn_view, + }; + match self.start_new_tenure( + block_header.index_block_hash(), + block_snapshot, + MinerReason::Extended { + parent_tenure_info, + tenure_change_mined: false, + }, + ) { + Ok(()) => { + debug!("Relayer: successfully started new tenure."); + } + Err(e) => { + error!("Relayer: Failed to start new tenure: {:?}", e); + } + } + } } Err(e) => { error!("Relayer: Failed to stop tenure: {:?}", e); From aa711728f708e4c4f76edd7f110bb017c975b2b2 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 28 May 2024 14:35:38 -0400 Subject: [PATCH 02/14] Move continue tenure logic to seperate function that passes up errors Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/nakamoto_node.rs | 2 + .../stacks-node/src/nakamoto_node/relayer.rs | 131 ++++++++++-------- 2 files changed, 75 insertions(+), 58 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node.rs b/testnet/stacks-node/src/nakamoto_node.rs index 8a1d80de32..a60370f612 100644 --- a/testnet/stacks-node/src/nakamoto_node.rs +++ b/testnet/stacks-node/src/nakamoto_node.rs @@ -103,6 +103,8 @@ pub enum Error { SigningCoordinatorFailure(String), // The thread that we tried to send to has closed ChannelClosed, + /// The block header for the tenure start is missing + MissingTenureStartBlockHeader, } impl StacksNode { diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index be405dda14..e25b0c7aa0 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -647,6 +647,75 @@ impl RelayerThread { Ok(()) } + fn continue_tenure(&mut self, new_burn_view: ConsensusHash) -> Result<(), NakamotoNodeError> { + if let Err(e) = self.stop_tenure() { + error!("Relayer: Failed to stop tenure: {:?}", e); + return Ok(()); + } + debug!("Relayer: successfully stopped tenure."); + // Check if we should undergo a tenure change to switch to the new burn view + let block_snapshot = self + .sortdb + .index_handle_at_tip() + .get_last_snapshot_with_sortition_from_tip() + .map_err(|e| { + error!("Relayer: failed to get last sortition snapshot: {e:?}"); + NakamotoNodeError::SnapshotNotFoundForChainTip + })?; + let Some(block_header) = NakamotoChainState::get_block_header_by_consensus_hash( + self.chainstate.db(), + &block_snapshot.consensus_hash, + ) + .map_err(|e| { + error!("Relayer: failed to get block header for the last sortition snapshsot: {e:?}"); + NakamotoNodeError::MissingTenureStartBlockHeader + })? + else { + error!("Relayer: failed to get block header for the last sortition snapshsot"); + return Err(NakamotoNodeError::MissingTenureStartBlockHeader); + }; + + if Some(block_snapshot.winning_block_txid) != self.current_mining_commit_tx { + debug!("Relayer: the miner did not win the last sortition. No tenure to continue."); + return Ok(()); + }; + + let Some(last_parent_tenure_header) = + NakamotoChainState::get_nakamoto_tenure_finish_block_header( + self.chainstate.db(), + &block_header.consensus_hash, + ) + .map_err(|e| { + error!("Relayer: failed to get last block of parent tenure: {e:?}"); + NakamotoNodeError::ParentNotFound + })? + else { + warn!("Failed loading last block of parent tenure"; "consensus_hash" => %block_header.consensus_hash); + return Err(NakamotoNodeError::ParentNotFound); + }; + let parent_tenure_info = ParentTenureInfo { + parent_tenure_blocks: 1 + last_parent_tenure_header.stacks_block_height + - block_header.stacks_block_height, + parent_tenure_consensus_hash: new_burn_view, + }; + match self.start_new_tenure( + block_header.index_block_hash(), + block_snapshot, + MinerReason::Extended { + parent_tenure_info, + tenure_change_mined: false, + }, + ) { + Ok(()) => { + debug!("Relayer: successfully started new tenure."); + } + Err(e) => { + error!("Relayer: Failed to start new tenure: {:?}", e); + } + } + Ok(()) + } + fn handle_sortition( &mut self, consensus_hash: ConsensusHash, @@ -673,67 +742,13 @@ impl RelayerThread { } }, MinerDirective::ContinueTenure { new_burn_view } => { - match self.stop_tenure() { + match self.continue_tenure(new_burn_view) { Ok(()) => { - debug!("Relayer: successfully stopped tenure."); - // Check if we should undergo a tenure change to switch to the new burn view - let Ok(block_snapshot) = self - .sortdb - .index_handle_at_tip() - .get_last_snapshot_with_sortition_from_tip() - else { - error!("Relayer: failed to get snapshot for current tip"); - return false; - }; - let Ok(Some(block_header)) = - NakamotoChainState::get_block_header_by_consensus_hash( - self.chainstate.db(), - &block_snapshot.consensus_hash, - ) - else { - error!("Relayer: failed to get block header for the last sortition snapshsot"); - return false; - }; - - let Some(current_mining_commit_tx) = self.current_mining_commit_tx else { - error!("Relayer: no current mining commit txid following a ContinueTenure directive. This implies the miner won a sortition without a commit transaction."); - return false; - }; - if block_snapshot.winning_block_txid == current_mining_commit_tx { - let Ok(Some(last_parent_tenure_header)) = - NakamotoChainState::get_nakamoto_tenure_finish_block_header( - self.chainstate.db(), - &block_header.consensus_hash, - ) - else { - warn!("Failed loading last block of parent tenure"; "consensus_hash" => %block_header.consensus_hash); - return false; - }; - let parent_tenure_info = ParentTenureInfo { - parent_tenure_blocks: 1 + last_parent_tenure_header - .stacks_block_height - - block_header.stacks_block_height, - parent_tenure_consensus_hash: new_burn_view, - }; - match self.start_new_tenure( - block_header.index_block_hash(), - block_snapshot, - MinerReason::Extended { - parent_tenure_info, - tenure_change_mined: false, - }, - ) { - Ok(()) => { - debug!("Relayer: successfully started new tenure."); - } - Err(e) => { - error!("Relayer: Failed to start new tenure: {:?}", e); - } - } - } + debug!("Relayer: handled continue tenure."); } Err(e) => { - error!("Relayer: Failed to stop tenure: {:?}", e); + error!("Relayer: Failed to continue tenure: {:?}", e); + return false; } } } From a84819ab1812791b86f1fa0ae4b6563fe26b20eb Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 30 May 2024 13:11:46 -0400 Subject: [PATCH 03/14] WIP: add a test for continue tenure Signed-off-by: Jacinta Ferrant --- .../stacks-node/src/nakamoto_node/relayer.rs | 37 +-- .../src/tests/nakamoto_integrations.rs | 249 ++++++++++++++++++ 2 files changed, 270 insertions(+), 16 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index e25b0c7aa0..2bb83c8db2 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -662,7 +662,16 @@ impl RelayerThread { error!("Relayer: failed to get last sortition snapshot: {e:?}"); NakamotoNodeError::SnapshotNotFoundForChainTip })?; - let Some(block_header) = NakamotoChainState::get_block_header_by_consensus_hash( + + if Some(block_snapshot.winning_block_txid) != self.current_mining_commit_tx { + debug!("Relayer: the miner did not win the last sortition. No tenure to continue."; + "current_mining_commit_tx" => %self.current_mining_commit_tx.unwrap_or(Txid([0u8; 32])), + "block_snapshot_winning_block_txid" => %block_snapshot.winning_block_txid + ); + return Ok(()); + }; + + let block_header = NakamotoChainState::get_block_header_by_consensus_hash( self.chainstate.db(), &block_snapshot.consensus_hash, ) @@ -670,17 +679,12 @@ impl RelayerThread { error!("Relayer: failed to get block header for the last sortition snapshsot: {e:?}"); NakamotoNodeError::MissingTenureStartBlockHeader })? - else { - error!("Relayer: failed to get block header for the last sortition snapshsot"); - return Err(NakamotoNodeError::MissingTenureStartBlockHeader); - }; - - if Some(block_snapshot.winning_block_txid) != self.current_mining_commit_tx { - debug!("Relayer: the miner did not win the last sortition. No tenure to continue."); - return Ok(()); - }; + .ok_or_else(|| { + error!("Relayer: failed to find block header for the last sortition snapshsot"); + NakamotoNodeError::MissingTenureStartBlockHeader + })?; - let Some(last_parent_tenure_header) = + let last_parent_tenure_header = NakamotoChainState::get_nakamoto_tenure_finish_block_header( self.chainstate.db(), &block_header.consensus_hash, @@ -689,10 +693,11 @@ impl RelayerThread { error!("Relayer: failed to get last block of parent tenure: {e:?}"); NakamotoNodeError::ParentNotFound })? - else { - warn!("Failed loading last block of parent tenure"; "consensus_hash" => %block_header.consensus_hash); - return Err(NakamotoNodeError::ParentNotFound); - }; + .ok_or_else(|| { + error!("Relayer: failed to find block header for parent tenure"); + NakamotoNodeError::ParentNotFound + })?; + let parent_tenure_info = ParentTenureInfo { parent_tenure_blocks: 1 + last_parent_tenure_header.stacks_block_height - block_header.stacks_block_height, @@ -744,7 +749,7 @@ impl RelayerThread { MinerDirective::ContinueTenure { new_burn_view } => { match self.continue_tenure(new_burn_view) { Ok(()) => { - debug!("Relayer: handled continue tenure."); + debug!("Relayer: successfully handled continue tenure."); } Err(e) => { error!("Relayer: Failed to continue tenure: {:?}", e); diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 55eb6753bf..d1d9f5ebcb 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -3895,3 +3895,252 @@ fn check_block_heights() { run_loop_thread.join().unwrap(); } + +#[test] +#[ignore] +/// This test spins up a nakamoto-neon node. +/// It starts in Epoch 2.0, mines with `neon_node` to Epoch 3.0, and then switches +/// to Nakamoto operation (activating pox-4 by submitting a stack-stx tx). The BootLoop +/// struct handles the epoch-2/3 tear-down and spin-up. +/// This test makes three assertions: +/// * 30 blocks are mined after 3.0 starts. This is enough to mine across 2 reward cycles +/// * A transaction submitted to the mempool in 3.0 will be mined in 3.0 +/// * The final chain tip is a nakamoto block +fn continue_tenure_extend() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let prom_bind = format!("{}:{}", "127.0.0.1", 6000); + naka_conf.node.prometheus_bind = Some(prom_bind.clone()); + naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000); + let sender_sk = Secp256k1PrivateKey::new(); + // setup sender + recipient for a test stx transfer + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 1000; + let send_fee = 100; + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + send_amt * 2 + send_fee, + ); + let sender_signer_sk = Secp256k1PrivateKey::new(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + let observer_port = test_observer::EVENT_OBSERVER_PORT; + naka_conf.events_observers.insert(EventObserverConfig { + endpoint: format!("localhost:{observer_port}"), + events_keys: vec![EventKeyType::AnyEvent], + }); + + let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone()); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, + naka_submitted_vrfs: vrfs_submitted, + naka_submitted_commits: commits_submitted, + naka_proposed_blocks: proposals_submitted, + .. + } = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::spawn(move || run_loop.start(None, 0)); + wait_for_runloop(&blocks_processed); + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk], + &[sender_signer_sk], + Some(&signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + let burnchain = naka_conf.get_burnchain(); + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let (mut chainstate, _) = StacksChainState::open( + naka_conf.is_mainnet(), + naka_conf.burnchain.chain_id, + &naka_conf.get_chainstate_path_str(), + None, + ) + .unwrap(); + + let block_height_pre_3_0 = + NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap() + .stacks_block_height; + + // query for prometheus metrics + #[cfg(feature = "monitoring_prom")] + { + let prom_http_origin = format!("http://{}", prom_bind); + let client = reqwest::blocking::Client::new(); + let res = client + .get(&prom_http_origin) + .send() + .unwrap() + .text() + .unwrap(); + let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}"); + assert!(res.contains(&expected_result)); + } + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, proposals_submitted); + + // first block wakes up the run loop, wait until a key registration has been submitted. + next_block_and(&mut btc_regtest_controller, 60, || { + let vrf_count = vrfs_submitted.load(Ordering::SeqCst); + Ok(vrf_count >= 1) + }) + .unwrap(); + + // second block should confirm the VRF register, wait until a block commit is submitted + next_block_and(&mut btc_regtest_controller, 60, || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + Ok(commits_count >= 1) + }) + .unwrap(); + + // Mine a regular nakamoto tenures + next_block_and_mine_commit( + &mut btc_regtest_controller, + 60, + &coord_channel, + &commits_submitted, + ) + .unwrap(); + + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); + + TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true); + + next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap(); + + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); + + TEST_SKIP_COMMIT_OP.lock().unwrap().replace(false); + // Submit a TX + let transfer_tx = make_stacks_transfer(&sender_sk, 0, send_fee, &recipient, send_amt); + let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx)); + + let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + + let mut mempool = naka_conf + .connect_mempool_db() + .expect("Database failure opening mempool"); + + mempool + .submit_raw( + &mut chainstate, + &sortdb, + &tip.consensus_hash, + &tip.anchored_header.block_hash(), + transfer_tx.clone(), + &ExecutionCost::max_value(), + &StacksEpochId::Epoch30, + ) + .unwrap(); + // Mine 15 more nakamoto tenures + for _i in 0..15 { + next_block_and_mine_commit( + &mut btc_regtest_controller, + 60, + &coord_channel, + &commits_submitted, + ) + .unwrap(); + + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); + } + + // 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() + .unwrap(); + info!( + "Latest tip"; + "height" => tip.stacks_block_height, + "is_nakamoto" => tip.anchored_header.as_stacks_nakamoto().is_some(), + ); + + // assert that the transfer tx was observed + let transfer_tx_included = test_observer::get_blocks() + .into_iter() + .find(|block_json| { + block_json["transactions"] + .as_array() + .unwrap() + .iter() + .find(|tx_json| tx_json["raw_tx"].as_str() == Some(&transfer_tx_hex)) + .is_some() + }) + .is_some(); + + assert!( + transfer_tx_included, + "Nakamoto node failed to include the transfer tx" + ); + + assert!(tip.anchored_header.as_stacks_nakamoto().is_some()); + assert!(tip.stacks_block_height >= block_height_pre_3_0 + 30); + + // make sure prometheus returns an updated height + #[cfg(feature = "monitoring_prom")] + { + let prom_http_origin = format!("http://{}", prom_bind); + let client = reqwest::blocking::Client::new(); + let res = client + .get(&prom_http_origin) + .send() + .unwrap() + .text() + .unwrap(); + let expected_result = format!("stacks_node_stacks_tip_height {}", tip.stacks_block_height); + assert!(res.contains(&expected_result)); + } + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} From 73eacaed8532bb846d15fd1db0ac864bd65f5b0b Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 4 Jun 2024 12:14:20 -0400 Subject: [PATCH 04/14] WIP: no idea what I am doing Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/nakamoto/mod.rs | 2 +- .../stacks-node/src/nakamoto_node/miner.rs | 115 ++++++++---------- .../stacks-node/src/nakamoto_node/relayer.rs | 76 ++++-------- .../src/tests/nakamoto_integrations.rs | 54 ++++++-- 4 files changed, 122 insertions(+), 125 deletions(-) diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 2cdf93eef5..302ac5a6e6 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -732,7 +732,7 @@ impl NakamotoBlock { // discontinuous warn!( "Invalid block -- discontiguous"; - "previosu_tenure_end" => %tc_payload.previous_tenure_end, + "previous_tenure_end" => %tc_payload.previous_tenure_end, "parent_block_id" => %self.header.parent_block_id ); return Err(()); diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index d20abe0ea5..9f424a7379 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -38,7 +38,6 @@ use stacks::net::stackerdb::StackerDBs; use stacks_common::codec::read_next; use stacks_common::types::chainstate::{StacksAddress, StacksBlockId}; use stacks_common::types::{PrivateKey, StacksEpochId}; -use stacks_common::util::hash::Hash160; use stacks_common::util::vrf::VRFProof; use wsts::curve::point::Point; use wsts::curve::scalar::Scalar; @@ -68,18 +67,21 @@ pub enum MinerDirective { burnchain_tip: BlockSnapshot, }, /// The miner should try to continue their tenure if they are the active miner - ContinueTenure { new_burn_view: ConsensusHash }, + ContinueTenure { + parent_tenure_start: StacksBlockId, + new_burn_view: ConsensusHash, + }, /// The miner did not win sortition StopTenure, } #[derive(PartialEq, Debug, Clone)] /// Tenure info needed to construct a tenure change or tenure extend transaction -pub struct ParentTenureInfo { +struct ParentTenureInfo { /// The number of blocks in the parent tenure - pub parent_tenure_blocks: u64, + parent_tenure_blocks: u64, /// The consensus hash of the parent tenure - pub parent_tenure_consensus_hash: ConsensusHash, + parent_tenure_consensus_hash: ConsensusHash, } /// Metadata required for beginning a new tenure @@ -98,8 +100,9 @@ pub enum MinerReason { BlockFound, /// The miner thread was spawned to extend an existing tenure Extended { - /// The parent tenure info to extend - parent_tenure_info: ParentTenureInfo, + /// Current consensus hash on the underlying burnchain. Corresponds to the last-seen + /// sortition. + burn_view_consensus_hash: ConsensusHash, /// Wether the tenure change transaction was mined tenure_change_mined: bool, }, @@ -109,9 +112,9 @@ impl std::fmt::Display for MinerReason { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MinerReason::BlockFound => write!(f, "BlockFound"), - MinerReason::Extended { parent_tenure_info, tenure_change_mined } => write!( + MinerReason::Extended { burn_view_consensus_hash, tenure_change_mined } => write!( f, - "Extended: tenure_info = {parent_tenure_info:?}, tenure_change_mined = {tenure_change_mined:?}", + "Extended: burn_view_consensus_hash = {burn_view_consensus_hash:?}, tenure_change_mined = {tenure_change_mined}", ), } } @@ -248,7 +251,10 @@ impl BlockMinerThread { } = &mut self.reason { // We should not issue multiple tenure change transactions for the same tenure - *tenure_change_mined = true; + if !*tenure_change_mined { + debug!("Miner: Tenure change mined"); + *tenure_change_mined = true; + } } } @@ -545,24 +551,11 @@ impl BlockMinerThread { fn generate_tenure_change_tx( &self, nonce: u64, - parent_block_id: StacksBlockId, - parent_tenure_consensus_hash: ConsensusHash, - parent_tenure_blocks: u64, - miner_pkh: Hash160, - cause: TenureChangeCause, + payload: TenureChangePayload, ) -> Result { let is_mainnet = self.config.is_mainnet(); let chain_id = self.config.burnchain.chain_id; - let tenure_change_tx_payload = TransactionPayload::TenureChange(TenureChangePayload { - tenure_consensus_hash: self.burn_block.consensus_hash.clone(), - prev_tenure_consensus_hash: parent_tenure_consensus_hash, - burn_view_consensus_hash: self.burn_block.consensus_hash.clone(), - previous_tenure_end: parent_block_id, - previous_tenure_blocks: u32::try_from(parent_tenure_blocks) - .expect("FATAL: more than u32 blocks in a tenure"), - cause, - pubkey_hash: miner_pkh, - }); + let tenure_change_tx_payload = TransactionPayload::TenureChange(payload); let mut tx_auth = self.keychain.get_transaction_auth().unwrap(); tx_auth.set_origin_nonce(nonce); @@ -847,53 +840,45 @@ impl BlockMinerThread { vrf_proof: VRFProof, target_epoch_id: StacksEpochId, ) -> Result { + debug!("MAKING TENURE START INFO"); let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash(); let current_miner_nonce = parent_block_info.coinbase_nonce; - let (coinbase_tx, tenure_change_tx) = match &self.reason { - MinerReason::BlockFound => { - // create our coinbase if this is the first block we've mined this tenure - if let Some(ref par_tenure_info) = parent_block_info.parent_tenure { - let tenure_change_tx = self.generate_tenure_change_tx( - current_miner_nonce, - parent_block_id, - par_tenure_info.parent_tenure_consensus_hash, - par_tenure_info.parent_tenure_blocks, - self.keychain.get_nakamoto_pkh(), - TenureChangeCause::BlockFound, - )?; - let coinbase_tx = self.generate_coinbase_tx( - current_miner_nonce + 1, - target_epoch_id, - vrf_proof, - ); - (Some(coinbase_tx), Some(tenure_change_tx)) - } else { - (None, None) - } - } - MinerReason::Extended { - parent_tenure_info, + let (tenure_change_tx, coinbase_tx) = if let Some(ref parent_tenure_info) = + parent_block_info.parent_tenure + { + debug!("Miner: Constructing tenure change and coinbase transactions"); + let num_blocks_so_far = u32::try_from(parent_tenure_info.parent_tenure_blocks) + .expect("FATAL: more than u32 blocks in a tenure"); + let mut payload = TenureChangePayload { + tenure_consensus_hash: self.burn_block.consensus_hash.clone(), + prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash, + burn_view_consensus_hash: self.burn_block.consensus_hash.clone(), + previous_tenure_end: parent_block_id, + previous_tenure_blocks: num_blocks_so_far, + cause: TenureChangeCause::BlockFound, + pubkey_hash: self.keychain.get_nakamoto_pkh(), + }; + if let MinerReason::Extended { + burn_view_consensus_hash, tenure_change_mined, - } => { - if !tenure_change_mined { - let tenure_change_tx = self.generate_tenure_change_tx( - current_miner_nonce, + } = &self.reason + { + debug!("Tenure change mined {tenure_change_mined}"); + if !*tenure_change_mined { + debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far); + payload = payload.extend( + *burn_view_consensus_hash, parent_block_id, - parent_tenure_info.parent_tenure_consensus_hash, - parent_tenure_info.parent_tenure_blocks, - self.keychain.get_nakamoto_pkh(), - TenureChangeCause::Extended, - )?; - let coinbase_tx = self.generate_coinbase_tx( - current_miner_nonce + 1, - target_epoch_id, - vrf_proof, + num_blocks_so_far, ); - (Some(coinbase_tx), Some(tenure_change_tx)) - } else { - (None, None) } } + let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; + let coinbase_tx = + self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof); + (Some(tenure_change_tx), Some(coinbase_tx)) + } else { + (None, None) }; Ok(NakamotoTenureInfo { diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index 2bb83c8db2..c2a86e0942 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -57,7 +57,7 @@ use super::{ BLOCK_PROCESSOR_STACK_SIZE, }; use crate::burnchains::BurnchainController; -use crate::nakamoto_node::miner::{BlockMinerThread, MinerDirective, ParentTenureInfo}; +use crate::nakamoto_node::miner::{BlockMinerThread, MinerDirective}; use crate::neon_node::{ fault_injection_skip_mining, open_chainstate_with_faults, LeaderKeyRegistrationState, }; @@ -302,7 +302,7 @@ impl RelayerThread { /// Given the pointer to a recently processed sortition, see if we won the sortition. /// - /// Returns `true` if we won this last sortition. + /// Returns a directive to the relayer thread to either start, stop, or continue a tenure. pub fn process_sortition( &mut self, consensus_hash: ConsensusHash, @@ -341,6 +341,7 @@ impl RelayerThread { } } else { MinerDirective::ContinueTenure { + parent_tenure_start: committed_index_hash, new_burn_view: consensus_hash, } } @@ -546,6 +547,9 @@ impl RelayerThread { parent_tenure_id: StacksBlockId, reason: MinerReason, ) -> Result { + debug!("Relayer: creating block miner thread"; + "reason" => %reason + ); if fault_injection_skip_mining(&self.config.node.rpc_bind, last_burn_block.block_height) { debug!( "Relayer: fault injection skip mining at block height {}", @@ -560,7 +564,7 @@ impl RelayerThread { let burn_chain_tip = burn_chain_sn.burn_header_hash.clone(); - if burn_chain_tip != burn_header_hash { + if burn_chain_tip != burn_header_hash && matches!(reason, MinerReason::BlockFound) { debug!( "Relayer: Drop stale RunTenure for {}: current sortition is for {}", &burn_header_hash, &burn_chain_tip @@ -647,7 +651,11 @@ impl RelayerThread { Ok(()) } - fn continue_tenure(&mut self, new_burn_view: ConsensusHash) -> Result<(), NakamotoNodeError> { + fn continue_tenure( + &mut self, + parent_tenure_start: StacksBlockId, + new_burn_view: ConsensusHash, + ) -> Result<(), NakamotoNodeError> { if let Err(e) = self.stop_tenure() { error!("Relayer: Failed to stop tenure: {:?}", e); return Ok(()); @@ -669,45 +677,14 @@ impl RelayerThread { "block_snapshot_winning_block_txid" => %block_snapshot.winning_block_txid ); return Ok(()); - }; - - let block_header = NakamotoChainState::get_block_header_by_consensus_hash( - self.chainstate.db(), - &block_snapshot.consensus_hash, - ) - .map_err(|e| { - error!("Relayer: failed to get block header for the last sortition snapshsot: {e:?}"); - NakamotoNodeError::MissingTenureStartBlockHeader - })? - .ok_or_else(|| { - error!("Relayer: failed to find block header for the last sortition snapshsot"); - NakamotoNodeError::MissingTenureStartBlockHeader - })?; - - let last_parent_tenure_header = - NakamotoChainState::get_nakamoto_tenure_finish_block_header( - self.chainstate.db(), - &block_header.consensus_hash, - ) - .map_err(|e| { - error!("Relayer: failed to get last block of parent tenure: {e:?}"); - NakamotoNodeError::ParentNotFound - })? - .ok_or_else(|| { - error!("Relayer: failed to find block header for parent tenure"); - NakamotoNodeError::ParentNotFound - })?; - - let parent_tenure_info = ParentTenureInfo { - parent_tenure_blocks: 1 + last_parent_tenure_header.stacks_block_height - - block_header.stacks_block_height, - parent_tenure_consensus_hash: new_burn_view, + } else { + debug!("Relayer: the miner won the last sortition. Continuing tenure."); }; match self.start_new_tenure( - block_header.index_block_hash(), + parent_tenure_start, block_snapshot, MinerReason::Extended { - parent_tenure_info, + burn_view_consensus_hash: new_burn_view, tenure_change_mined: false, }, ) { @@ -746,17 +723,18 @@ impl RelayerThread { error!("Relayer: Failed to start new tenure: {:?}", e); } }, - MinerDirective::ContinueTenure { new_burn_view } => { - match self.continue_tenure(new_burn_view) { - Ok(()) => { - debug!("Relayer: successfully handled continue tenure."); - } - Err(e) => { - error!("Relayer: Failed to continue tenure: {:?}", e); - return false; - } + MinerDirective::ContinueTenure { + new_burn_view, + parent_tenure_start, + } => match self.continue_tenure(parent_tenure_start, new_burn_view) { + Ok(()) => { + debug!("Relayer: successfully handled continue tenure."); } - } + Err(e) => { + error!("Relayer: Failed to continue tenure: {:?}", e); + return false; + } + }, MinerDirective::StopTenure => match self.stop_tenure() { Ok(()) => { debug!("Relayer: successfully stopped tenure."); diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index d1d9f5ebcb..cf615d6391 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -4021,7 +4021,7 @@ fn continue_tenure_extend() { }) .unwrap(); - // Mine a regular nakamoto tenures + // Mine a regular nakamoto tenure next_block_and_mine_commit( &mut btc_regtest_controller, 60, @@ -4037,9 +4037,11 @@ fn continue_tenure_extend() { &signers, ); + info!("Pausing commit op for the next block"); TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true); - next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap(); + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); signer_vote_if_needed( &btc_regtest_controller, @@ -4048,7 +4050,6 @@ fn continue_tenure_extend() { &signers, ); - TEST_SKIP_COMMIT_OP.lock().unwrap().replace(false); // Submit a TX let transfer_tx = make_stacks_transfer(&sender_sk, 0, send_fee, &recipient, send_amt); let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx)); @@ -4072,14 +4073,47 @@ fn continue_tenure_extend() { &StacksEpochId::Epoch30, ) .unwrap(); - // Mine 15 more nakamoto tenures + + debug!("MINING A STACKS BLOCK"); + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); + + debug!("MINING THE NEXT BLOCK"); + next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap(); + + signer_vote_if_needed( + &btc_regtest_controller, + &naka_conf, + &[sender_signer_sk], + &signers, + ); + + debug!("Unpausing commit op"); + TEST_SKIP_COMMIT_OP.lock().unwrap().replace(false); + + debug!("MINING THE NEXT TENURES"); + // Mine 15 more regular nakamoto tenures for _i in 0..15 { - next_block_and_mine_commit( - &mut btc_regtest_controller, - 60, - &coord_channel, - &commits_submitted, - ) + let commits_before = commits_submitted.load(Ordering::SeqCst); + let blocks_processed_before = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + next_block_and(&mut btc_regtest_controller, 60, || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + let blocks_processed = coord_channel + .lock() + .expect("Mutex poisoned") + .get_stacks_blocks_processed(); + Ok(commits_count > commits_before && blocks_processed > blocks_processed_before) + }) .unwrap(); signer_vote_if_needed( From 2f0d7cd08297a535bee8826adfeeda7407e32f6b Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 4 Jun 2024 14:17:42 -0400 Subject: [PATCH 05/14] WIP: burnchain tip has changed Signed-off-by: Jacinta Ferrant --- .../stacks-node/src/nakamoto_node/miner.rs | 20 ++++++++++--------- .../src/tests/nakamoto_integrations.rs | 3 +-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 9f424a7379..09bab45751 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -745,19 +745,20 @@ impl BlockMinerThread { .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)? .expect("FATAL: no epoch defined") .epoch_id; + debug!("HERE WE GO"); let mut parent_block_info = self.load_block_parent_info(&mut burn_db, &mut chain_state)?; let vrf_proof = self .make_vrf_proof() .ok_or_else(|| NakamotoNodeError::BadVrfConstruction)?; - if self.mined_blocks.is_empty() { - if parent_block_info.parent_tenure.is_none() { - warn!( - "Miner should be starting a new tenure, but failed to load parent tenure info" - ); - return Err(NakamotoNodeError::ParentNotFound); - } - } + if self.mined_blocks.is_empty() && parent_block_info.parent_tenure.is_none() { + warn!( + "Miner should be starting a new tenure, but failed to load parent tenure info" + ); + return Err(NakamotoNodeError::ParentNotFound); + }; + + debug!("Parent block info parent tenure: {:?} and {:?} mined blocks", parent_block_info.parent_tenure, self.mined_blocks.len()); // create our coinbase if this is the first block we've mined this tenure let tenure_start_info = @@ -846,7 +847,7 @@ impl BlockMinerThread { let (tenure_change_tx, coinbase_tx) = if let Some(ref parent_tenure_info) = parent_block_info.parent_tenure { - debug!("Miner: Constructing tenure change and coinbase transactions"); + debug!("Miner: Constructing tenure change and coinbase transactions: {}", self.reason); let num_blocks_so_far = u32::try_from(parent_tenure_info.parent_tenure_blocks) .expect("FATAL: more than u32 blocks in a tenure"); let mut payload = TenureChangePayload { @@ -878,6 +879,7 @@ impl BlockMinerThread { self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof); (Some(tenure_change_tx), Some(coinbase_tx)) } else { + debug!("Miner: NOT Constructing tenure change and coinbase transactions"); (None, None) }; diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index cf615d6391..6ec75bc0af 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -4040,8 +4040,7 @@ fn continue_tenure_extend() { info!("Pausing commit op for the next block"); TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true); - next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) - .unwrap(); + next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap(); signer_vote_if_needed( &btc_regtest_controller, From ed15f3b6a3dc072cd3a5c983f5af4ea6899f633a Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 4 Jun 2024 15:44:37 -0400 Subject: [PATCH 06/14] WIP: burnchain tip has changed due to not using selected burn block Signed-off-by: Jacinta Ferrant --- .../stacks-node/src/nakamoto_node/miner.rs | 93 ++++++++++++------- .../stacks-node/src/nakamoto_node/relayer.rs | 59 ++++++++---- 2 files changed, 98 insertions(+), 54 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 09bab45751..8f2a4c3aab 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -68,7 +68,6 @@ pub enum MinerDirective { }, /// The miner should try to continue their tenure if they are the active miner ContinueTenure { - parent_tenure_start: StacksBlockId, new_burn_view: ConsensusHash, }, /// The miner did not win sortition @@ -134,6 +133,8 @@ pub struct BlockMinerThread { /// Copy of the node's registered VRF key registered_key: RegisteredKey, /// Burnchain block snapshot which elected this miner + burn_election_block: BlockSnapshot, + /// Current burnchain tip burn_block: BlockSnapshot, /// The start of the parent tenure for this tenure parent_tenure_id: StacksBlockId, @@ -148,6 +149,7 @@ impl BlockMinerThread { pub fn new( rt: &RelayerThread, registered_key: RegisteredKey, + burn_election_block: BlockSnapshot, burn_block: BlockSnapshot, parent_tenure_id: StacksBlockId, reason: MinerReason, @@ -159,6 +161,7 @@ impl BlockMinerThread { burnchain: rt.burnchain.clone(), mined_blocks: vec![], registered_key, + burn_election_block, burn_block, event_dispatcher: rt.event_dispatcher.clone(), parent_tenure_id, @@ -251,10 +254,7 @@ impl BlockMinerThread { } = &mut self.reason { // We should not issue multiple tenure change transactions for the same tenure - if !*tenure_change_mined { - debug!("Miner: Tenure change mined"); - *tenure_change_mined = true; - } + *tenure_change_mined = true; } } @@ -745,24 +745,30 @@ impl BlockMinerThread { .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)? .expect("FATAL: no epoch defined") .epoch_id; - debug!("HERE WE GO"); + debug!("HERE WE GO"); let mut parent_block_info = self.load_block_parent_info(&mut burn_db, &mut chain_state)?; let vrf_proof = self .make_vrf_proof() .ok_or_else(|| NakamotoNodeError::BadVrfConstruction)?; if self.mined_blocks.is_empty() && parent_block_info.parent_tenure.is_none() { - warn!( - "Miner should be starting a new tenure, but failed to load parent tenure info" - ); + warn!("Miner should be starting a new tenure, but failed to load parent tenure info"); return Err(NakamotoNodeError::ParentNotFound); }; - debug!("Parent block info parent tenure: {:?} and {:?} mined blocks", parent_block_info.parent_tenure, self.mined_blocks.len()); + debug!( + "Parent block info parent tenure: {:?} and {:?} mined blocks", + parent_block_info.parent_tenure, + self.mined_blocks.len() + ); // create our coinbase if this is the first block we've mined this tenure - let tenure_start_info = - self.make_tenure_start_info(&parent_block_info, vrf_proof, target_epoch_id)?; + let tenure_start_info = self.make_tenure_start_info( + &chain_state, + &parent_block_info, + vrf_proof, + target_epoch_id, + )?; parent_block_info.stacks_parent_header.microblock_tail = None; @@ -778,8 +784,8 @@ impl BlockMinerThread { &burn_db.index_conn(), &mut mem_pool, &parent_block_info.stacks_parent_header, - &self.burn_block.consensus_hash, - self.burn_block.total_burn, + &self.burn_election_block.consensus_hash, + self.burn_election_block.total_burn, tenure_start_info, self.config .make_nakamoto_block_builder_settings(self.globals.get_miner_status()), @@ -837,6 +843,7 @@ impl BlockMinerThread { /// Create the tenure start info for the block we're going to build fn make_tenure_start_info( &self, + chainstate: &StacksChainState, parent_block_info: &ParentStacksBlockInfo, vrf_proof: VRFProof, target_epoch_id: StacksEpochId, @@ -847,37 +854,53 @@ impl BlockMinerThread { let (tenure_change_tx, coinbase_tx) = if let Some(ref parent_tenure_info) = parent_block_info.parent_tenure { - debug!("Miner: Constructing tenure change and coinbase transactions: {}", self.reason); - let num_blocks_so_far = u32::try_from(parent_tenure_info.parent_tenure_blocks) - .expect("FATAL: more than u32 blocks in a tenure"); + debug!( + "Miner: Constructing tenure change and coinbase transactions: {}", + self.reason + ); let mut payload = TenureChangePayload { tenure_consensus_hash: self.burn_block.consensus_hash.clone(), prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash, burn_view_consensus_hash: self.burn_block.consensus_hash.clone(), previous_tenure_end: parent_block_id, - previous_tenure_blocks: num_blocks_so_far, + previous_tenure_blocks: u32::try_from(parent_tenure_info.parent_tenure_blocks) + .expect("FATAL: more than u32 blocks in a tenure"), cause: TenureChangeCause::BlockFound, pubkey_hash: self.keychain.get_nakamoto_pkh(), }; - if let MinerReason::Extended { - burn_view_consensus_hash, - tenure_change_mined, - } = &self.reason - { - debug!("Tenure change mined {tenure_change_mined}"); - if !*tenure_change_mined { - debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far); - payload = payload.extend( - *burn_view_consensus_hash, - parent_block_id, - num_blocks_so_far, - ); + + match &self.reason { + MinerReason::BlockFound => { + debug!("Miner: Constructing tenure change and coinbase transactions"); + let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; + let coinbase_tx = + self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof); + (Some(tenure_change_tx), Some(coinbase_tx)) + }, + MinerReason::Extended { + burn_view_consensus_hash, + tenure_change_mined, + } => { + let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length( + chainstate.db(), + &self.burn_election_block.consensus_hash, + ) + .map_err(NakamotoNodeError::MiningFailure)?; + debug!("Tenure change mined {tenure_change_mined}"); + if !*tenure_change_mined { + debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far); + payload = payload.extend( + *burn_view_consensus_hash, + parent_block_id, + num_blocks_so_far, + ); + let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; + (Some(tenure_change_tx), None) + } else { + (None, None) + } } } - let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; - let coinbase_tx = - self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof); - (Some(tenure_change_tx), Some(coinbase_tx)) } else { debug!("Miner: NOT Constructing tenure change and coinbase transactions"); (None, None) diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index c2a86e0942..aa52ce43ca 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -341,7 +341,6 @@ impl RelayerThread { } } else { MinerDirective::ContinueTenure { - parent_tenure_start: committed_index_hash, new_burn_view: consensus_hash, } } @@ -543,28 +542,29 @@ impl RelayerThread { fn create_block_miner( &mut self, registered_key: RegisteredKey, - last_burn_block: BlockSnapshot, + burn_tip: BlockSnapshot, + burn_election_block: BlockSnapshot, parent_tenure_id: StacksBlockId, reason: MinerReason, ) -> Result { debug!("Relayer: creating block miner thread"; "reason" => %reason ); - if fault_injection_skip_mining(&self.config.node.rpc_bind, last_burn_block.block_height) { + if fault_injection_skip_mining(&self.config.node.rpc_bind, burn_tip.block_height) { debug!( "Relayer: fault injection skip mining at block height {}", - last_burn_block.block_height + burn_tip.block_height ); return Err(NakamotoNodeError::FaultInjection); } - let burn_header_hash = last_burn_block.burn_header_hash.clone(); + let burn_header_hash = burn_tip.burn_header_hash.clone(); let burn_chain_sn = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn()) .expect("FATAL: failed to query sortition DB for canonical burn chain tip"); let burn_chain_tip = burn_chain_sn.burn_header_hash.clone(); - if burn_chain_tip != burn_header_hash && matches!(reason, MinerReason::BlockFound) { + if burn_chain_tip != burn_header_hash { debug!( "Relayer: Drop stale RunTenure for {}: current sortition is for {}", &burn_header_hash, &burn_chain_tip @@ -575,16 +575,19 @@ impl RelayerThread { debug!( "Relayer: Spawn tenure thread"; - "height" => last_burn_block.block_height, + "height" => burn_tip.block_height, "burn_header_hash" => %burn_header_hash, "parent_tenure_id" => %parent_tenure_id, - "reason" => %reason + "reason" => %reason, + "burn_election_block.consensus_hash" => %burn_election_block.consensus_hash, + "burn_tip.consensus_hash" => %burn_tip.consensus_hash, ); let miner_thread_state = BlockMinerThread::new( self, registered_key, - last_burn_block, + burn_election_block, + burn_tip, parent_tenure_id, reason, ); @@ -595,6 +598,7 @@ impl RelayerThread { &mut self, parent_tenure_start: StacksBlockId, burn_tip: BlockSnapshot, + block_election_snapshot: BlockSnapshot, reason: MinerReason, ) -> Result<(), NakamotoNodeError> { // when starting a new tenure, block the mining thread if its currently running. @@ -608,8 +612,13 @@ impl RelayerThread { warn!("Trying to start new tenure, but no VRF key active"); NakamotoNodeError::NoVRFKeyActive })?; - let new_miner_state = - self.create_block_miner(vrf_key, burn_tip, parent_tenure_start, reason)?; + let new_miner_state = self.create_block_miner( + vrf_key, + burn_tip, + block_election_snapshot, + parent_tenure_start, + reason, + )?; let new_miner_handle = std::thread::Builder::new() .name(format!("miner.{parent_tenure_start}")) @@ -653,7 +662,6 @@ impl RelayerThread { fn continue_tenure( &mut self, - parent_tenure_start: StacksBlockId, new_burn_view: ConsensusHash, ) -> Result<(), NakamotoNodeError> { if let Err(e) = self.stop_tenure() { @@ -662,7 +670,7 @@ impl RelayerThread { } debug!("Relayer: successfully stopped tenure."); // Check if we should undergo a tenure change to switch to the new burn view - let block_snapshot = self + let block_election_snapshot = self .sortdb .index_handle_at_tip() .get_last_snapshot_with_sortition_from_tip() @@ -671,18 +679,31 @@ impl RelayerThread { NakamotoNodeError::SnapshotNotFoundForChainTip })?; - if Some(block_snapshot.winning_block_txid) != self.current_mining_commit_tx { + if Some(block_election_snapshot.winning_block_txid) != self.current_mining_commit_tx { debug!("Relayer: the miner did not win the last sortition. No tenure to continue."; "current_mining_commit_tx" => %self.current_mining_commit_tx.unwrap_or(Txid([0u8; 32])), - "block_snapshot_winning_block_txid" => %block_snapshot.winning_block_txid + "block_snapshot_winning_block_txid" => %block_election_snapshot.winning_block_txid ); return Ok(()); } else { debug!("Relayer: the miner won the last sortition. Continuing tenure."); }; + + let burn_tip = + SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &new_burn_view) + .map_err(|e| { + error!("Relayer: failed to get block snapshot for new burn view: {e:?}"); + NakamotoNodeError::SnapshotNotFoundForChainTip + })? + .ok_or_else(|| { + error!("Relayer: failed to get block snapshot for new burn view"); + NakamotoNodeError::SnapshotNotFoundForChainTip + })?; + match self.start_new_tenure( - parent_tenure_start, - block_snapshot, + burn_tip.get_canonical_stacks_block_id(), // For tenure extend, we should be extending off the canonical tip + burn_tip, + block_election_snapshot, MinerReason::Extended { burn_view_consensus_hash: new_burn_view, tenure_change_mined: false, @@ -713,6 +734,7 @@ impl RelayerThread { burnchain_tip, } => match self.start_new_tenure( parent_tenure_start, + burnchain_tip.clone(), burnchain_tip, MinerReason::BlockFound, ) { @@ -725,8 +747,7 @@ impl RelayerThread { }, MinerDirective::ContinueTenure { new_burn_view, - parent_tenure_start, - } => match self.continue_tenure(parent_tenure_start, new_burn_view) { + } => match self.continue_tenure(new_burn_view) { Ok(()) => { debug!("Relayer: successfully handled continue tenure."); } From a2c0553f0b7655521e7761f976defd4c41529335 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 4 Jun 2024 16:19:19 -0400 Subject: [PATCH 07/14] WIP: cargo fmt stuff Signed-off-by: Jacinta Ferrant --- .../stacks-node/src/nakamoto_node/miner.rs | 33 ++++++++++--------- .../stacks-node/src/nakamoto_node/relayer.rs | 25 +++++++------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 8f2a4c3aab..fdff434044 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -67,9 +67,7 @@ pub enum MinerDirective { burnchain_tip: BlockSnapshot, }, /// The miner should try to continue their tenure if they are the active miner - ContinueTenure { - new_burn_view: ConsensusHash, - }, + ContinueTenure { new_burn_view: ConsensusHash }, /// The miner did not win sortition StopTenure, } @@ -859,9 +857,9 @@ impl BlockMinerThread { self.reason ); let mut payload = TenureChangePayload { - tenure_consensus_hash: self.burn_block.consensus_hash.clone(), + tenure_consensus_hash: self.burn_election_block.consensus_hash.clone(), prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash, - burn_view_consensus_hash: self.burn_block.consensus_hash.clone(), + burn_view_consensus_hash: self.burn_election_block.consensus_hash.clone(), previous_tenure_end: parent_block_id, previous_tenure_blocks: u32::try_from(parent_tenure_info.parent_tenure_blocks) .expect("FATAL: more than u32 blocks in a tenure"), @@ -872,29 +870,34 @@ impl BlockMinerThread { match &self.reason { MinerReason::BlockFound => { debug!("Miner: Constructing tenure change and coinbase transactions"); - let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; - let coinbase_tx = - self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof); + let tenure_change_tx = + self.generate_tenure_change_tx(current_miner_nonce, payload)?; + let coinbase_tx = self.generate_coinbase_tx( + current_miner_nonce + 1, + target_epoch_id, + vrf_proof, + ); (Some(tenure_change_tx), Some(coinbase_tx)) - }, + } MinerReason::Extended { burn_view_consensus_hash, tenure_change_mined, } => { - let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length( - chainstate.db(), - &self.burn_election_block.consensus_hash, - ) - .map_err(NakamotoNodeError::MiningFailure)?; debug!("Tenure change mined {tenure_change_mined}"); if !*tenure_change_mined { + let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length( + chainstate.db(), + &self.burn_election_block.consensus_hash, + ) + .map_err(NakamotoNodeError::MiningFailure)?; debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far); payload = payload.extend( *burn_view_consensus_hash, parent_block_id, num_blocks_so_far, ); - let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; + let tenure_change_tx = + self.generate_tenure_change_tx(current_miner_nonce, payload)?; (Some(tenure_change_tx), None) } else { (None, None) diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index aa52ce43ca..62026add20 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -660,10 +660,7 @@ impl RelayerThread { Ok(()) } - fn continue_tenure( - &mut self, - new_burn_view: ConsensusHash, - ) -> Result<(), NakamotoNodeError> { + fn continue_tenure(&mut self, new_burn_view: ConsensusHash) -> Result<(), NakamotoNodeError> { if let Err(e) = self.stop_tenure() { error!("Relayer: Failed to stop tenure: {:?}", e); return Ok(()); @@ -745,17 +742,17 @@ impl RelayerThread { error!("Relayer: Failed to start new tenure: {:?}", e); } }, - MinerDirective::ContinueTenure { - new_burn_view, - } => match self.continue_tenure(new_burn_view) { - Ok(()) => { - debug!("Relayer: successfully handled continue tenure."); + MinerDirective::ContinueTenure { new_burn_view } => { + match self.continue_tenure(new_burn_view) { + Ok(()) => { + debug!("Relayer: successfully handled continue tenure."); + } + Err(e) => { + error!("Relayer: Failed to continue tenure: {:?}", e); + return false; + } } - Err(e) => { - error!("Relayer: Failed to continue tenure: {:?}", e); - return false; - } - }, + } MinerDirective::StopTenure => match self.stop_tenure() { Ok(()) => { debug!("Relayer: successfully stopped tenure."); From ff40a27249483bf82cf922217669480df3813203 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 5 Jun 2024 16:09:51 -0400 Subject: [PATCH 08/14] WIP: Can't figure out why block election snapshot does not match between new block tenure and following continue tenure Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/nakamoto/miner.rs | 9 +++ stackslib/src/chainstate/nakamoto/mod.rs | 65 ++++++++----------- stackslib/src/chainstate/nakamoto/tenure.rs | 15 ++++- .../stacks-node/src/nakamoto_node/relayer.rs | 10 +-- 4 files changed, 56 insertions(+), 43 deletions(-) diff --git a/stackslib/src/chainstate/nakamoto/miner.rs b/stackslib/src/chainstate/nakamoto/miner.rs index ab9ae6a5f9..6b434cfb6d 100644 --- a/stackslib/src/chainstate/nakamoto/miner.rs +++ b/stackslib/src/chainstate/nakamoto/miner.rs @@ -79,6 +79,7 @@ use crate::util_lib::boot::boot_code_id; use crate::util_lib::db::Error as DBError; /// Nakamaoto tenure information +#[derive(Debug)] pub struct NakamotoTenureInfo { /// Coinbase tx, if this is a new tenure pub coinbase_tx: Option, @@ -410,6 +411,13 @@ impl NakamotoBlockBuilder { signer_transactions: Vec, signer_bitvec_len: u16, ) -> Result<(NakamotoBlock, ExecutionCost, u64, Vec), Error> { + let parent_block_id = parent_stacks_header.index_block_hash(); + info!("Building a nakamoto block"; + "parent_block_id" => %parent_block_id, + "tenure_id_consensus_hash" => %tenure_id_consensus_hash, + "parent_consensus_hash" => %parent_stacks_header.consensus_hash, + "tenure_info" => ?tenure_info + ); let (tip_consensus_hash, tip_block_hash, tip_height) = ( parent_stacks_header.consensus_hash.clone(), parent_stacks_header.anchored_header.block_hash(), @@ -503,6 +511,7 @@ impl NakamotoBlockBuilder { "execution_consumed" => %consumed, "%-full" => block_limit.proportion_largest_dimension(&consumed), "assembly_time_ms" => ts_end.saturating_sub(ts_start), + "consensus_hash" => %block.header.consensus_hash ); Ok((block, consumed, size, tx_events)) diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 302ac5a6e6..cd97c2a5ed 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -2784,6 +2784,10 @@ impl NakamotoChainState { ) }; + error!( + "Processing block: block.header.consensus_hash {:?}, parent_ch {:?}", + block.header.consensus_hash, parent_ch + ); let parent_block_id = StacksBlockId::new(&parent_ch, &parent_block_hash); if parent_block_id != block.header.parent_block_id { warn!("Error processing nakamoto block: Parent consensus hash does not match db view"; @@ -2802,44 +2806,31 @@ impl NakamotoChainState { let burn_header_height = tenure_block_snapshot.block_height; let block_hash = block.header.block_hash(); - let new_tenure = match block.is_wellformed_tenure_start_block() { - Ok(true) => true, - Ok(false) => { - // this block is mined in the ongoing tenure. - if !Self::check_tenure_continuity( - chainstate_tx, - burn_dbconn.sqlite(), - &parent_ch, - &block.header, - )? { - // this block is not part of the ongoing tenure; it's invalid - return Err(ChainstateError::ExpectedTenureChange); - } - false - } - Err(_) => { - return Err(ChainstateError::InvalidStacksBlock( - "Invalid tenure changes in nakamoto block".into(), - )); - } - }; + let new_tenure = block.is_wellformed_tenure_start_block().map_err(|_| { + ChainstateError::InvalidStacksBlock("Invalid tenure changes in nakamoto block".into()) + })?; + // this block is mined in the ongoing tenure. + if !new_tenure + && !Self::check_tenure_continuity( + chainstate_tx, + burn_dbconn.sqlite(), + &parent_ch, + &block.header, + )? + { + warn!("FAILED"); + // this block is not part of the ongoing tenure; it's invalid + return Err(ChainstateError::ExpectedTenureChange); + } + let tenure_extend = block.is_wellformed_tenure_extend_block().map_err(|_| { + ChainstateError::InvalidStacksBlock("Invalid tenure changes in nakamoto block".into()) + })?; - let tenure_extend = match block.is_wellformed_tenure_extend_block() { - Ok(true) => { - if new_tenure { - return Err(ChainstateError::InvalidStacksBlock( - "Both started and extended tenure".into(), - )); - } - true - } - Ok(false) => false, - Err(_) => { - return Err(ChainstateError::InvalidStacksBlock( - "Invalid tenure extend in nakamoto block".into(), - )); - } - }; + if tenure_extend && new_tenure { + return Err(ChainstateError::InvalidStacksBlock( + "Both started and extended tenure".into(), + )); + } let parent_coinbase_height = if block.is_first_mined() { 0 diff --git a/stackslib/src/chainstate/nakamoto/tenure.rs b/stackslib/src/chainstate/nakamoto/tenure.rs index c9e5c0cf59..ed1b3dd35e 100644 --- a/stackslib/src/chainstate/nakamoto/tenure.rs +++ b/stackslib/src/chainstate/nakamoto/tenure.rs @@ -853,7 +853,7 @@ impl NakamotoChainState { } /// Check that this block is in the same tenure as its parent, and that this tenure is the - /// highest-seen tenure. Use this to check blocks that do _not_ have tenure-changes. + /// highest-seen tenure. Use this to check blocks that do _not_ have BlockFound tenure-changes. /// /// Returns Ok(bool) to indicate whether or not this block is in the same tenure as its parent. /// Returns Err(..) on DB error @@ -865,17 +865,30 @@ impl NakamotoChainState { ) -> Result { // block must have the same consensus hash as its parent if block_header.is_first_mined() || parent_ch != &block_header.consensus_hash { + error!( + "BLOCK HEADER IS FIRST MINED: {}", + block_header.is_first_mined() + ); + error!("Block is not in the same tenure as its parent"; + "parent_ch" => %parent_ch, + "block_header.consensus_hash" => %block_header.consensus_hash, + "block_header" => ?block_header); return Ok(false); } // block must be in the same tenure as the highest-processed tenure. let Some(highest_tenure) = Self::get_highest_nakamoto_tenure(headers_conn, sortdb_conn)? else { + error!("No tenure found"); // no tenures yet, so definitely not continuous return Ok(false); }; if &highest_tenure.tenure_id_consensus_hash != parent_ch { + error!("Block is not in the highest-known tenure"; + "highest_tenure" => %highest_tenure.tenure_id_consensus_hash, + "block_header.consensus_hash" => %block_header.consensus_hash, + "block_header" => ?block_header); // this block is not in the highest-known tenure, so it can't be continuous return Ok(false); } diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index 62026add20..8da99d1e36 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -542,8 +542,8 @@ impl RelayerThread { fn create_block_miner( &mut self, registered_key: RegisteredKey, - burn_tip: BlockSnapshot, burn_election_block: BlockSnapshot, + burn_tip: BlockSnapshot, parent_tenure_id: StacksBlockId, reason: MinerReason, ) -> Result { @@ -573,7 +573,7 @@ impl RelayerThread { return Err(NakamotoNodeError::MissedMiningOpportunity); } - debug!( + info!( "Relayer: Spawn tenure thread"; "height" => burn_tip.block_height, "burn_header_hash" => %burn_header_hash, @@ -597,8 +597,8 @@ impl RelayerThread { fn start_new_tenure( &mut self, parent_tenure_start: StacksBlockId, - burn_tip: BlockSnapshot, block_election_snapshot: BlockSnapshot, + burn_tip: BlockSnapshot, reason: MinerReason, ) -> Result<(), NakamotoNodeError> { // when starting a new tenure, block the mining thread if its currently running. @@ -614,8 +614,8 @@ impl RelayerThread { })?; let new_miner_state = self.create_block_miner( vrf_key, - burn_tip, block_election_snapshot, + burn_tip, parent_tenure_start, reason, )?; @@ -699,8 +699,8 @@ impl RelayerThread { match self.start_new_tenure( burn_tip.get_canonical_stacks_block_id(), // For tenure extend, we should be extending off the canonical tip - burn_tip, block_election_snapshot, + burn_tip, MinerReason::Extended { burn_view_consensus_hash: new_burn_view, tenure_change_mined: false, From 27e206c45143a2ceccb6a6da258a72ac80b54e03 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 5 Jun 2024 17:27:40 -0400 Subject: [PATCH 09/14] WIP: fetch the bitcoin snapshot that elected the current canconical tip Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/nakamoto/miner.rs | 2 +- .../stacks-node/src/nakamoto_node/relayer.rs | 52 +++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/stackslib/src/chainstate/nakamoto/miner.rs b/stackslib/src/chainstate/nakamoto/miner.rs index 6b434cfb6d..32402325b1 100644 --- a/stackslib/src/chainstate/nakamoto/miner.rs +++ b/stackslib/src/chainstate/nakamoto/miner.rs @@ -412,7 +412,7 @@ impl NakamotoBlockBuilder { signer_bitvec_len: u16, ) -> Result<(NakamotoBlock, ExecutionCost, u64, Vec), Error> { let parent_block_id = parent_stacks_header.index_block_hash(); - info!("Building a nakamoto block"; + info!("Building a nakamoto block"; "parent_block_id" => %parent_block_id, "tenure_id_consensus_hash" => %tenure_id_consensus_hash, "parent_consensus_hash" => %parent_stacks_header.consensus_hash, diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index 8da99d1e36..75653d98ae 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -44,7 +44,7 @@ use stacks::net::db::LocalPeer; use stacks::net::relay::Relayer; use stacks::net::NetworkResult; use stacks_common::types::chainstate::{ - BlockHeaderHash, BurnchainHeaderHash, StacksBlockId, VRFSeed, + BlockHeaderHash, BurnchainHeaderHash, StacksBlockId, StacksPublicKey, VRFSeed, }; use stacks_common::types::StacksEpochId; use stacks_common::util::get_epoch_time_ms; @@ -667,25 +667,6 @@ impl RelayerThread { } debug!("Relayer: successfully stopped tenure."); // Check if we should undergo a tenure change to switch to the new burn view - let block_election_snapshot = self - .sortdb - .index_handle_at_tip() - .get_last_snapshot_with_sortition_from_tip() - .map_err(|e| { - error!("Relayer: failed to get last sortition snapshot: {e:?}"); - NakamotoNodeError::SnapshotNotFoundForChainTip - })?; - - if Some(block_election_snapshot.winning_block_txid) != self.current_mining_commit_tx { - debug!("Relayer: the miner did not win the last sortition. No tenure to continue."; - "current_mining_commit_tx" => %self.current_mining_commit_tx.unwrap_or(Txid([0u8; 32])), - "block_snapshot_winning_block_txid" => %block_election_snapshot.winning_block_txid - ); - return Ok(()); - } else { - debug!("Relayer: the miner won the last sortition. Continuing tenure."); - }; - let burn_tip = SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &new_burn_view) .map_err(|e| { @@ -697,8 +678,37 @@ impl RelayerThread { NakamotoNodeError::SnapshotNotFoundForChainTip })?; + let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) = + SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn()).unwrap(); + let canonical_stacks_tip = + StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh); + let block_election_snapshot = + SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch) + .map_err(|e| { + error!("Relayer: failed to get block snapshot for canonical tip: {e:?}"); + NakamotoNodeError::SnapshotNotFoundForChainTip + })? + .ok_or_else(|| { + error!("Relayer: failed to get block snapshot for canonical tip"); + NakamotoNodeError::SnapshotNotFoundForChainTip + })?; + + let Some(ref mining_key) = self.config.miner.mining_key else { + return Ok(()); + }; + let mining_pkh = Hash160::from_node_public_key(&StacksPublicKey::from_private(mining_key)); + if block_election_snapshot.miner_pk_hash != Some(mining_pkh) { + debug!("Relayer: the miner did not win the last sortition. No tenure to continue."; + "current_mining_pkh" => %mining_pkh, + "block_snapshot.miner_pk_hash" => ?block_election_snapshot.miner_pk_hash, + ); + return Ok(()); + } else { + debug!("Relayer: the miner won the last sortition. Continuing tenure."); + } + match self.start_new_tenure( - burn_tip.get_canonical_stacks_block_id(), // For tenure extend, we should be extending off the canonical tip + canonical_stacks_tip, // For tenure extend, we should be extending off the canonical tip block_election_snapshot, burn_tip, MinerReason::Extended { From 2d32ba08ce311542510ac42f7c71204b5ad5aba0 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 6 Jun 2024 09:34:25 -0400 Subject: [PATCH 10/14] Fix test to only care about the 15 tenures we mine Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/nakamoto/miner.rs | 7 -- stackslib/src/chainstate/nakamoto/mod.rs | 5 -- stackslib/src/chainstate/nakamoto/tenure.rs | 13 ---- .../stacks-node/src/nakamoto_node/miner.rs | 66 ++++++------------- .../stacks-node/src/nakamoto_node/relayer.rs | 6 +- .../src/tests/nakamoto_integrations.rs | 14 +--- 6 files changed, 23 insertions(+), 88 deletions(-) diff --git a/stackslib/src/chainstate/nakamoto/miner.rs b/stackslib/src/chainstate/nakamoto/miner.rs index 32402325b1..233304adb2 100644 --- a/stackslib/src/chainstate/nakamoto/miner.rs +++ b/stackslib/src/chainstate/nakamoto/miner.rs @@ -411,13 +411,6 @@ impl NakamotoBlockBuilder { signer_transactions: Vec, signer_bitvec_len: u16, ) -> Result<(NakamotoBlock, ExecutionCost, u64, Vec), Error> { - let parent_block_id = parent_stacks_header.index_block_hash(); - info!("Building a nakamoto block"; - "parent_block_id" => %parent_block_id, - "tenure_id_consensus_hash" => %tenure_id_consensus_hash, - "parent_consensus_hash" => %parent_stacks_header.consensus_hash, - "tenure_info" => ?tenure_info - ); let (tip_consensus_hash, tip_block_hash, tip_height) = ( parent_stacks_header.consensus_hash.clone(), parent_stacks_header.anchored_header.block_hash(), diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index cd97c2a5ed..c7a0fd3c95 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -2784,10 +2784,6 @@ impl NakamotoChainState { ) }; - error!( - "Processing block: block.header.consensus_hash {:?}, parent_ch {:?}", - block.header.consensus_hash, parent_ch - ); let parent_block_id = StacksBlockId::new(&parent_ch, &parent_block_hash); if parent_block_id != block.header.parent_block_id { warn!("Error processing nakamoto block: Parent consensus hash does not match db view"; @@ -2818,7 +2814,6 @@ impl NakamotoChainState { &block.header, )? { - warn!("FAILED"); // this block is not part of the ongoing tenure; it's invalid return Err(ChainstateError::ExpectedTenureChange); } diff --git a/stackslib/src/chainstate/nakamoto/tenure.rs b/stackslib/src/chainstate/nakamoto/tenure.rs index ed1b3dd35e..45a0149b64 100644 --- a/stackslib/src/chainstate/nakamoto/tenure.rs +++ b/stackslib/src/chainstate/nakamoto/tenure.rs @@ -865,30 +865,17 @@ impl NakamotoChainState { ) -> Result { // block must have the same consensus hash as its parent if block_header.is_first_mined() || parent_ch != &block_header.consensus_hash { - error!( - "BLOCK HEADER IS FIRST MINED: {}", - block_header.is_first_mined() - ); - error!("Block is not in the same tenure as its parent"; - "parent_ch" => %parent_ch, - "block_header.consensus_hash" => %block_header.consensus_hash, - "block_header" => ?block_header); return Ok(false); } // block must be in the same tenure as the highest-processed tenure. let Some(highest_tenure) = Self::get_highest_nakamoto_tenure(headers_conn, sortdb_conn)? else { - error!("No tenure found"); // no tenures yet, so definitely not continuous return Ok(false); }; if &highest_tenure.tenure_id_consensus_hash != parent_ch { - error!("Block is not in the highest-known tenure"; - "highest_tenure" => %highest_tenure.tenure_id_consensus_hash, - "block_header.consensus_hash" => %block_header.consensus_hash, - "block_header" => ?block_header); // this block is not in the highest-known tenure, so it can't be continuous return Ok(false); } diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index fdff434044..ae16ca3ff1 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -100,8 +100,6 @@ pub enum MinerReason { /// Current consensus hash on the underlying burnchain. Corresponds to the last-seen /// sortition. burn_view_consensus_hash: ConsensusHash, - /// Wether the tenure change transaction was mined - tenure_change_mined: bool, }, } @@ -109,9 +107,11 @@ impl std::fmt::Display for MinerReason { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MinerReason::BlockFound => write!(f, "BlockFound"), - MinerReason::Extended { burn_view_consensus_hash, tenure_change_mined } => write!( + MinerReason::Extended { + burn_view_consensus_hash, + } => write!( f, - "Extended: burn_view_consensus_hash = {burn_view_consensus_hash:?}, tenure_change_mined = {tenure_change_mined}", + "Extended: burn_view_consensus_hash = {burn_view_consensus_hash:?}", ), } } @@ -246,14 +246,6 @@ impl BlockMinerThread { "consensus_hash" => %new_block.header.consensus_hash, ); self.globals.coord().announce_new_stacks_block(); - if let MinerReason::Extended { - tenure_change_mined, - .. - } = &mut self.reason - { - // We should not issue multiple tenure change transactions for the same tenure - *tenure_change_mined = true; - } } self.globals.counters.bump_naka_mined_blocks(); @@ -743,7 +735,6 @@ impl BlockMinerThread { .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)? .expect("FATAL: no epoch defined") .epoch_id; - debug!("HERE WE GO"); let mut parent_block_info = self.load_block_parent_info(&mut burn_db, &mut chain_state)?; let vrf_proof = self .make_vrf_proof() @@ -754,12 +745,6 @@ impl BlockMinerThread { return Err(NakamotoNodeError::ParentNotFound); }; - debug!( - "Parent block info parent tenure: {:?} and {:?} mined blocks", - parent_block_info.parent_tenure, - self.mined_blocks.len() - ); - // create our coinbase if this is the first block we've mined this tenure let tenure_start_info = self.make_tenure_start_info( &chain_state, @@ -783,7 +768,7 @@ impl BlockMinerThread { &mut mem_pool, &parent_block_info.stacks_parent_header, &self.burn_election_block.consensus_hash, - self.burn_election_block.total_burn, + self.burn_block.total_burn, tenure_start_info, self.config .make_nakamoto_block_builder_settings(self.globals.get_miner_status()), @@ -846,16 +831,11 @@ impl BlockMinerThread { vrf_proof: VRFProof, target_epoch_id: StacksEpochId, ) -> Result { - debug!("MAKING TENURE START INFO"); let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash(); let current_miner_nonce = parent_block_info.coinbase_nonce; let (tenure_change_tx, coinbase_tx) = if let Some(ref parent_tenure_info) = parent_block_info.parent_tenure { - debug!( - "Miner: Constructing tenure change and coinbase transactions: {}", - self.reason - ); let mut payload = TenureChangePayload { tenure_consensus_hash: self.burn_election_block.consensus_hash.clone(), prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash, @@ -869,7 +849,6 @@ impl BlockMinerThread { match &self.reason { MinerReason::BlockFound => { - debug!("Miner: Constructing tenure change and coinbase transactions"); let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload)?; let coinbase_tx = self.generate_coinbase_tx( @@ -881,31 +860,24 @@ impl BlockMinerThread { } MinerReason::Extended { burn_view_consensus_hash, - tenure_change_mined, } => { - debug!("Tenure change mined {tenure_change_mined}"); - if !*tenure_change_mined { - let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length( - chainstate.db(), - &self.burn_election_block.consensus_hash, - ) - .map_err(NakamotoNodeError::MiningFailure)?; - debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far); - payload = payload.extend( - *burn_view_consensus_hash, - parent_block_id, - num_blocks_so_far, - ); - let tenure_change_tx = - self.generate_tenure_change_tx(current_miner_nonce, payload)?; - (Some(tenure_change_tx), None) - } else { - (None, None) - } + let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length( + chainstate.db(), + &self.burn_election_block.consensus_hash, + ) + .map_err(NakamotoNodeError::MiningFailure)?; + debug!("Miner: Extending tenure"; "burn_view_consensus_hash" => %burn_view_consensus_hash, "parent_block_id" => %parent_block_id, "num_blocks_so_far" => num_blocks_so_far); + payload = payload.extend( + *burn_view_consensus_hash, + parent_block_id, + num_blocks_so_far, + ); + let tenure_change_tx = + self.generate_tenure_change_tx(current_miner_nonce, payload)?; + (Some(tenure_change_tx), None) } } } else { - debug!("Miner: NOT Constructing tenure change and coinbase transactions"); (None, None) }; diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index 75653d98ae..5ae749faac 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -547,9 +547,6 @@ impl RelayerThread { parent_tenure_id: StacksBlockId, reason: MinerReason, ) -> Result { - debug!("Relayer: creating block miner thread"; - "reason" => %reason - ); if fault_injection_skip_mining(&self.config.node.rpc_bind, burn_tip.block_height) { debug!( "Relayer: fault injection skip mining at block height {}", @@ -573,7 +570,7 @@ impl RelayerThread { return Err(NakamotoNodeError::MissedMiningOpportunity); } - info!( + debug!( "Relayer: Spawn tenure thread"; "height" => burn_tip.block_height, "burn_header_hash" => %burn_header_hash, @@ -713,7 +710,6 @@ impl RelayerThread { burn_tip, MinerReason::Extended { burn_view_consensus_hash: new_burn_view, - tenure_change_mined: false, }, ) { Ok(()) => { diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 6ec75bc0af..63c95070a7 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -4037,7 +4037,7 @@ fn continue_tenure_extend() { &signers, ); - info!("Pausing commit op for the next block"); + info!("Pausing commit ops to trigger a tenure extend."); TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true); next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap(); @@ -4073,7 +4073,6 @@ fn continue_tenure_extend() { ) .unwrap(); - debug!("MINING A STACKS BLOCK"); next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) .unwrap(); @@ -4084,7 +4083,6 @@ fn continue_tenure_extend() { &signers, ); - debug!("MINING THE NEXT BLOCK"); next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap(); signer_vote_if_needed( @@ -4094,10 +4092,9 @@ fn continue_tenure_extend() { &signers, ); - debug!("Unpausing commit op"); + info!("Resuming commit ops to mine regular tenures."); TEST_SKIP_COMMIT_OP.lock().unwrap().replace(false); - debug!("MINING THE NEXT TENURES"); // Mine 15 more regular nakamoto tenures for _i in 0..15 { let commits_before = commits_submitted.load(Ordering::SeqCst); @@ -4127,11 +4124,6 @@ fn continue_tenure_extend() { let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) .unwrap() .unwrap(); - info!( - "Latest tip"; - "height" => tip.stacks_block_height, - "is_nakamoto" => tip.anchored_header.as_stacks_nakamoto().is_some(), - ); // assert that the transfer tx was observed let transfer_tx_included = test_observer::get_blocks() @@ -4152,7 +4144,7 @@ fn continue_tenure_extend() { ); assert!(tip.anchored_header.as_stacks_nakamoto().is_some()); - assert!(tip.stacks_block_height >= block_height_pre_3_0 + 30); + assert!(tip.stacks_block_height >= block_height_pre_3_0 + 15); // make sure prometheus returns an updated height #[cfg(feature = "monitoring_prom")] From 309cafe9b87ebe6cc580cf06b6a64dddafd2c462 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 6 Jun 2024 10:11:51 -0400 Subject: [PATCH 11/14] Fix bad merge Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/nakamoto_node/miner.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 94f2ad7016..42f540540a 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -886,8 +886,6 @@ impl BlockMinerThread { return Err(NakamotoNodeError::ParentNotFound); }; - let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash(); - // create our coinbase if this is the first block we've mined this tenure let tenure_start_info = self.make_tenure_start_info( &chain_state, @@ -1021,6 +1019,7 @@ impl BlockMinerThread { }); }; + let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash(); let mut payload = TenureChangePayload { tenure_consensus_hash: self.burn_election_block.consensus_hash.clone(), prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash, From 24b5e790f4b8bff2b31a4253ff5bc7c82182338d Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 6 Jun 2024 12:35:51 -0400 Subject: [PATCH 12/14] Cleanup stale data from prior commits Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/nakamoto_node.rs | 2 -- testnet/stacks-node/src/nakamoto_node/relayer.rs | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node.rs b/testnet/stacks-node/src/nakamoto_node.rs index a60370f612..8a1d80de32 100644 --- a/testnet/stacks-node/src/nakamoto_node.rs +++ b/testnet/stacks-node/src/nakamoto_node.rs @@ -103,8 +103,6 @@ pub enum Error { SigningCoordinatorFailure(String), // The thread that we tried to send to has closed ChannelClosed, - /// The block header for the tenure start is missing - MissingTenureStartBlockHeader, } impl StacksNode { diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index 073787f565..ddad31ad72 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -163,8 +163,6 @@ pub struct RelayerThread { /// This is the last snapshot in which the relayer committed, and the parent_tenure_id /// which was committed to last_committed: Option<(BlockSnapshot, StacksBlockId)>, - /// The last commit that the relayer submitted which won the sortition - current_mining_commit_tx: Option, } impl RelayerThread { @@ -222,7 +220,6 @@ impl RelayerThread { is_miner, next_initiative: Instant::now() + Duration::from_millis(next_initiative_delay), last_committed: None, - current_mining_commit_tx: None, } } @@ -315,7 +312,9 @@ impl RelayerThread { .expect("FATAL: unknown consensus hash"); self.globals.set_last_sortition(sn.clone()); + let won_sortition = sn.sortition && self.last_commits.remove(&sn.winning_block_txid); + info!( "Relayer: Process sortition"; "sortition_ch" => %consensus_hash, @@ -328,7 +327,6 @@ impl RelayerThread { if won_sortition { increment_stx_blocks_mined_counter(); - self.current_mining_commit_tx = Some(sn.winning_block_txid); } if sn.sortition { From 8281f17150ce6f8337868d503690199643e20768 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 17 Jun 2024 13:31:00 -0400 Subject: [PATCH 13/14] CRC: use the last snapshot with a sortition to determine if we should proceed with a continue_tenure Signed-off-by: Jacinta Ferrant --- .../stacks-node/src/nakamoto_node/relayer.rs | 14 ++++++- .../src/tests/nakamoto_integrations.rs | 42 +++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index ddad31ad72..0552851437 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -698,10 +698,20 @@ impl RelayerThread { return Ok(()); }; let mining_pkh = Hash160::from_node_public_key(&StacksPublicKey::from_private(mining_key)); - if block_election_snapshot.miner_pk_hash != Some(mining_pkh) { + + let last_winner_snapshot = { + let ih = self.sortdb.index_handle(&burn_tip.sortition_id); + ih.get_last_snapshot_with_sortition(burn_tip.block_height) + .map_err(|e| { + error!("Relayer: failed to get last snapshot with sortition: {e:?}"); + NakamotoNodeError::SnapshotNotFoundForChainTip + })? + }; + + if last_winner_snapshot.miner_pk_hash != Some(mining_pkh) { debug!("Relayer: the miner did not win the last sortition. No tenure to continue."; "current_mining_pkh" => %mining_pkh, - "block_snapshot.miner_pk_hash" => ?block_election_snapshot.miner_pk_hash, + "last_winner_snapshot.miner_pk_hash" => ?last_winner_snapshot.miner_pk_hash, ); return Ok(()); } else { diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index d9045a820c..33daed875b 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -52,9 +52,10 @@ use stacks::chainstate::stacks::miner::{ BlockBuilder, BlockLimitFunction, TransactionEvent, TransactionResult, TransactionSuccessEvent, }; use stacks::chainstate::stacks::{ - SinglesigHashMode, SinglesigSpendingCondition, StacksTransaction, TenureChangePayload, - TransactionAnchorMode, TransactionAuth, TransactionPayload, TransactionPostConditionMode, - TransactionPublicKeyEncoding, TransactionSpendingCondition, TransactionVersion, MAX_BLOCK_LEN, + SinglesigHashMode, SinglesigSpendingCondition, StacksTransaction, TenureChangeCause, + TenureChangePayload, TransactionAnchorMode, TransactionAuth, TransactionPayload, + TransactionPostConditionMode, TransactionPublicKeyEncoding, TransactionSpendingCondition, + TransactionVersion, MAX_BLOCK_LEN, }; use stacks::core::mempool::MAXIMUM_MEMPOOL_TX_CHAINING; use stacks::core::{ @@ -5233,6 +5234,40 @@ fn continue_tenure_extend() { .unwrap() .unwrap(); + // assert that the tenure extend tx was observed + let extend_tx_included = test_observer::get_blocks() + .into_iter() + .find(|block_json| { + block_json["transactions"] + .as_array() + .unwrap() + .iter() + .find(|tx_json| { + let raw_tx = tx_json["raw_tx"].as_str().unwrap(); + if raw_tx == "0x00" { + return false; + } + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = + StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + match parsed.payload { + TransactionPayload::TenureChange(payload) => { + if payload.cause == TenureChangeCause::Extended { + return true; + } + } + _ => {} + }; + false + }) + .is_some() + }) + .is_some(); + assert!( + extend_tx_included, + "Nakamoto node failed to include the tenure extend tx" + ); + // assert that the transfer tx was observed let transfer_tx_included = test_observer::get_blocks() .into_iter() @@ -5245,7 +5280,6 @@ fn continue_tenure_extend() { .is_some() }) .is_some(); - assert!( transfer_tx_included, "Nakamoto node failed to include the transfer tx" From 11a6262651a82ad4d64f4a1148e9badfee209b7f Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 17 Jun 2024 21:13:11 -0400 Subject: [PATCH 14/14] CRC: update continue tenure extend to include a check for block found tenure reasons Signed-off-by: Jacinta Ferrant --- .../src/tests/nakamoto_integrations.rs | 82 +++++++++---------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 33daed875b..4b74f6af8b 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -5010,10 +5010,13 @@ fn signer_chainstate() { /// This test spins up a nakamoto-neon node. /// It starts in Epoch 2.0, mines with `neon_node` to Epoch 3.0, and then switches /// to Nakamoto operation (activating pox-4 by submitting a stack-stx tx). The BootLoop -/// struct handles the epoch-2/3 tear-down and spin-up. +/// struct handles the epoch-2/3 tear-down and spin-up. It mines a regular Nakamoto tenure +/// before pausing the commit op to produce an empty sortition, forcing a tenure extend. +/// Commit ops are resumed, and an additional 15 nakamoto tenures mined. /// This test makes three assertions: -/// * 30 blocks are mined after 3.0 starts. This is enough to mine across 2 reward cycles +/// * 15 blocks are mined after 3.0 starts. /// * A transaction submitted to the mempool in 3.0 will be mined in 3.0 +/// * A tenure extend transaction was successfully mined in 3.0 /// * The final chain tip is a nakamoto block fn continue_tenure_extend() { if env::var("BITCOIND_TEST") != Ok("1".into()) { @@ -5235,58 +5238,47 @@ fn continue_tenure_extend() { .unwrap(); // assert that the tenure extend tx was observed - let extend_tx_included = test_observer::get_blocks() - .into_iter() - .find(|block_json| { - block_json["transactions"] - .as_array() - .unwrap() - .iter() - .find(|tx_json| { - let raw_tx = tx_json["raw_tx"].as_str().unwrap(); - if raw_tx == "0x00" { - return false; - } - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = - StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - match parsed.payload { - TransactionPayload::TenureChange(payload) => { - if payload.cause == TenureChangeCause::Extended { - return true; - } - } - _ => {} - }; - false - }) - .is_some() - }) - .is_some(); + let mut tenure_extends = vec![]; + let mut tenure_block_founds = vec![]; + let mut transfer_tx_included = false; + for block in test_observer::get_blocks() { + for tx in block["transactions"].as_array().unwrap() { + let raw_tx = tx["raw_tx"].as_str().unwrap(); + if raw_tx == &transfer_tx_hex { + transfer_tx_included = true; + continue; + } + if raw_tx == "0x00" { + continue; + } + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + match &parsed.payload { + TransactionPayload::TenureChange(payload) => match payload.cause { + TenureChangeCause::Extended => tenure_extends.push(parsed), + TenureChangeCause::BlockFound => tenure_block_founds.push(parsed), + }, + _ => {} + }; + } + } assert!( - extend_tx_included, - "Nakamoto node failed to include the tenure extend tx" + !tenure_extends.is_empty(), + "Nakamoto node failed to include the tenure extend txs" + ); + + assert!( + tenure_block_founds.len() >= 17 - tenure_extends.len(), + "Nakamoto node failed to include the block found tx per winning sortition" ); - // assert that the transfer tx was observed - let transfer_tx_included = test_observer::get_blocks() - .into_iter() - .find(|block_json| { - block_json["transactions"] - .as_array() - .unwrap() - .iter() - .find(|tx_json| tx_json["raw_tx"].as_str() == Some(&transfer_tx_hex)) - .is_some() - }) - .is_some(); assert!( transfer_tx_included, "Nakamoto node failed to include the transfer tx" ); assert!(tip.anchored_header.as_stacks_nakamoto().is_some()); - assert!(tip.stacks_block_height >= block_height_pre_3_0 + 15); + assert!(tip.stacks_block_height >= block_height_pre_3_0 + 17); // make sure prometheus returns an updated height #[cfg(feature = "monitoring_prom")]