diff --git a/stacks-common/src/types/mod.rs b/stacks-common/src/types/mod.rs index e6e5cf5f79..1e0a056c21 100644 --- a/stacks-common/src/types/mod.rs +++ b/stacks-common/src/types/mod.rs @@ -151,6 +151,28 @@ impl StacksEpochId { StacksEpochId::Epoch30 => MINING_COMMITMENT_FREQUENCY_NAKAMOTO, } } + + /// Does this epoch use the nakamoto reward set, or the epoch2 reward set? + /// We use the epoch2 reward set in all pre-3.0 epochs. + /// We also use the epoch2 reward set in the first 3.0 reward cycle. + /// After that, we use the nakamoto reward set. + pub fn uses_nakamoto_reward_set( + &self, + cur_reward_cycle: u64, + first_epoch30_reward_cycle: u64, + ) -> bool { + match self { + StacksEpochId::Epoch10 + | StacksEpochId::Epoch20 + | StacksEpochId::Epoch2_05 + | StacksEpochId::Epoch21 + | StacksEpochId::Epoch22 + | StacksEpochId::Epoch23 + | StacksEpochId::Epoch24 + | StacksEpochId::Epoch25 => false, + StacksEpochId::Epoch30 => cur_reward_cycle > first_epoch30_reward_cycle, + } + } } impl std::fmt::Display for StacksEpochId { diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 0247a54512..52fef1a4f7 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -551,16 +551,18 @@ impl Burnchain { .reward_cycle_to_block_height(self.first_block_height, reward_cycle) } - pub fn next_reward_cycle(&self, block_height: u64) -> Option { + /// Compute the reward cycle ID of the PoX reward set which is active as of this burn_height. + /// The reward set is calculated at reward cycle index 1, so if this block height is at or after + /// reward cycle index 1, then this behaves like `block_height_to_reward_cycle()`. However, + /// if it's reward cycle index is 0, then it belongs to the previous reward cycle. + pub fn pox_reward_cycle(&self, block_height: u64) -> Option { let cycle = self.block_height_to_reward_cycle(block_height)?; let effective_height = block_height.checked_sub(self.first_block_height)?; - let next_bump = if effective_height % u64::from(self.pox_constants.reward_cycle_length) == 0 - { - 0 + if effective_height % u64::from(self.pox_constants.reward_cycle_length) == 0 { + Some(cycle.saturating_sub(1)) } else { - 1 - }; - Some(cycle + next_bump) + Some(cycle) + } } pub fn block_height_to_reward_cycle(&self, block_height: u64) -> Option { diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 633588faf0..a3550cb0ea 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -43,8 +43,6 @@ use stacks_common::util::hash::{hex_bytes, to_hex, Hash160, Sha512Trunc256Sum}; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; use stacks_common::util::vrf::*; use stacks_common::util::{get_epoch_time_secs, log}; -use wsts::common::Signature as WSTSSignature; -use wsts::curve::point::{Compressed, Point}; use crate::burnchains::affirmation::{AffirmationMap, AffirmationMapEntry}; use crate::burnchains::bitcoin::BitcoinNetworkType; @@ -1860,80 +1858,6 @@ impl<'a> SortitionHandleConn<'a> { SortitionHandleConn::open_reader(connection, &sn.sortition_id) } - /// Does the sortition db expect to receive blocks - /// signed by this signer set? - /// - /// This only works if `consensus_hash` is within two reward cycles (4200 blocks) of the - /// sortition pointed to by this handle's sortiton tip. If it isn't, then this - /// method returns Ok(false). This is to prevent a DDoS vector whereby compromised stale - /// Signer keys can be used to blast out lots of Nakamoto blocks that will be accepted - /// but never processed. So, `consensus_hash` can be in the same reward cycle as - /// `self.context.chain_tip`, or the previous, but no earlier. - pub fn expects_signer_signature( - &self, - consensus_hash: &ConsensusHash, - signer_signature: &WSTSSignature, - message: &[u8], - aggregate_public_key: &Point, - ) -> Result { - let sn = SortitionDB::get_block_snapshot(self, &self.context.chain_tip)? - .ok_or(db_error::NotFoundError) - .map_err(|e| { - warn!("No sortition for tip: {:?}", &self.context.chain_tip); - e - })?; - - let ch_sn = SortitionDB::get_block_snapshot_consensus(self, consensus_hash)? - .ok_or(db_error::NotFoundError) - .map_err(|e| { - warn!("No sortition for consensus hash: {:?}", consensus_hash); - e - })?; - - if ch_sn.block_height - + u64::from(self.context.pox_constants.reward_cycle_length) - + u64::from(self.context.pox_constants.prepare_length) - < sn.block_height - { - // too far in the past - debug!("Block with consensus hash {} is too far in the past", consensus_hash; - "consensus_hash" => %consensus_hash, - "block_height" => ch_sn.block_height, - "tip_block_height" => sn.block_height - ); - return Ok(false); - } - - // this given consensus hash must be an ancestor of our chain tip - let ch_at = self - .get_consensus_at(ch_sn.block_height)? - .ok_or(db_error::NotFoundError) - .map_err(|e| { - warn!("No ancestor consensus hash"; - "tip" => %self.context.chain_tip, - "consensus_hash" => %consensus_hash, - "consensus_hash height" => %ch_sn.block_height - ); - e - })?; - - if ch_at != ch_sn.consensus_hash { - // not an ancestor - warn!("Consensus hash is not an ancestor of the sortition tip"; - "tip" => %self.context.chain_tip, - "consensus_hash" => %consensus_hash - ); - return Err(db_error::NotFoundError); - } - - // is this consensus hash in this fork? - if SortitionDB::get_burnchain_header_hash_by_consensus(self, consensus_hash)?.is_none() { - return Ok(false); - } - - Ok(signer_signature.verify(aggregate_public_key, message)) - } - pub fn get_reward_set_size_at(&self, sortition_id: &SortitionId) -> Result { self.get_indexed(sortition_id, &db_keys::pox_reward_set_size()) .map(|x| { @@ -1984,32 +1908,6 @@ impl<'a> SortitionHandleConn<'a> { Ok(anchor_block_txid) } - /// Get the last processed reward cycle. - /// Since we always process a RewardSetInfo at the start of a reward cycle (anchor block or - /// no), this is simply the same as asking which reward cycle this SortitionHandleConn's - /// sortition tip is in. - pub fn get_last_processed_reward_cycle(&self) -> Result { - let sn = SortitionDB::get_block_snapshot(self, &self.context.chain_tip)? - .ok_or(db_error::NotFoundError)?; - let rc = self - .context - .pox_constants - .block_height_to_reward_cycle(self.context.first_block_height, sn.block_height) - .expect("FATAL: sortition from before system start"); - let rc_start_block = self - .context - .pox_constants - .reward_cycle_to_block_height(self.context.first_block_height, rc); - let last_rc = if sn.block_height >= rc_start_block { - rc - } else { - // NOTE: the reward cycle is "processed" at reward cycle index 1, not index 0 - rc.saturating_sub(1) - }; - - Ok(last_rc) - } - pub fn get_reward_cycle_unlocks( &mut self, cycle: u64, @@ -3314,11 +3212,18 @@ impl SortitionDB { ) -> Result<(), db_error> { let pox_constants = self.pox_constants.clone(); for rc in 0..=(canonical_tip.block_height / u64::from(pox_constants.reward_cycle_length)) { - if pox_constants.reward_cycle_to_block_height(self.first_block_height, rc) - > canonical_tip.block_height - { + let rc_start = pox_constants.reward_cycle_to_block_height(self.first_block_height, rc); + if rc_start > canonical_tip.block_height { break; } + let epoch_at_height = SortitionDB::get_stacks_epoch(self.conn(), rc_start)? + .unwrap_or_else(|| panic!("FATAL: no epoch defined for burn height {}", rc_start)) + .epoch_id; + + if epoch_at_height >= StacksEpochId::Epoch30 { + break; + } + info!("Regenerating reward set for cycle {}", &rc); migrator.regenerate_reward_cycle_info(self, rc)?; } @@ -3535,21 +3440,45 @@ impl SortitionDB { } /// Store a pre-processed reward set. - /// `sortition_id` is the first sortition ID of the prepare phase + /// `sortition_id` is the first sortition ID of the prepare phase. + /// No-op if the reward set has a selected-and-unknown anchor block. pub fn store_preprocessed_reward_set( sort_tx: &mut DBTx, sortition_id: &SortitionId, rc_info: &RewardCycleInfo, ) -> Result<(), db_error> { + if !rc_info.is_reward_info_known() { + return Ok(()); + } let sql = "REPLACE INTO preprocessed_reward_sets (sortition_id,reward_set) VALUES (?1,?2)"; let rc_json = serde_json::to_string(rc_info).map_err(db_error::SerializationError)?; - let args: &[&dyn ToSql] = &[sortition_id, &rc_json]; + let args = rusqlite::params![sortition_id, &rc_json]; sort_tx.execute(sql, args)?; Ok(()) } + /// Get the prepare phase end sortition ID of a reward cycle. This is the last prepare + /// phase sortition for the prepare phase that began this reward cycle (i.e. the returned + /// sortition will be in the preceding reward cycle) + /// Wrapper around SortitionDBConn::get_prepare_phase_end_sortition_id_for_reward_ccyle() + pub fn get_prepare_phase_end_sortition_id_for_reward_cycle( + &self, + tip: &SortitionId, + reward_cycle_id: u64, + ) -> Result { + self.index_conn() + .get_prepare_phase_end_sortition_id_for_reward_cycle( + &self.pox_constants, + self.first_block_height, + tip, + reward_cycle_id, + ) + } + + /// Get the prepare phase start sortition ID of a reward cycle. This is the first prepare + /// phase sortition for the prepare phase that began this reward cycle (i.e. the returned + /// sortition will be in the preceding reward cycle) /// Wrapper around SortitionDBConn::get_prepare_phase_start_sortition_id_for_reward_cycle(). - /// See that method for details. pub fn get_prepare_phase_start_sortition_id_for_reward_cycle( &self, tip: &SortitionId, @@ -3564,8 +3493,11 @@ impl SortitionDB { ) } + /// Figure out the reward cycle for `tip` and lookup the preprocessed + /// reward set (if it exists) for the active reward cycle during `tip`. + /// Returns the reward cycle info on success. + /// Returns Error on DB errors, as well as if the reward set is not yet processed. /// Wrapper around SortitionDBConn::get_preprocessed_reward_set_for_reward_cycle(). - /// See that method for details. pub fn get_preprocessed_reward_set_for_reward_cycle( &self, tip: &SortitionId, @@ -3580,8 +3512,11 @@ impl SortitionDB { ) } + /// Figure out the reward cycle for `tip` and lookup the preprocessed + /// reward set (if it exists) for the active reward cycle during `tip`. + /// Returns the reward cycle info on success. + /// Returns Error on DB errors, as well as if the reward set is not yet processed. /// Wrapper around SortitionDBConn::get_preprocessed_reward_set_of(). - /// See that method for details. pub fn get_preprocessed_reward_set_of( &self, tip: &SortitionId, @@ -3859,12 +3794,8 @@ impl<'a> SortitionDBConn<'a> { db_error::NotFoundError })?; - // NOTE: the .saturating_sub(1) is necessary because the reward set is calculated in epoch - // 2.5 and lower at reward cycle index 1, not 0. This correction ensures that the last - // block is checked against the signers who were active just before the new reward set is - // calculated. let reward_cycle_id = pox_constants - .block_height_to_reward_cycle(first_block_height, tip_sn.block_height.saturating_sub(1)) + .block_height_to_reward_cycle(first_block_height, tip_sn.block_height) .expect("FATAL: stored snapshot with block height < first_block_height"); self.get_preprocessed_reward_set_for_reward_cycle( @@ -3876,6 +3807,33 @@ impl<'a> SortitionDBConn<'a> { .and_then(|(reward_cycle_info, _anchor_sortition_id)| Ok(reward_cycle_info)) } + /// Get the prepare phase end sortition ID of a reward cycle. This is the last prepare + /// phase sortition for the prepare phase that began this reward cycle (i.e. the returned + /// sortition will be in the preceding reward cycle) + pub fn get_prepare_phase_end_sortition_id_for_reward_cycle( + &self, + pox_constants: &PoxConstants, + first_block_height: u64, + tip: &SortitionId, + reward_cycle_id: u64, + ) -> Result { + let prepare_phase_end = pox_constants + .reward_cycle_to_block_height(first_block_height, reward_cycle_id) + .saturating_sub(1); + + let last_sortition = + get_ancestor_sort_id(self, prepare_phase_end, tip)?.ok_or_else(|| { + error!( + "Could not find prepare phase end ancestor while fetching reward set"; + "tip_sortition_id" => %tip, + "reward_cycle_id" => reward_cycle_id, + "prepare_phase_end_height" => prepare_phase_end + ); + db_error::NotFoundError + })?; + Ok(last_sortition) + } + /// Get the prepare phase start sortition ID of a reward cycle. This is the first prepare /// phase sortition for the prepare phase that began this reward cycle (i.e. the returned /// sortition will be in the preceding reward cycle) @@ -6101,16 +6059,6 @@ impl<'a> SortitionHandleTx<'a> { keys.push(db_keys::pox_affirmation_map().to_string()); values.push(cur_affirmation_map.encode()); - if cfg!(test) { - // last reward cycle. - // NOTE: We keep this only for testing, since this is what the original (but - // unmigratable code) did, and we need to verify that the compatibility fix to - // SortitionDB::get_last_processed_reward_cycle() is semantically compatible - // with querying this key. - keys.push(db_keys::last_reward_cycle_key().to_string()); - values.push(db_keys::last_reward_cycle_to_string(_reward_cycle)); - } - pox_payout_addrs } else { // if this snapshot consumed some reward set entries AND @@ -6193,15 +6141,6 @@ impl<'a> SortitionHandleTx<'a> { keys.push(db_keys::pox_last_selected_anchor_txid().to_string()); values.push("".to_string()); - if cfg!(test) { - // NOTE: We keep this only for testing, since this is what the original (but - // unmigratable code) did, and we need to verify that the compatibility fix to - // SortitionDB::get_last_processed_reward_cycle() is semantically compatible - // with querying this key. - keys.push(db_keys::last_reward_cycle_key().to_string()); - values.push(db_keys::last_reward_cycle_to_string(0)); - } - // no payouts vec![] }; @@ -6543,30 +6482,6 @@ pub mod tests { use crate::core::{StacksEpochExtension, *}; use crate::util_lib::db::Error as db_error; - impl<'a> SortitionHandleConn<'a> { - /// At one point in the development lifecycle, this code depended on a MARF key/value - /// pair to map the sortition tip to the last-processed reward cycle number. This data would - /// not have been present in epoch 2.4 chainstate and earlier, but would have been present in - /// epoch 2.5 and later, since at the time it was expected that all nodes would perform a - /// genesis sync when booting into epoch 2.5. However, that requirement changed at the last - /// minute, so this code was reworked to avoid the need for the MARF key. But to ensure that - /// this method is semantically consistent with the old code (which the Nakamoto chains - /// coordinator depends on), this code will test that the new reward cycle calculation matches - /// the old reward cycle calculation. - #[cfg(test)] - pub fn legacy_get_last_processed_reward_cycle(&self) -> Result { - // verify that this is semantically compatible with the older behavior, which shipped - // for epoch 2.5 but needed to be removed at the last minute in order to support a - // migration path from 2.4 chainstate to 2.5/3.0 chainstate. - let encoded_rc = self - .get_indexed(&self.context.chain_tip, &db_keys::last_reward_cycle_key())? - .expect("FATAL: no last-processed reward cycle"); - - let expected_rc = db_keys::last_reward_cycle_from_string(&encoded_rc); - Ok(expected_rc) - } - } - impl<'a> SortitionHandleTx<'a> { /// Update the canonical Stacks tip (testing only) pub fn test_update_canonical_stacks_tip( diff --git a/stackslib/src/chainstate/burn/operations/leader_block_commit.rs b/stackslib/src/chainstate/burn/operations/leader_block_commit.rs index 087a3e3b42..539181f9af 100644 --- a/stackslib/src/chainstate/burn/operations/leader_block_commit.rs +++ b/stackslib/src/chainstate/burn/operations/leader_block_commit.rs @@ -662,7 +662,7 @@ impl LeaderBlockCommitOp { check_recipients.sort(); let mut commit_outs = self.commit_outs.clone(); commit_outs.sort(); - for (expected_commit, found_commit) in + for (found_commit, expected_commit) in commit_outs.iter().zip(check_recipients) { if expected_commit.to_burnchain_repr() diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index f34e21d1bd..b3e170987e 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -752,6 +752,7 @@ pub fn get_reward_cycle_info( ) -> Result, Error> { let epoch_at_height = SortitionDB::get_stacks_epoch(sort_db.conn(), burn_height)? .unwrap_or_else(|| panic!("FATAL: no epoch defined for burn height {}", burn_height)); + if !burnchain.is_reward_cycle_start(burn_height) { return Ok(None); } @@ -830,7 +831,8 @@ pub fn get_reward_cycle_info( }; // cache the reward cycle info as of the first sortition in the prepare phase, so that - // the Nakamoto epoch can go find it later + // the first Nakamoto epoch can go find it later. Subsequent Nakamoto epochs will use the + // reward set stored to the Nakamoto chain state. let ic = sort_db.index_handle(sortition_tip); let prev_reward_cycle = burnchain .block_height_to_reward_cycle(burn_height) @@ -3530,6 +3532,7 @@ impl SortitionDBMigrator { .pox_constants .reward_cycle_to_block_height(sort_db.first_block_height, reward_cycle) .saturating_sub(1); + let sort_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn())?; let ancestor_sn = { diff --git a/stackslib/src/chainstate/nakamoto/coordinator/mod.rs b/stackslib/src/chainstate/nakamoto/coordinator/mod.rs index f399615c80..c8d1adb826 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/mod.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/mod.rs @@ -28,7 +28,7 @@ use stacks_common::types::{StacksEpoch, StacksEpochId}; use crate::burnchains::db::{BurnchainBlockData, BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{Burnchain, BurnchainBlockHeader}; -use crate::chainstate::burn::db::sortdb::SortitionDB; +use crate::chainstate::burn::db::sortdb::{get_ancestor_sort_id, SortitionDB}; use crate::chainstate::burn::operations::leader_block_commit::RewardSetInfo; use crate::chainstate::burn::BlockSnapshot; use crate::chainstate::coordinator::comm::{ @@ -42,7 +42,7 @@ use crate::chainstate::coordinator::{ }; use crate::chainstate::nakamoto::NakamotoChainState; use crate::chainstate::stacks::boot::{RewardSet, SIGNERS_NAME}; -use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState}; +use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo}; use crate::chainstate::stacks::miner::{signal_mining_blocked, signal_mining_ready, MinerStatus}; use crate::chainstate::stacks::Error as ChainstateError; use crate::cost_estimates::{CostEstimator, FeeEstimator}; @@ -200,9 +200,13 @@ fn find_prepare_phase_sortitions( Ok(sns) } -/// Try to get the reward cycle information for a Nakamoto reward cycle. +/// Try to get the reward cycle information for a Nakamoto reward cycle, identified by the +/// burn_height. The reward cycle info returned will be from the reward cycle that is active as of +/// `burn_height`. `sortition_tip` can be any sortition ID that's at a higher height than +/// `burn_height`. +/// /// In Nakamoto, the PoX anchor block for reward cycle _R_ is the _first_ Stacks block mined in the -/// _last_ tenure of _R - 1_'s reward phase phase (i.e. which takes place toward the end of reward cycle). +/// _last_ tenure of _R - 1_'s reward phase (i.e. which takes place toward the end of reward cycle). /// The reason it must be this way is because its hash will be in the block-commit for the first /// prepare-phase tenure of cycle _R_ (which is required for the PoX ancestry query in the /// block-commit validation logic). @@ -210,14 +214,9 @@ fn find_prepare_phase_sortitions( /// If this method returns None, the caller should try again when there are more Stacks blocks. In /// Nakamoto, every reward cycle _must_ have a PoX anchor block; otherwise, the chain halts. /// -/// N.B. this method assumes that the prepare phase is comprised _solely_ of Nakamoto tenures. It -/// will not work if any of the prepare-phase tenures are from epoch 2.x. -/// /// Returns Ok(Some(reward-cycle-info)) if we found the first sortition in the prepare phase. /// Returns Ok(None) if we're still waiting for the PoX anchor block sortition /// Returns Err(Error::NotInPreparePhase) if `burn_height` is not in the prepare phase -/// Returns Err(Error::RewardCycleAlreadyProcessed) if the reward set for this reward cycle has -/// already been processed. pub fn get_nakamoto_reward_cycle_info( burn_height: u64, sortition_tip: &SortitionId, @@ -235,15 +234,10 @@ pub fn get_nakamoto_reward_cycle_info( "FATAL: called a nakamoto function outside of epoch 3" ); - if !burnchain.is_in_prepare_phase(burn_height) { - return Err(Error::NotInPreparePhase); - } - - // calculating the reward set for the _next_ reward cycle + // calculating the reward set for the current reward cycle let reward_cycle = burnchain - .next_reward_cycle(burn_height) + .pox_reward_cycle(burn_height) .expect("FATAL: no reward cycle for burn height"); - let reward_start_height = burnchain.reward_cycle_to_block_height(reward_cycle); debug!("Processing reward set for Nakamoto reward cycle"; "burn_height" => burn_height, @@ -251,26 +245,155 @@ pub fn get_nakamoto_reward_cycle_info( "reward_cycle_length" => burnchain.pox_constants.reward_cycle_length, "prepare_phase_length" => burnchain.pox_constants.prepare_length); + let Some((rc_info, anchor_block_header)) = load_nakamoto_reward_set( + reward_cycle, + sortition_tip, + burnchain, + chain_state, + sort_db, + provider, + )? + else { + return Ok(None); + }; + + let block_id = match anchor_block_header.anchored_header { + StacksBlockHeaderTypes::Epoch2(..) => anchor_block_header.index_block_hash(), + StacksBlockHeaderTypes::Nakamoto(ref header) => header.block_id(), + }; + + info!( + "Anchor block selected"; + "cycle" => reward_cycle, + "block_id" => %block_id, + "consensus_hash" => %anchor_block_header.consensus_hash, + "burn_height" => anchor_block_header.burn_header_height, + "anchor_chain_tip" => %anchor_block_header.index_block_hash(), + "anchor_chain_tip_height" => %anchor_block_header.burn_header_height, + ); + + return Ok(Some(rc_info)); +} + +/// Helper to get the Nakamoto reward set for a given reward cycle, identified by `reward_cycle`. +/// +/// In all but the first Nakamoto reward cycle, this will load up the stored reward set from the +/// Nakamoto chain state. In the first Nakamoto reward cycle, where the reward set is computed +/// from epoch2 state, the reward set will be loaded from the sortition DB (which is the only place +/// it will be stored). +/// +/// Returns Ok(Some((reward set info, PoX anchor block header))) on success +/// Returns Ok(None) if the reward set is not yet known, but could be known by the time a +/// subsequent call is made. +pub fn load_nakamoto_reward_set( + reward_cycle: u64, + sortition_tip: &SortitionId, + burnchain: &Burnchain, + chain_state: &mut StacksChainState, + sort_db: &SortitionDB, + provider: &U, +) -> Result, Error> { + let prepare_end_height = burnchain + .reward_cycle_to_block_height(reward_cycle) + .saturating_sub(1); + + let epoch_at_height = SortitionDB::get_stacks_epoch(sort_db.conn(), prepare_end_height)? + .unwrap_or_else(|| { + panic!( + "FATAL: no epoch defined for burn height {}", + prepare_end_height + ) + }); + + let Some(prepare_end_sortition_id) = + get_ancestor_sort_id(&sort_db.index_conn(), prepare_end_height, sortition_tip)? + else { + // reward cycle is too far in the future + warn!("Requested reward cycle start ancestor sortition ID for cycle {} prepare-end height {}, but tip is {}", reward_cycle, prepare_end_height, sortition_tip); + return Ok(None); + }; + // Find the first Stacks block in this reward cycle's preceding prepare phase. // This block will have invoked `.signers.stackerdb-set-signer-slots()` with the reward set. // Note that we may not have processed it yet. But, if we do find it, then it's // unique (and since Nakamoto Stacks blocks are processed in order, the anchor block // cannot change later). - let prepare_phase_sortitions = - find_prepare_phase_sortitions(sort_db, burnchain, sortition_tip)?; - - // did we already calculate the reward cycle info? If so, then return it. - let first_sortition_id = if let Some(first_sn) = prepare_phase_sortitions.first() { - if let Some(persisted_reward_cycle_info) = - SortitionDB::get_preprocessed_reward_set(sort_db.conn(), &first_sn.sortition_id)? + let first_epoch30_reward_cycle = burnchain + .pox_reward_cycle(epoch_at_height.start_height) + .expect("FATAL: no reward cycle for epoch 3.0 start height"); + + if !epoch_at_height + .epoch_id + .uses_nakamoto_reward_set(reward_cycle, first_epoch30_reward_cycle) + { + // in epoch 2.5, and in the first reward cycle of epoch 3.0, the reward set can *only* be found in the sortition DB. + // The nakamoto chain-processing rules aren't active yet, so we can't look for the reward + // cycle info in the nakamoto chain state. + if let Ok(persisted_reward_cycle_info) = + sort_db.get_preprocessed_reward_set_of(&prepare_end_sortition_id) { - return Ok(Some(persisted_reward_cycle_info)); + if persisted_reward_cycle_info + .known_selected_anchor_block() + .is_none() + { + debug!("No reward set known yet for prepare phase"; + "sortition_tip" => %sortition_tip, + "prepare_end_sortition_id" => %prepare_end_sortition_id); + return Ok(None); + } + + // find the corresponding Stacks anchor block header + let Some((anchor_block_hash, _)) = persisted_reward_cycle_info.selected_anchor_block() + else { + // should be unreachable + error!("No anchor block known for persisted reward set"; + "sortition_tip" => %sortition_tip, + "prepare_end_sortition_id" => %prepare_end_sortition_id); + return Ok(None); + }; + + let ic = sort_db.index_conn(); + let Some(anchor_block_snapshot) = + SortitionDB::get_block_snapshot_for_winning_stacks_block( + &ic, + &prepare_end_sortition_id, + anchor_block_hash, + )? + else { + // should be unreachable + error!("No ancestor block snapshot for anchor block"; + "anchor_block_hash" => %anchor_block_hash, + "sortition_tip" => %sortition_tip, + "prepare_end_sortition_id" => %prepare_end_sortition_id); + + return Ok(None); + }; + + let Some(anchor_block_header) = + StacksChainState::get_stacks_block_header_info_by_consensus_hash( + chain_state.db(), + &anchor_block_snapshot.consensus_hash, + )? + else { + // should be unreachable + error!("No block header for anchor block"; + "consensus_hash" => %anchor_block_snapshot.consensus_hash, + "anchor_block_hash" => %anchor_block_hash); + return Ok(None); + }; + + debug!("Loaded reward set calculated in epoch 2.5 for reward cycle {} (which is in epoch {})", reward_cycle, epoch_at_height.epoch_id); + return Ok(Some((persisted_reward_cycle_info, anchor_block_header))); } - first_sn.sortition_id.clone() - } else { - // can't do anything + + // no reward set known yet. It's possible that it simply hasn't been processed yet. + debug!("No pre-processed PoX reward set known for pre-Nakamoto cycle {reward_cycle}"); return Ok(None); - }; + } + + // find the reward cycle's prepare-phase sortitions (in the preceding reward cycle) + let prepare_phase_sortitions = + find_prepare_phase_sortitions(sort_db, burnchain, &prepare_end_sortition_id)?; // iterate over the prepare_phase_sortitions, finding the first such sortition // with a processed stacks block @@ -317,7 +440,7 @@ pub fn get_nakamoto_reward_cycle_info( .expect("FATAL: no snapshot for winning PoX anchor block"); // make sure the `anchor_block` field is the same as whatever goes into the block-commit, - // or PoX ancestry queries won't work + // or PoX ancestry queries won't work. let (block_id, stacks_block_hash) = match anchor_block_header.anchored_header { StacksBlockHeaderTypes::Epoch2(ref header) => ( StacksBlockId::new(&anchor_block_header.consensus_hash, &header.block_hash()), @@ -330,19 +453,16 @@ pub fn get_nakamoto_reward_cycle_info( let txid = anchor_block_sn.winning_block_txid; - info!( - "Anchor block selected"; - "cycle" => reward_cycle, - "block_id" => %block_id, - "consensus_hash" => %anchor_block_header.consensus_hash, - "burn_height" => anchor_block_header.burn_header_height, - "anchor_chain_tip" => %anchor_block_header.index_block_hash(), - "anchor_chain_tip_height" => %anchor_block_header.burn_header_height, - "first_prepare_sortition_id" => %first_sortition_id - ); + test_debug!("Stacks anchor block found"; + "block_id" => %block_id, + "block_hash" => %stacks_block_hash, + "consensus_hash" => %anchor_block_sn.consensus_hash, + "txid" => %txid, + "prepare_end_height" => %prepare_end_height, + "burnchain_height" => %anchor_block_sn.block_height); let reward_set = provider.get_reward_set_nakamoto( - reward_start_height, + prepare_end_height.saturating_sub(1), chain_state, burnchain, sort_db, @@ -358,13 +478,7 @@ pub fn get_nakamoto_reward_cycle_info( reward_cycle, anchor_status, }; - - // persist this - let mut tx = sort_db.tx_begin()?; - SortitionDB::store_preprocessed_reward_set(&mut tx, &first_sortition_id, &rc_info)?; - tx.commit()?; - - return Ok(Some(rc_info)); + Ok(Some((rc_info, anchor_block_header))) } /// Get the next PoX recipients in the Nakamoto epoch. @@ -375,39 +489,28 @@ pub fn get_nakamoto_reward_cycle_info( pub fn get_nakamoto_next_recipients( sortition_tip: &BlockSnapshot, sort_db: &mut SortitionDB, + chain_state: &mut StacksChainState, burnchain: &Burnchain, ) -> Result, Error> { - let reward_cycle_info = if burnchain.is_reward_cycle_start(sortition_tip.block_height + 1) { - // load up new reward cycle info so we can start using *that* - let prepare_phase_sortitions = - find_prepare_phase_sortitions(sort_db, burnchain, &sortition_tip.parent_sortition_id)?; - - // NOTE: this must panic because Nakamoto's first reward cycle has stackers - let first_sn = prepare_phase_sortitions - .first() - .expect("FATAL: unreachable: no prepare-phase sortitions at start of reward cycle"); - - debug!("Get pre-processed reward set"; - "sortition_id" => %first_sn.sortition_id); - - // NOTE: don't panic here. The only caller of this method is a stacks-node miner, - // and they *may* have invoked this before they've processed the prepare phase. - // That's recoverable by simply waiting to mine until they've processed those - // blocks. - let reward_set = - SortitionDB::get_preprocessed_reward_set(sort_db.conn(), &first_sn.sortition_id)? - .ok_or_else(|| { - warn!( - "No preprocessed reward set found"; - "reward_cycle_start" => sortition_tip.block_height + 1, - "first_prepare_sortition_id" => %first_sn.sortition_id - ); - Error::PoXNotProcessedYet - })?; - Some(reward_set) - } else { - None - }; + let reward_cycle_info = + if burnchain.is_reward_cycle_start(sortition_tip.block_height.saturating_add(1)) { + let Some((reward_set, _)) = load_nakamoto_reward_set( + burnchain + .pox_reward_cycle(sortition_tip.block_height.saturating_add(1)) + .expect("Sortition block height has no reward cycle"), + &sortition_tip.sortition_id, + burnchain, + chain_state, + sort_db, + &OnChainRewardSetProvider::new(), + )? + else { + return Ok(None); + }; + Some(reward_set) + } else { + None + }; sort_db .get_next_block_recipients(burnchain, sortition_tip, reward_cycle_info.as_ref()) .map_err(Error::from) @@ -465,9 +568,20 @@ impl< .expect("FATAL: epoch3 block height has no reward cycle"); // only proceed if we have processed the _anchor block_ for this reward cycle - let handle_conn = self.sortition_db.index_handle(&canonical_sortition_tip); - let last_processed_rc = handle_conn.get_last_processed_reward_cycle()?; - Ok(last_processed_rc >= first_epoch3_reward_cycle) + let Some((rc_info, _)) = load_nakamoto_reward_set( + self.burnchain + .pox_reward_cycle(canonical_sn.block_height) + .expect("FATAL: snapshot has no reward cycle"), + &canonical_sn.sortition_id, + &self.burnchain, + &mut self.chain_state_db, + &self.sortition_db, + &OnChainRewardSetProvider::new(), + )? + else { + return Ok(false); + }; + Ok(rc_info.reward_cycle >= first_epoch3_reward_cycle) } /// This is the main loop body for the coordinator in epoch 3. @@ -707,8 +821,26 @@ impl< }); let last_processed_reward_cycle = { - let ic = self.sortition_db.index_handle(&canonical_sortition_tip); - ic.get_last_processed_reward_cycle()? + let canonical_sn = SortitionDB::get_block_snapshot( + &self.sortition_db.conn(), + &canonical_sortition_tip, + )? + .ok_or(DBError::NotFoundError)?; + let Some((rc_info, _)) = load_nakamoto_reward_set( + self.burnchain + .pox_reward_cycle(canonical_sn.block_height) + .expect("FATAL: snapshot has no reward cycle"), + &canonical_sn.sortition_id, + &self.burnchain, + &mut self.chain_state_db, + &self.sortition_db, + &OnChainRewardSetProvider::new(), + )? + else { + // no anchor block yet, so try processing another block + continue; + }; + rc_info.reward_cycle }; if last_processed_reward_cycle > current_reward_cycle { @@ -856,42 +988,22 @@ impl< } }; - if self.burnchain.is_in_prepare_phase(header.block_height) { - // try to eagerly load up the reward cycle information, so we can persist it and - // make it available to signers. If we're at the _end_ of the prepare phase, then - // we have no choice but to block. - let reward_cycle_info = self.get_nakamoto_reward_cycle_info(header.block_height)?; - if let Some(rc_info) = reward_cycle_info { - // in nakamoto, if we have any reward cycle info at all, it will be known. - assert!( - rc_info.known_selected_anchor_block().is_some(), - "FATAL: unknown PoX anchor block in Nakamoto" - ); - } - } - let reward_cycle_info = if self.burnchain.is_reward_cycle_start(header.block_height) { // we're at the end of the prepare phase, so we'd better have obtained the reward // cycle info of we must block. - // N.B. it's `- 2` because `is_reward_cycle_start` implies that `block_height % reward_cycle_length == 1`, - // but this call needs `block_height % reward_cycle_length == reward_cycle_length - 1` -- i.e. `block_height` - // must be the last block height in the last reward cycle. - let end_cycle_block_height = header.block_height.saturating_sub(2); - let reward_cycle_info = - self.get_nakamoto_reward_cycle_info(end_cycle_block_height)?; + let reward_cycle_info = self.get_nakamoto_reward_cycle_info(header.block_height)?; if let Some(rc_info) = reward_cycle_info.as_ref() { // in nakamoto, if we have any reward cycle info at all, it will be known. // otherwise, we may have to process some more Stacks blocks if rc_info.known_selected_anchor_block().is_none() { - warn!("Unknown PoX anchor block in Nakamoto (at height {}). Refusing to process more burnchain blocks until that changes.", end_cycle_block_height); + warn!("Unknown PoX anchor block in Nakamoto (at height {}). Refusing to process more burnchain blocks until that changes.", header.block_height); return Ok(false); } } else { // have to block -- we don't have the reward cycle information debug!("Do not yet have PoX anchor block for next reward cycle -- no anchor block found"; "next_reward_cycle" => self.burnchain.block_height_to_reward_cycle(header.block_height), - "reward_cycle_end" => end_cycle_block_height - ); + "block_height" => header.block_height); return Ok(false); } reward_cycle_info diff --git a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs index b7c0bb5ba9..a95d968f3e 100644 --- a/stackslib/src/chainstate/nakamoto/coordinator/tests.rs +++ b/stackslib/src/chainstate/nakamoto/coordinator/tests.rs @@ -400,6 +400,7 @@ fn replay_reward_cycle( info!("Process Nakamoto block {} ({:?}", &block_id, &block.header); let accepted = Relayer::process_new_nakamoto_block( + &peer.config.burnchain, &sortdb, &mut sort_handle, &mut node.chainstate, diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index c106132b34..8afed52f6a 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -2710,6 +2710,12 @@ impl NakamotoChainState { &mut clarity_tx, vote_for_agg_key_ops.clone(), )); + + if signer_set_calc.is_some() { + debug!("Setup block: computed reward set for the next reward cycle"; + "anchor_block_height" => coinbase_height, + "burn_header_height" => burn_header_height); + } } else { signer_set_calc = None; } diff --git a/stackslib/src/chainstate/nakamoto/signer_set.rs b/stackslib/src/chainstate/nakamoto/signer_set.rs index a7e8df6ed0..b6c0aefaa1 100644 --- a/stackslib/src/chainstate/nakamoto/signer_set.rs +++ b/stackslib/src/chainstate/nakamoto/signer_set.rs @@ -216,6 +216,8 @@ impl NakamotoSigners { Ok(slots) } + /// Compute the reward set for the next reward cycle, store it, and write it to the .signers + /// contract. `reward_cycle` is the _current_ reward cycle. pub fn handle_signer_stackerdb_update( clarity: &mut ClarityTransactionConnection, pox_constants: &PoxConstants, @@ -350,6 +352,11 @@ impl NakamotoSigners { Ok(SignerCalculation { events, reward_set }) } + /// If this block is mined in the prepare phase, based on its tenure's `burn_tip_height`. If + /// so, and if we haven't done so yet, then compute the PoX reward set, store it, and update + /// the .signers contract. The stored PoX reward set is the reward set for the next reward + /// cycle, and will be used by the Nakamoto chains coordinator to validate its block-commits + /// and block signatures. pub fn check_and_handle_prepare_phase_start( clarity_tx: &mut ClarityTx, first_block_height: u64, diff --git a/stackslib/src/chainstate/nakamoto/test_signers.rs b/stackslib/src/chainstate/nakamoto/test_signers.rs index 4a2aa4f29c..13d7f2ff1e 100644 --- a/stackslib/src/chainstate/nakamoto/test_signers.rs +++ b/stackslib/src/chainstate/nakamoto/test_signers.rs @@ -50,7 +50,6 @@ use crate::chainstate::burn::*; use crate::chainstate::coordinator::{ ChainsCoordinator, Error as CoordinatorError, OnChainRewardSetProvider, }; -use crate::chainstate::nakamoto::coordinator::get_nakamoto_next_recipients; use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder; use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; use crate::chainstate::stacks::address::PoxAddress; diff --git a/stackslib/src/chainstate/nakamoto/tests/node.rs b/stackslib/src/chainstate/nakamoto/tests/node.rs index fc425d0580..201cadb9ac 100644 --- a/stackslib/src/chainstate/nakamoto/tests/node.rs +++ b/stackslib/src/chainstate/nakamoto/tests/node.rs @@ -46,7 +46,9 @@ use crate::chainstate::burn::*; use crate::chainstate::coordinator::{ ChainsCoordinator, Error as CoordinatorError, OnChainRewardSetProvider, }; -use crate::chainstate::nakamoto::coordinator::get_nakamoto_next_recipients; +use crate::chainstate::nakamoto::coordinator::{ + get_nakamoto_next_recipients, load_nakamoto_reward_set, +}; use crate::chainstate::nakamoto::miner::NakamotoBlockBuilder; use crate::chainstate::nakamoto::test_signers::TestSigners; use crate::chainstate::nakamoto::tests::get_account; @@ -384,11 +386,12 @@ impl TestStacksNode { .unwrap(); test_debug!( - "Work in {} {} for Nakamoto parent: {},{}", + "Work in {} {} for Nakamoto parent: {},{}. Last tenure ID is {}", burn_block.block_height, burn_block.parent_snapshot.burn_header_hash, parent_sortition.total_burn, last_parent.header.chain_length + 1, + &parent_tenure_id, ); (parent_tenure_id, parent_sortition) @@ -418,11 +421,12 @@ impl TestStacksNode { let parent_tenure_id = parent_chain_tip.index_block_hash(); test_debug!( - "Work in {} {} for Stacks 2.x parent: {},{}", + "Work in {} {} for Stacks 2.x parent: {},{}. Last tenure ID is {}", burn_block.block_height, burn_block.parent_snapshot.burn_header_hash, parent_stacks_block_snapshot.total_burn, parent_chain_tip.anchored_header.height(), + &parent_tenure_id, ); (parent_tenure_id, parent_stacks_block_snapshot) @@ -579,12 +583,23 @@ impl TestStacksNode { .unwrap(); // Get the reward set - let sort_tip = SortitionDB::get_canonical_sortition_tip(sortdb.conn()).unwrap(); - let reward_set = sortdb - .get_preprocessed_reward_set_of(&sort_tip) - .expect("Failed to get reward cycle info") - .known_selected_anchor_block_owned() - .expect("Expected a reward set"); + let sort_tip_sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); + let reward_set = load_nakamoto_reward_set( + miner + .burnchain + .pox_reward_cycle(sort_tip_sn.block_height) + .expect("FATAL: no reward cycle for sortition"), + &sort_tip_sn.sortition_id, + &miner.burnchain, + chainstate, + sortdb, + &OnChainRewardSetProvider::new(), + ) + .expect("Failed to load reward set") + .expect("Expected a reward set") + .0 + .known_selected_anchor_block_owned() + .expect("Unknown reward set"); test_debug!( "Signing Nakamoto block {} in tenure {} with key in cycle {}", @@ -609,6 +624,7 @@ impl TestStacksNode { let mut sort_handle = sortdb.index_handle(&sort_tip); info!("Processing the new nakamoto block"); let accepted = match Relayer::process_new_nakamoto_block( + &miner.burnchain, sortdb, &mut sort_handle, chainstate, @@ -912,7 +928,12 @@ impl<'a> TestPeer<'a> { } // patch in reward set info - match get_nakamoto_next_recipients(&tip, &mut sortdb, &self.config.burnchain) { + match get_nakamoto_next_recipients( + &tip, + &mut sortdb, + &mut stacks_node.chainstate, + &self.config.burnchain, + ) { Ok(recipients) => { block_commit_op.commit_outs = match recipients { Some(info) => { @@ -1130,6 +1151,7 @@ impl<'a> TestPeer<'a> { let block_id = block.block_id(); debug!("Process Nakamoto block {} ({:?}", &block_id, &block.header); let accepted = Relayer::process_new_nakamoto_block( + &self.network.burnchain, &sortdb, &mut sort_handle, &mut node.chainstate, diff --git a/stackslib/src/net/download/nakamoto/download_state_machine.rs b/stackslib/src/net/download/nakamoto/download_state_machine.rs index 27b675ae49..c1f41fcd8a 100644 --- a/stackslib/src/net/download/nakamoto/download_state_machine.rs +++ b/stackslib/src/net/download/nakamoto/download_state_machine.rs @@ -63,7 +63,7 @@ use crate::net::inv::epoch2x::InvState; use crate::net::inv::nakamoto::{NakamotoInvStateMachine, NakamotoTenureInv}; use crate::net::neighbors::rpc::NeighborRPC; use crate::net::neighbors::NeighborComms; -use crate::net::p2p::PeerNetwork; +use crate::net::p2p::{CurrentRewardSet, PeerNetwork}; use crate::net::server::HttpPeer; use crate::net::{Error as NetError, Neighbor, NeighborAddress, NeighborKey}; use crate::util_lib::db::{DBConn, Error as DBError}; @@ -1154,7 +1154,7 @@ impl NakamotoDownloadStateMachine { fn update_tenure_downloaders( &mut self, count: usize, - current_reward_sets: &BTreeMap, + current_reward_sets: &BTreeMap, ) { self.tenure_downloads.make_tenure_downloaders( &mut self.tenure_download_schedule, diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader.rs b/stackslib/src/net/download/nakamoto/tenure_downloader.rs index a3586602e6..9f261929b5 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader.rs @@ -57,7 +57,7 @@ use crate::net::inv::epoch2x::InvState; use crate::net::inv::nakamoto::{NakamotoInvStateMachine, NakamotoTenureInv}; use crate::net::neighbors::rpc::NeighborRPC; use crate::net::neighbors::NeighborComms; -use crate::net::p2p::PeerNetwork; +use crate::net::p2p::{CurrentRewardSet, PeerNetwork}; use crate::net::server::HttpPeer; use crate::net::{Error as NetError, Neighbor, NeighborAddress, NeighborKey}; use crate::util_lib::db::{DBConn, Error as DBError}; diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs index 6c53ff924c..f275e83d29 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader_set.rs @@ -62,7 +62,7 @@ use crate::net::inv::epoch2x::InvState; use crate::net::inv::nakamoto::{NakamotoInvStateMachine, NakamotoTenureInv}; use crate::net::neighbors::rpc::NeighborRPC; use crate::net::neighbors::NeighborComms; -use crate::net::p2p::PeerNetwork; +use crate::net::p2p::{CurrentRewardSet, PeerNetwork}; use crate::net::server::HttpPeer; use crate::net::{Error as NetError, Neighbor, NeighborAddress, NeighborKey}; use crate::util_lib::db::{DBConn, Error as DBError}; @@ -419,7 +419,7 @@ impl NakamotoTenureDownloaderSet { available: &mut HashMap>, tenure_block_ids: &HashMap, count: usize, - current_reward_cycles: &BTreeMap, + current_reward_cycles: &BTreeMap, ) { test_debug!("schedule: {:?}", schedule); test_debug!("available: {:?}", &available); @@ -482,7 +482,7 @@ impl NakamotoTenureDownloaderSet { }; let Some(Some(start_reward_set)) = current_reward_cycles .get(&tenure_info.start_reward_cycle) - .map(|cycle_info| cycle_info.known_selected_anchor_block()) + .map(|cycle_info| cycle_info.reward_set()) else { test_debug!( "Cannot fetch tenure-start block due to no known start reward set for cycle {}: {:?}", @@ -494,7 +494,7 @@ impl NakamotoTenureDownloaderSet { }; let Some(Some(end_reward_set)) = current_reward_cycles .get(&tenure_info.end_reward_cycle) - .map(|cycle_info| cycle_info.known_selected_anchor_block()) + .map(|cycle_info| cycle_info.reward_set()) else { test_debug!( "Cannot fetch tenure-end block due to no known end reward set for cycle {}: {:?}", diff --git a/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs b/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs index 101229f7f6..2fd9599c5e 100644 --- a/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs +++ b/stackslib/src/net/download/nakamoto/tenure_downloader_unconfirmed.rs @@ -62,7 +62,7 @@ use crate::net::inv::epoch2x::InvState; use crate::net::inv::nakamoto::{NakamotoInvStateMachine, NakamotoTenureInv}; use crate::net::neighbors::rpc::NeighborRPC; use crate::net::neighbors::NeighborComms; -use crate::net::p2p::PeerNetwork; +use crate::net::p2p::{CurrentRewardSet, PeerNetwork}; use crate::net::server::HttpPeer; use crate::net::{Error as NetError, Neighbor, NeighborAddress, NeighborKey}; use crate::util_lib::db::{DBConn, Error as DBError}; @@ -186,7 +186,7 @@ impl NakamotoUnconfirmedTenureDownloader { local_sort_tip: &BlockSnapshot, chainstate: &StacksChainState, remote_tenure_tip: RPCGetTenureInfo, - current_reward_sets: &BTreeMap, + current_reward_sets: &BTreeMap, ) -> Result<(), NetError> { if self.state != NakamotoUnconfirmedDownloadState::GetTenureInfo { return Err(NetError::InvalidState); @@ -301,7 +301,7 @@ impl NakamotoUnconfirmedTenureDownloader { // get reward set info for the unconfirmed tenure and highest-complete tenure sortitions let Some(Some(confirmed_reward_set)) = current_reward_sets .get(&parent_tenure_rc) - .map(|cycle_info| cycle_info.known_selected_anchor_block()) + .map(|cycle_info| cycle_info.reward_set()) else { warn!( "No signer public keys for confirmed tenure {} (rc {})", @@ -312,7 +312,7 @@ impl NakamotoUnconfirmedTenureDownloader { let Some(Some(unconfirmed_reward_set)) = current_reward_sets .get(&tenure_rc) - .map(|cycle_info| cycle_info.known_selected_anchor_block()) + .map(|cycle_info| cycle_info.reward_set()) else { warn!( "No signer public keys for unconfirmed tenure {} (rc {})", @@ -728,7 +728,7 @@ impl NakamotoUnconfirmedTenureDownloader { sortdb: &SortitionDB, local_sort_tip: &BlockSnapshot, chainstate: &StacksChainState, - current_reward_sets: &BTreeMap, + current_reward_sets: &BTreeMap, ) -> Result>, NetError> { match &self.state { NakamotoUnconfirmedDownloadState::GetTenureInfo => { diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 1da3af6622..e2e67093e6 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -2757,6 +2757,7 @@ pub mod test { let receipts_res = self.relayer.process_network_result( self.network.get_local_peer(), &mut net_result, + &self.network.burnchain, &mut sortdb, &mut stacks_node.chainstate, &mut mempool, @@ -3885,29 +3886,12 @@ pub mod test { } /// Verify that the sortition DB migration into Nakamoto worked correctly. - /// For now, it's sufficient to check that the `get_last_processed_reward_cycle()` calculation - /// works the same across both the original and migration-compatible implementations. pub fn check_nakamoto_migration(&mut self) { let mut sortdb = self.sortdb.take().unwrap(); let mut node = self.stacks_node.take().unwrap(); let chainstate = &mut node.chainstate; let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); - for height in 0..=tip.block_height { - let sns = - SortitionDB::get_all_snapshots_by_burn_height(sortdb.conn(), height).unwrap(); - for sn in sns { - let ih = sortdb.index_handle(&sn.sortition_id); - let highest_processed_rc = ih.get_last_processed_reward_cycle().unwrap(); - let expected_highest_processed_rc = - ih.legacy_get_last_processed_reward_cycle().unwrap(); - assert_eq!( - highest_processed_rc, expected_highest_processed_rc, - "BUG: at burn height {} the highest-processed reward cycles diverge", - height - ); - } - } let epochs = SortitionDB::get_stacks_epochs(sortdb.conn()).unwrap(); let epoch_3_idx = StacksEpoch::find_epoch_by_id(&epochs, StacksEpochId::Epoch30).unwrap(); diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index ca32fe0671..4e999e1abf 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -40,13 +40,14 @@ use {mio, url}; use crate::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{Address, Burnchain, BurnchainView, PublicKey}; -use crate::chainstate::burn::db::sortdb::{BlockHeaderCache, SortitionDB}; +use crate::chainstate::burn::db::sortdb::{get_ancestor_sort_id, BlockHeaderCache, SortitionDB}; use crate::chainstate::burn::BlockSnapshot; use crate::chainstate::coordinator::{ static_get_canonical_affirmation_map, static_get_heaviest_affirmation_map, - static_get_stacks_tip_affirmation_map, RewardCycleInfo, + static_get_stacks_tip_affirmation_map, OnChainRewardSetProvider, RewardCycleInfo, }; -use crate::chainstate::stacks::boot::MINERS_NAME; +use crate::chainstate::nakamoto::coordinator::load_nakamoto_reward_set; +use crate::chainstate::stacks::boot::{RewardSet, MINERS_NAME}; use crate::chainstate::stacks::db::StacksChainState; use crate::chainstate::stacks::{StacksBlockHeader, MAX_BLOCK_LEN, MAX_TRANSACTION_LEN}; use crate::core::StacksEpoch; @@ -232,6 +233,24 @@ impl ConnectingPeer { } } +#[derive(Clone, Debug, PartialEq)] +pub struct CurrentRewardSet { + pub reward_cycle: u64, + pub reward_cycle_info: RewardCycleInfo, + pub anchor_block_consensus_hash: ConsensusHash, + pub anchor_block_hash: BlockHeaderHash, +} + +impl CurrentRewardSet { + pub fn reward_set(&self) -> Option<&RewardSet> { + self.reward_cycle_info.known_selected_anchor_block() + } + + pub fn anchor_block_id(&self) -> StacksBlockId { + StacksBlockId::new(&self.anchor_block_consensus_hash, &self.anchor_block_hash) + } +} + pub struct PeerNetwork { // constants pub peer_version: u32, @@ -258,16 +277,9 @@ pub struct PeerNetwork { /// In epoch 2.x, this is the same as the tip block ID /// In nakamoto, this is the block ID of the first block in the current tenure pub tenure_start_block_id: StacksBlockId, - /// The reward sets of the current and past reward cycle. + /// The reward sets of the past three reward cycles. /// Needed to validate blocks, which are signed by a threshold of stackers - pub current_reward_sets: BTreeMap, - /// The sortition IDs that began the prepare-phases for given reward cycles. This is used to - /// determine whether or not the reward cycle info in `current_reward_sets` is still valid -- a - /// burnchain fork may invalidate them, so the code must check that the sortition ID for the - /// start of the prepare-phase is still canonical. - /// This needs to be in 1-to-1 correspondence with `current_reward_sets` -- the sortition IDs - /// that make up the values need to correspond to the reward sets computed as of the sortition. - pub current_reward_set_ids: BTreeMap, + pub current_reward_sets: BTreeMap, // information about the state of the network's anchor blocks pub heaviest_affirmation_map: AffirmationMap, @@ -479,7 +491,6 @@ impl PeerNetwork { parent_stacks_tip: (ConsensusHash([0x00; 20]), BlockHeaderHash([0x00; 32]), 0), tenure_start_block_id: StacksBlockId([0x00; 32]), current_reward_sets: BTreeMap::new(), - current_reward_set_ids: BTreeMap::new(), peerdb: peerdb, atlasdb: atlasdb, @@ -5435,38 +5446,10 @@ impl PeerNetwork { } /// Clear out old reward cycles - fn free_old_reward_cycles( - &mut self, - sortdb: &SortitionDB, - tip_sortition_id: &SortitionId, - prev_rc: u64, - ) { + fn free_old_reward_cycles(&mut self, rc: u64) { if self.current_reward_sets.len() > 3 { self.current_reward_sets.retain(|old_rc, _| { - if (*old_rc).saturating_add(1) < prev_rc { - self.current_reward_set_ids.remove(old_rc); - test_debug!("Drop reward cycle info for cycle {}", old_rc); - return false; - } - let Some(old_sortition_id) = self.current_reward_set_ids.get(old_rc) else { - // shouldn't happen - self.current_reward_set_ids.remove(old_rc); - test_debug!("Drop reward cycle info for cycle {}", old_rc); - return false; - }; - let Ok(prepare_phase_sort_id) = sortdb - .get_prepare_phase_start_sortition_id_for_reward_cycle( - &tip_sortition_id, - *old_rc, - ) - else { - self.current_reward_set_ids.remove(old_rc); - test_debug!("Drop reward cycle info for cycle {}", old_rc); - return false; - }; - if prepare_phase_sort_id != *old_sortition_id { - // non-canonical reward cycle info - self.current_reward_set_ids.remove(old_rc); + if (*old_rc).saturating_add(2) < rc { test_debug!("Drop reward cycle info for cycle {}", old_rc); return false; } @@ -5475,10 +5458,11 @@ impl PeerNetwork { } } - /// Refresh our view of the last two reward cycles + /// Refresh our view of the last three reward cycles fn refresh_reward_cycles( &mut self, sortdb: &SortitionDB, + chainstate: &mut StacksChainState, tip_sn: &BlockSnapshot, ) -> Result<(), net_error> { let cur_rc = self @@ -5487,57 +5471,68 @@ impl PeerNetwork { .expect("FATAL: sortition from before system start"); let prev_rc = cur_rc.saturating_sub(1); + let prev_prev_rc = prev_rc.saturating_sub(1); + let ih = sortdb.index_handle(&tip_sn.sortition_id); - // keyed by both rc and sortition ID in case there's a bitcoin fork -- we'd want the - // canonical reward set to be loaded - let cur_rc_sortition_id = sortdb - .get_prepare_phase_start_sortition_id_for_reward_cycle(&tip_sn.sortition_id, cur_rc)?; - let prev_rc_sortition_id = sortdb - .get_prepare_phase_start_sortition_id_for_reward_cycle(&tip_sn.sortition_id, prev_rc)?; - - for (rc, sortition_id) in [ - (prev_rc, prev_rc_sortition_id), - (cur_rc, cur_rc_sortition_id), - ] - .into_iter() - { - if let Some(sort_id) = self.current_reward_set_ids.get(&rc) { - if sort_id == &sortition_id { - continue; - } - } - let Ok((reward_cycle_info, reward_cycle_sort_id)) = sortdb - .get_preprocessed_reward_set_for_reward_cycle(&tip_sn.sortition_id, rc) - .map_err(|e| { - warn!( - "Failed to load reward set for cycle {} ({}): {:?}", - rc, &sortition_id, &e - ); - e - }) + for rc in [cur_rc, prev_rc, prev_prev_rc] { + let rc_start_height = self.burnchain.reward_cycle_to_block_height(rc); + let Some(ancestor_sort_id) = + get_ancestor_sort_id(&ih, rc_start_height, &tip_sn.sortition_id)? else { - // NOTE: this should never be reached - error!("Unreachable code (but not panicking): no reward cycle info for reward cycle {}", rc); + // reward cycle is too far back for there to be an ancestor continue; }; - if !reward_cycle_info.is_reward_info_known() { - // haven't yet processed the anchor block, so don't store - debug!("Reward cycle info for cycle {} at sortition {} expects the PoX anchor block, so will not cache", rc, &reward_cycle_sort_id); - continue; + let ancestor_ih = sortdb.index_handle(&ancestor_sort_id); + let anchor_hash_opt = ancestor_ih.get_last_anchor_block_hash()?; + + if let Some(cached_rc_info) = self.current_reward_sets.get(&rc) { + if let Some(anchor_hash) = anchor_hash_opt.as_ref() { + // careful -- the sortition DB stores a StacksBlockId's value (the tenure-start + // StacksBlockId) as a BlockHeaderHash, since that's what it was designed to + // deal with in the pre-Nakamoto days + if cached_rc_info.anchor_block_id() == StacksBlockId(anchor_hash.0.clone()) + || cached_rc_info.anchor_block_hash == *anchor_hash + { + // cached reward set data is still valid + continue; + } + } } + let Some((reward_set_info, anchor_block_header)) = load_nakamoto_reward_set( + rc, + &tip_sn.sortition_id, + &self.burnchain, + chainstate, + sortdb, + &OnChainRewardSetProvider::new(), + ) + .map_err(|e| { + warn!( + "Failed to load reward cycle info for cycle {}: {:?}", + rc, &e + ); + e + }) + .unwrap_or(None) else { + continue; + }; + + let rc_info = CurrentRewardSet { + reward_cycle: rc, + reward_cycle_info: reward_set_info, + anchor_block_consensus_hash: anchor_block_header.consensus_hash, + anchor_block_hash: anchor_block_header.anchored_header.block_hash(), + }; + test_debug!( - "Reward cycle info for cycle {} at sortition {} is {:?}", + "Store cached reward set for reward cycle {} anchor block {}", rc, - &reward_cycle_sort_id, - &reward_cycle_info + &rc_info.anchor_block_hash ); - self.current_reward_sets.insert(rc, reward_cycle_info); - self.current_reward_set_ids.insert(rc, reward_cycle_sort_id); + self.current_reward_sets.insert(rc, rc_info); } - - // free memory - self.free_old_reward_cycles(sortdb, &tip_sn.sortition_id, prev_rc); + self.free_old_reward_cycles(cur_rc); Ok(()) } @@ -5561,7 +5556,9 @@ impl PeerNetwork { SortitionDB::get_canonical_stacks_chain_tip_hash_and_height(sortdb.conn())?; let burnchain_tip_changed = canonical_sn.block_height != self.chain_view.burn_block_height - || self.num_state_machine_passes == 0; + || self.num_state_machine_passes == 0 + || canonical_sn.sortition_id != self.burnchain_tip.sortition_id; + let stacks_tip_changed = self.stacks_tip != stacks_tip; let new_stacks_tip_block_id = StacksBlockId::new(&stacks_tip.0, &stacks_tip.1); let need_stackerdb_refresh = canonical_sn.canonical_stacks_tip_consensus_hash @@ -5569,8 +5566,8 @@ impl PeerNetwork { || burnchain_tip_changed || stacks_tip_changed; - if stacks_tip_changed || burnchain_tip_changed { - self.refresh_reward_cycles(sortdb, &canonical_sn)?; + if burnchain_tip_changed || stacks_tip_changed { + self.refresh_reward_cycles(sortdb, chainstate, &canonical_sn)?; } let mut ret: HashMap> = HashMap::new(); @@ -6790,11 +6787,13 @@ mod test { while peer_1_mempool_txs < num_txs || peer_2_mempool_txs < num_txs { if let Ok(mut result) = peer_1.step_with_ibd(false) { let lp = peer_1.network.local_peer.clone(); + let burnchain = peer_1.network.burnchain.clone(); peer_1 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -6808,11 +6807,13 @@ mod test { if let Ok(mut result) = peer_2.step_with_ibd(false) { let lp = peer_2.network.local_peer.clone(); + let burnchain = peer_2.network.burnchain.clone(); peer_2 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -6977,11 +6978,13 @@ mod test { while peer_1_mempool_txs < num_txs || peer_2_mempool_txs < num_txs { if let Ok(mut result) = peer_1.step_with_ibd(false) { let lp = peer_1.network.local_peer.clone(); + let burnchain = peer_1.network.burnchain.clone(); peer_1 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -6995,11 +6998,13 @@ mod test { if let Ok(mut result) = peer_2.step_with_ibd(false) { let lp = peer_2.network.local_peer.clone(); + let burnchain = peer_2.network.burnchain.clone(); peer_2 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -7181,11 +7186,13 @@ mod test { while peer_1_mempool_txs < num_txs || peer_2_mempool_txs < num_txs / 2 { if let Ok(mut result) = peer_1.step_with_ibd(false) { let lp = peer_1.network.local_peer.clone(); + let burnchain = peer_1.network.burnchain.clone(); peer_1 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -7199,11 +7206,13 @@ mod test { if let Ok(mut result) = peer_2.step_with_ibd(false) { let lp = peer_2.network.local_peer.clone(); + let burnchain = peer_2.network.burnchain.clone(); peer_2 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -7365,11 +7374,13 @@ mod test { while peer_1_mempool_txs < num_txs || peer_2.network.mempool_sync_txs < (num_txs as u64) { if let Ok(mut result) = peer_1.step_with_ibd(false) { let lp = peer_1.network.local_peer.clone(); + let burnchain = peer_1.network.burnchain.clone(); peer_1 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, @@ -7383,11 +7394,13 @@ mod test { if let Ok(mut result) = peer_2.step_with_ibd(false) { let lp = peer_2.network.local_peer.clone(); + let burnchain = peer_2.network.burnchain.clone(); peer_2 .with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, diff --git a/stackslib/src/net/relay.rs b/stackslib/src/net/relay.rs index 4db684ca35..022d1edf14 100644 --- a/stackslib/src/net/relay.rs +++ b/stackslib/src/net/relay.rs @@ -38,7 +38,10 @@ use crate::chainstate::burn::db::sortdb::{ }; use crate::chainstate::burn::{BlockSnapshot, ConsensusHash}; use crate::chainstate::coordinator::comm::CoordinatorChannels; -use crate::chainstate::coordinator::BlockEventDispatcher; +use crate::chainstate::coordinator::{ + BlockEventDispatcher, Error as CoordinatorError, OnChainRewardSetProvider, +}; +use crate::chainstate::nakamoto::coordinator::load_nakamoto_reward_set; use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; use crate::chainstate::stacks::db::unconfirmed::ProcessedUnconfirmedState; use crate::chainstate::stacks::db::{StacksChainState, StacksEpochReceipt, StacksHeaderInfo}; @@ -655,6 +658,7 @@ impl Relayer { /// downloaded by us, or pushed via p2p. /// Return Ok(true) if we stored it, Ok(false) if we didn't pub fn process_new_nakamoto_block( + burnchain: &Burnchain, sortdb: &SortitionDB, sort_handle: &mut SortitionHandleConn, chainstate: &mut StacksChainState, @@ -725,15 +729,37 @@ impl Relayer { let config = chainstate.config(); let tip = block_sn.sortition_id; - let reward_info = match sortdb.get_preprocessed_reward_set_of(&tip) { - Ok(x) => x, - Err(db_error::NotFoundError) => { + let reward_info = match load_nakamoto_reward_set( + burnchain + .pox_reward_cycle(block_sn.block_height) + .expect("FATAL: block snapshot has no reward cycle"), + &tip, + burnchain, + chainstate, + sortdb, + &OnChainRewardSetProvider::new(), + ) { + Ok(Some((reward_info, ..))) => reward_info, + Ok(None) => { error!("No RewardCycleInfo found for tip {}", tip); return Err(chainstate_error::PoxNoRewardCycle); } - Err(e) => { + Err(CoordinatorError::DBError(db_error::NotFoundError)) => { + error!("No RewardCycleInfo found for tip {}", tip); + return Err(chainstate_error::PoxNoRewardCycle); + } + Err(CoordinatorError::ChainstateError(e)) => { + error!("No RewardCycleInfo loaded for tip {}: {:?}", tip, &e); + return Err(e); + } + Err(CoordinatorError::DBError(e)) => { + error!("No RewardCycleInfo loaded for tip {}: {:?}", tip, &e); return Err(chainstate_error::DBError(e)); } + Err(e) => { + error!("Failed to load RewardCycleInfo for tip {}: {:?}", tip, &e); + return Err(chainstate_error::PoxNoRewardCycle); + } }; let reward_cycle = reward_info.reward_cycle; @@ -769,6 +795,7 @@ impl Relayer { /// Process nakamoto blocks. /// Log errors but do not return them. pub fn process_nakamoto_blocks( + burnchain: &Burnchain, sortdb: &SortitionDB, chainstate: &mut StacksChainState, blocks: impl Iterator, @@ -779,6 +806,7 @@ impl Relayer { for block in blocks { let block_id = block.block_id(); if let Err(e) = Self::process_new_nakamoto_block( + burnchain, sortdb, &mut sort_handle, chainstate, @@ -2028,6 +2056,7 @@ impl Relayer { &mut self, _local_peer: &LocalPeer, network_result: &mut NetworkResult, + burnchain: &Burnchain, sortdb: &mut SortitionDB, chainstate: &mut StacksChainState, mempool: &mut MemPoolDB, @@ -2121,6 +2150,7 @@ impl Relayer { let nakamoto_blocks = std::mem::replace(&mut network_result.nakamoto_blocks, HashMap::new()); if let Err(e) = Relayer::process_nakamoto_blocks( + burnchain, sortdb, chainstate, nakamoto_blocks.into_values(), diff --git a/stackslib/src/net/tests/download/epoch2x.rs b/stackslib/src/net/tests/download/epoch2x.rs index 5e9ea0daf2..1f7a266596 100644 --- a/stackslib/src/net/tests/download/epoch2x.rs +++ b/stackslib/src/net/tests/download/epoch2x.rs @@ -329,10 +329,12 @@ where let mut result = peer.step_dns(&mut dns_clients[i]).unwrap(); let lp = peer.network.local_peer.clone(); + let burnchain = peer.network.burnchain.clone(); peer.with_db_state(|sortdb, chainstate, relayer, mempool| { relayer.process_network_result( &lp, &mut result, + &burnchain, sortdb, chainstate, mempool, diff --git a/stackslib/src/net/tests/download/nakamoto.rs b/stackslib/src/net/tests/download/nakamoto.rs index c084527336..719e901076 100644 --- a/stackslib/src/net/tests/download/nakamoto.rs +++ b/stackslib/src/net/tests/download/nakamoto.rs @@ -504,6 +504,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -512,6 +513,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -579,6 +581,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -587,6 +590,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -680,6 +684,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -688,6 +693,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -780,6 +786,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -788,6 +795,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -859,6 +867,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -867,6 +876,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -920,6 +930,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); @@ -928,6 +939,7 @@ fn test_nakamoto_unconfirmed_tenure_downloader() { .get(&tip_rc) .cloned() .unwrap() + .reward_cycle_info .known_selected_anchor_block_owned() .unwrap(), ); diff --git a/stackslib/src/net/tests/mod.rs b/stackslib/src/net/tests/mod.rs index 5e2cb3e6cc..9e225d8f0d 100644 --- a/stackslib/src/net/tests/mod.rs +++ b/stackslib/src/net/tests/mod.rs @@ -230,6 +230,7 @@ impl NakamotoBootPlan { for block in blocks { let block_id = block.block_id(); let accepted = Relayer::process_new_nakamoto_block( + &peer.network.burnchain, &sortdb, &mut sort_handle, &mut node.chainstate, diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 0d04d12537..2ad77a425d 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -25,6 +25,8 @@ use libsigner::v1::messages::{MessageSlotID, SignerMessage}; use stacks::burnchains::Burnchain; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; +use stacks::chainstate::coordinator::OnChainRewardSetProvider; +use stacks::chainstate::nakamoto::coordinator::load_nakamoto_reward_set; use stacks::chainstate::nakamoto::miner::{NakamotoBlockBuilder, NakamotoTenureInfo}; use stacks::chainstate::nakamoto::signer_set::NakamotoSigners; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; @@ -298,8 +300,42 @@ impl BlockMinerThread { }) })?; - let reward_info = match sort_db.get_preprocessed_reward_set_of(&tip.sortition_id) { - Ok(x) => x, + let mut chain_state = + neon_node::open_chainstate_with_faults(&self.config).map_err(|e| { + NakamotoNodeError::SigningCoordinatorFailure(format!( + "Failed to open chainstate DB. Cannot mine! {e:?}" + )) + })?; + + let reward_cycle = self + .burnchain + .pox_constants + .block_height_to_reward_cycle( + self.burnchain.first_block_height, + self.burn_block.block_height, + ) + .ok_or_else(|| { + NakamotoNodeError::SigningCoordinatorFailure( + "Building on a burn block that is before the first burn block".into(), + ) + })?; + + let reward_info = match load_nakamoto_reward_set( + self.burnchain + .pox_reward_cycle(tip.block_height.saturating_add(1)) + .expect("FATAL: no reward cycle for sortition"), + &tip.sortition_id, + &self.burnchain, + &mut chain_state, + &sort_db, + &OnChainRewardSetProvider::new(), + ) { + Ok(Some((reward_info, _))) => reward_info, + Ok(None) => { + return Err(NakamotoNodeError::SigningCoordinatorFailure( + "No reward set stored yet. Cannot mine!".into(), + )); + } Err(e) => { return Err(NakamotoNodeError::SigningCoordinatorFailure(format!( "Failure while fetching reward set. Cannot initialize miner coordinator. {e:?}" @@ -380,8 +416,29 @@ impl BlockMinerThread { }) })?; - let reward_info = match sort_db.get_preprocessed_reward_set_of(&tip.sortition_id) { - Ok(x) => x, + let mut chain_state = + neon_node::open_chainstate_with_faults(&self.config).map_err(|e| { + NakamotoNodeError::SigningCoordinatorFailure(format!( + "Failed to open chainstate DB. Cannot mine! {e:?}" + )) + })?; + + let reward_info = match load_nakamoto_reward_set( + self.burnchain + .pox_reward_cycle(tip.block_height.saturating_add(1)) + .expect("FATAL: no reward cycle for sortition"), + &tip.sortition_id, + &self.burnchain, + &mut chain_state, + &sort_db, + &OnChainRewardSetProvider::new(), + ) { + Ok(Some((reward_info, _))) => reward_info, + Ok(None) => { + return Err(NakamotoNodeError::SigningCoordinatorFailure( + "No reward set stored yet. Cannot mine!".into(), + )); + } Err(e) => { return Err(NakamotoNodeError::SigningCoordinatorFailure(format!( "Failure while fetching reward set. Cannot initialize miner coordinator. {e:?}" @@ -841,8 +898,44 @@ impl BlockMinerThread { let signer_transactions = self.get_signer_transactions(&mut chain_state, &burn_db, &stackerdbs)?; - let signer_bitvec_len = - &burn_db.get_preprocessed_reward_set_size(&self.burn_block.sortition_id); + let tip = SortitionDB::get_canonical_burn_chain_tip(burn_db.conn()) + .map_err(|e| NakamotoNodeError::MiningFailure(ChainstateError::DBError(e)))?; + + let reward_info = match load_nakamoto_reward_set( + self.burnchain + .pox_reward_cycle(tip.block_height.saturating_add(1)) + .expect("FATAL: no reward cycle defined for sortition tip"), + &tip.sortition_id, + &self.burnchain, + &mut chain_state, + &burn_db, + &OnChainRewardSetProvider::new(), + ) { + Ok(Some((reward_info, _))) => reward_info, + Ok(None) => { + return Err(NakamotoNodeError::SigningCoordinatorFailure( + "No reward set stored yet. Cannot mine!".into(), + )); + } + Err(e) => { + return Err(NakamotoNodeError::SigningCoordinatorFailure(format!( + "Failure while fetching reward set. Cannot initialize miner coordinator. {e:?}" + ))); + } + }; + + let Some(reward_set) = reward_info.known_selected_anchor_block_owned() else { + return Err(NakamotoNodeError::SigningCoordinatorFailure( + "Current reward cycle did not select a reward set. Cannot mine!".into(), + )); + }; + let signer_bitvec_len = reward_set + .signers + .as_ref() + .map(|x| x.len()) + .unwrap_or(0) + .try_into() + .ok(); // build the block itself let (mut block, consumed, size, tx_events) = NakamotoBlockBuilder::build_nakamoto_block( diff --git a/testnet/stacks-node/src/nakamoto_node/relayer.rs b/testnet/stacks-node/src/nakamoto_node/relayer.rs index d0ff26acdb..a839374b13 100644 --- a/testnet/stacks-node/src/nakamoto_node/relayer.rs +++ b/testnet/stacks-node/src/nakamoto_node/relayer.rs @@ -258,6 +258,7 @@ impl RelayerThread { .process_network_result( &self.local_peer, &mut net_result, + &self.burnchain, &mut self.sortdb, &mut self.chainstate, &mut self.mempool, @@ -416,11 +417,16 @@ impl RelayerThread { .unwrap_or_else(|| VRFProof::empty()); // let's figure out the recipient set! - let recipients = get_nakamoto_next_recipients(&sort_tip, &mut self.sortdb, &self.burnchain) - .map_err(|e| { - error!("Relayer: Failure fetching recipient set: {:?}", e); - NakamotoNodeError::SnapshotNotFoundForChainTip - })?; + let recipients = get_nakamoto_next_recipients( + &sort_tip, + &mut self.sortdb, + &mut self.chainstate, + &self.burnchain, + ) + .map_err(|e| { + error!("Relayer: Failure fetching recipient set: {:?}", e); + NakamotoNodeError::SnapshotNotFoundForChainTip + })?; let block_header = NakamotoChainState::get_block_header_by_consensus_hash(self.chainstate.db(), target_ch) diff --git a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs index a0be82f06e..7fcb5bb008 100644 --- a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs +++ b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs @@ -206,7 +206,8 @@ impl SignCoordinator { ) -> Result { let is_mainnet = config.is_mainnet(); let Some(ref reward_set_signers) = reward_set.signers else { - error!("Could not initialize WSTS coordinator for reward set without signer"); + error!("Could not initialize signing coordinator for reward set without signer"); + debug!("reward set: {:?}", &reward_set); return Err(ChainstateError::NoRegisteredSigners(0)); }; diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index a6b3035938..0045e74d85 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -2727,6 +2727,7 @@ impl RelayerThread { .process_network_result( &relayer_thread.local_peer, &mut net_result, + &relayer_thread.burnchain, sortdb, chainstate, mempool, diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 7c57e8c14c..376ef1409b 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -37,6 +37,8 @@ use stacks::chainstate::burn::operations::{ BlockstackOperationType, PreStxOp, StackStxOp, VoteForAggregateKeyOp, }; use stacks::chainstate::coordinator::comm::CoordinatorChannels; +use stacks::chainstate::coordinator::OnChainRewardSetProvider; +use stacks::chainstate::nakamoto::coordinator::load_nakamoto_reward_set; use stacks::chainstate::nakamoto::miner::NakamotoBlockBuilder; use stacks::chainstate::nakamoto::test_signers::TestSigners; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState}; @@ -363,13 +365,31 @@ pub fn read_and_sign_block_proposal( ) -> Result { let burnchain = conf.get_burnchain(); let sortdb = burnchain.open_sortition_db(true).unwrap(); + let (mut chainstate, _) = StacksChainState::open( + conf.is_mainnet(), + conf.burnchain.chain_id, + &conf.get_chainstate_path_str(), + None, + ) + .unwrap(); + let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); - let reward_set = sortdb - .get_preprocessed_reward_set_of(&tip.sortition_id) - .expect("Failed to get reward cycle info") - .known_selected_anchor_block_owned() - .expect("Expected a reward set"); + let reward_set = load_nakamoto_reward_set( + burnchain + .pox_reward_cycle(tip.block_height.saturating_add(1)) + .unwrap(), + &tip.sortition_id, + &burnchain, + &mut chainstate, + &sortdb, + &OnChainRewardSetProvider::new(), + ) + .expect("Failed to query reward set") + .expect("No reward set calculated") + .0 + .known_selected_anchor_block_owned() + .expect("Expected a reward set"); let mut proposed_block = get_latest_block_proposal(conf, &sortdb)?; let proposed_block_hash = format!("0x{}", proposed_block.header.block_hash());