From ba1ceac7452fd88b97a7fa61b200963a2a25f284 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 14:41:35 -0700 Subject: [PATCH 01/35] Remove handle_affirmation_reorg, check_pox_anchor_affirmation Signed-off-by: Jacinta Ferrant --- stacks-node/src/tests/signer/multiversion.rs | 2 +- stackslib/src/chainstate/coordinator/mod.rs | 861 +------------------ 2 files changed, 14 insertions(+), 849 deletions(-) diff --git a/stacks-node/src/tests/signer/multiversion.rs b/stacks-node/src/tests/signer/multiversion.rs index 39d903ff82..7435f1875d 100644 --- a/stacks-node/src/tests/signer/multiversion.rs +++ b/stacks-node/src/tests/signer/multiversion.rs @@ -14,7 +14,7 @@ // along with this program. If not, see . use std::sync::mpsc::TryRecvError; use std::thread; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use libsigner::v0::messages::{SignerMessage, StateMachineUpdate}; use libsigner::v0::signer_state::{MinerState, ReplayTransactionSet, SignerStateMachine}; diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index cc21170543..7cb0adf654 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -29,7 +29,7 @@ use stacks_common::util::get_epoch_time_secs; pub use self::comm::CoordinatorCommunication; use super::stacks::boot::{RewardSet, RewardSetData}; use super::stacks::db::blocks::DummyEventDispatcher; -use crate::burnchains::affirmation::{AffirmationMap, AffirmationMapEntry}; +use crate::burnchains::affirmation::AffirmationMap; use crate::burnchains::db::{BurnchainBlockData, BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{ Burnchain, BurnchainBlockHeader, Error as BurnchainError, PoxConstants, Txid, @@ -1671,595 +1671,6 @@ impl< Ok(()) } - /// Compare the coordinator's heaviest affirmation map to the heaviest affirmation map in the - /// burnchain DB. If they are different, then invalidate all sortitions not represented on - /// the coordinator's heaviest affirmation map that are now represented by the burnchain DB's - /// heaviest affirmation map. - /// - /// Care must be taken to ensure that a sortition that was already created, but invalidated, is - /// not re-created. This can happen if the affirmation map flaps, causing a sortition that was - /// created and invalidated to become valid again. The code here addresses this by considering - /// three ranges of sortitions (grouped by reward cycle) when processing a new heaviest - /// affirmation map: - /// - /// * The range of sortitions that are valid in both affirmation maps. These sortitions - /// correspond to the affirmation maps' common prefix. - /// * The range of sortitions that exists and are invalid on the coordinator's current - /// affirmation map, but are valid on the new heaviest affirmation map. These sortitions - /// come strictly after the common prefix, and are identified by the variables - /// `first_invalid_start_block` and `last_invalid_start_block` (which identifies their lowest - /// and highest block heights). - /// * The range of sortitions that are currently valid, and need to be invalidated. This range - /// comes strictly after the aforementioned previously-invalid-but-now-valid sortition range. - /// - /// The code does not modify any sortition state for the common prefix of sortitions. - /// - /// The code identifies the second range of previously-invalid-but-now-valid sortitions and marks them - /// as valid once again. In addition, it updates the Stacks chainstate DB such that any Stacks - /// blocks that were orphaned and never processed can be retried with the now-revalidated - /// sortition. - /// - /// The code identifies the third range of now-invalid sortitions and marks them as invalid in - /// the sortition DB. - /// - /// Note that regardless of the affirmation map status, a Stacks block will remain processed - /// once it gets accepted. Its underlying sortition may become invalidated, in which case, the - /// Stacks block would no longer be considered as part of the canonical Stacks fork (since the - /// canonical Stacks chain tip must reside on a valid sortition). However, a Stacks block that - /// should be processed at the end of the day may temporarily be considered orphaned if there - /// is a "deep" affirmation map reorg that causes at least one reward cycle's sortitions to - /// be treated as invalid. This is what necessitates retrying Stacks blocks that have been - /// downloaded and considered orphaned because they were never processed -- they may in fact be - /// valid and processable once the node has identified the canonical sortition history! - /// - /// The only kinds of errors returned here are database query errors. - fn handle_affirmation_reorg(&mut self) -> Result<(), Error> { - // find the stacks chain's affirmation map - let canonical_burnchain_tip = self.burnchain_blocks_db.get_canonical_chain_tip()?; - - let sortition_tip = self.canonical_sortition_tip.as_ref().expect( - "FAIL: processing an affirmation reorg, but don't have a canonical sortition tip", - ); - - let last_2_05_rc = self.sortition_db.get_last_epoch_2_05_reward_cycle()?; - - let sortition_height = - SortitionDB::get_block_snapshot(self.sortition_db.conn(), sortition_tip)? - .unwrap_or_else(|| panic!("FATAL: no sortition {sortition_tip}")) - .block_height; - - let sortition_reward_cycle = self - .burnchain - .block_height_to_reward_cycle(sortition_height) - .unwrap_or(0); - - let heaviest_am = self.get_heaviest_affirmation_map(sortition_tip)?; - - if let Some(changed_reward_cycle) = - self.check_chainstate_against_burnchain_affirmations()? - { - debug!( - "Canonical sortition tip is {sortition_tip} height {sortition_height} (rc {sortition_reward_cycle}); changed reward cycle is {changed_reward_cycle}" - ); - - if changed_reward_cycle >= sortition_reward_cycle { - // nothing we can do - debug!("Changed reward cycle is {changed_reward_cycle} but canonical sortition is in {sortition_reward_cycle}, so no affirmation reorg is possible"); - return Ok(()); - } - - let current_reward_cycle = self - .burnchain - .block_height_to_reward_cycle(canonical_burnchain_tip.block_height) - .unwrap_or(0); - - // sortitions between [first_invalidate_start_block, last_invalidate_start_block) will - // be invalidated. Any orphaned Stacks blocks in this range will be forgotten, so they - // can be retried later with the new sortitions in this burnchain block range. - // - // valid_sortitions include all sortitions in this range that are now valid (i.e. - // they were invalidated before, but will be valid again as a result of this reorg). - let (first_invalidate_start_block, last_invalidate_start_block, valid_sortitions) = - match self.find_invalid_and_revalidated_sortitions( - &heaviest_am, - changed_reward_cycle, - current_reward_cycle, - )? { - Some(x) => x, - None => { - // the sortition AM is consistent with the heaviest AM. - // If the sortition AM is not consistent with the canonical AM, then it - // means that we have new anchor blocks to consider - let canonical_affirmation_map = - self.get_canonical_affirmation_map(sortition_tip)?; - let sort_am = self - .sortition_db - .find_sortition_tip_affirmation_map(sortition_tip)?; - let revalidation_params = if canonical_affirmation_map.len() - == sort_am.len() - && canonical_affirmation_map != sort_am - { - if let Some(diverged_rc) = - canonical_affirmation_map.find_divergence(&sort_am) - { - debug!( - "Sortition AM `{sort_am}` diverges from canonical AM `{canonical_affirmation_map}` at cycle {diverged_rc}" - ); - let (last_invalid_sortition_height, valid_sortitions) = self - .find_valid_sortitions( - &canonical_affirmation_map, - self.burnchain.reward_cycle_to_block_height(diverged_rc), - canonical_burnchain_tip.block_height, - )?; - Some(( - last_invalid_sortition_height, - self.burnchain - .reward_cycle_to_block_height(sort_am.len() as u64), - valid_sortitions, - )) - } else { - None - } - } else { - None - }; - if let Some(x) = revalidation_params { - debug!( - "Sortition AM `{sort_am}` is not consistent with canonical AM `{canonical_affirmation_map}`" - ); - x - } else { - // everything is consistent. - // Just update the canonical stacks block pointer on the highest valid - // sortition. - let last_2_05_rc = - self.sortition_db.get_last_epoch_2_05_reward_cycle()?; - - let mut sort_tx = self.sortition_db.tx_begin()?; - let (canonical_ch, canonical_bhh, canonical_height) = - Self::find_highest_stacks_block_with_compatible_affirmation_map( - &heaviest_am, - sortition_tip, - &self.burnchain_blocks_db, - &mut sort_tx, - self.chain_state_db.db(), - )?; - - let stacks_am = inner_static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - last_2_05_rc, - &sort_tx.find_sortition_tip_affirmation_map(sortition_tip)?, - &sort_tx, - &canonical_ch, - &canonical_bhh, - )?; - - debug!("Canonical Stacks tip for highest valid sortition {} ({}) is {}/{} height {} am `{}`", &sortition_tip, sortition_height, &canonical_ch, &canonical_bhh, canonical_height, &stacks_am); - - SortitionDB::revalidate_snapshot_with_block( - &sort_tx, - sortition_tip, - &canonical_ch, - &canonical_bhh, - canonical_height, - Some(true), - )?; - sort_tx.commit()?; - return Ok(()); - } - } - }; - - // check valid_sortitions -- it may correspond to a range of sortitions beyond our - // current highest-valid sortition (in which case, *do not* revalidate them) - let valid_sortitions = if let Some(first_sn) = valid_sortitions.first() { - if first_sn.block_height > sortition_height { - debug!("No sortitions to revalidate: highest is {},{}, first candidate is {},{}. Will not revalidate.", sortition_height, &sortition_tip, first_sn.block_height, &first_sn.sortition_id); - vec![] - } else { - valid_sortitions - } - } else { - valid_sortitions - }; - - // find our ancestral sortition ID that's the end of the last reward cycle - // the new affirmation map would have in common with the old affirmation - // map, and invalidate its descendants - let ic = self.sortition_db.index_conn(); - - // find the burnchain block hash and height of the first burnchain block in which we'll - // invalidate all descendant sortitions, but retain some previously-invalidated - // sortitions - let revalidated_burn_header = BurnchainDB::get_burnchain_header( - self.burnchain_blocks_db.conn(), - &self.burnchain_indexer, - first_invalidate_start_block - 1, - ) - .expect("FATAL: failed to read burnchain DB") - .unwrap_or_else(|| { - panic!( - "FATAL: no burnchain block {}", - first_invalidate_start_block - 1 - ) - }); - - // find the burnchain block hash and height of the first burnchain block in which we'll - // invalidate all descendant sortitions, no matter what. - let invalidated_burn_header = BurnchainDB::get_burnchain_header( - self.burnchain_blocks_db.conn(), - &self.burnchain_indexer, - last_invalidate_start_block - 1, - ) - .expect("FATAL: failed to read burnchain DB") - .unwrap_or_else(|| { - panic!( - "FATAL: no burnchain block {}", - last_invalidate_start_block - 1 - ) - }); - - // let invalidation_height = revalidate_sn.block_height; - let invalidation_height = revalidated_burn_header.block_height; - - debug!("Invalidate all descendants of {} (after height {}), revalidate some sortitions at and after height {}, and retry all orphaned Stacks blocks at or after height {}", - &revalidated_burn_header.block_hash, revalidated_burn_header.block_height, invalidated_burn_header.block_height, first_invalidate_start_block); - - let mut highest_valid_sortition_id = - if sortition_height > last_invalidate_start_block - 1 { - let invalidate_sn = SortitionDB::get_ancestor_snapshot( - &ic, - last_invalidate_start_block - 1, - sortition_tip, - )? - .unwrap_or_else(|| { - panic!( - "BUG: no ancestral sortition at height {}", - last_invalidate_start_block - 1 - ) - }); - - valid_sortitions - .last() - .unwrap_or(&invalidate_sn) - .sortition_id - .clone() - } else { - sortition_tip.clone() - }; - - let mut stacks_blocks_to_unorphan = vec![]; - let chainstate_db_conn = self.chain_state_db.db(); - - self.sortition_db.invalidate_descendants_with_closures( - &revalidated_burn_header.block_hash, - |_sort_tx, burn_header, _invalidate_queue| { - // do this once in the transaction, after we've invalidated all other - // sibling blocks to these now-valid sortitions - test_debug!( - "Invalidate all sortitions descending from {} ({} remaining)", - &burn_header, - _invalidate_queue.len() - ); - stacks_blocks_to_unorphan.push((burn_header.clone(), invalidation_height)); - }, - |sort_tx| { - // no more sortitions to invalidate -- all now-incompatible - // sortitions have been invalidated. - let (canonical_ch, canonical_bhh, canonical_height) = Self::find_highest_stacks_block_with_compatible_affirmation_map(&heaviest_am, &highest_valid_sortition_id, &self.burnchain_blocks_db, sort_tx, chainstate_db_conn) - .expect("FATAL: could not find a valid parent Stacks block"); - - let stacks_am = inner_static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - last_2_05_rc, - &sort_tx.find_sortition_tip_affirmation_map(&highest_valid_sortition_id).expect("FATAL: failed to query stacks DB"), - sort_tx, - &canonical_ch, - &canonical_bhh - ) - .expect("FATAL: failed to query stacks DB"); - - debug!("Canonical Stacks tip after invalidations is {}/{} height {} am `{}`", &canonical_ch, &canonical_bhh, canonical_height, &stacks_am); - - // Revalidate sortitions, and declare that we have their Stacks blocks. - for valid_sn in valid_sortitions.iter() { - test_debug!("Revalidate snapshot {},{}", valid_sn.block_height, &valid_sn.sortition_id); - let block_known = StacksChainState::is_stacks_block_processed( - chainstate_db_conn, - &valid_sn.consensus_hash, - &valid_sn.winning_stacks_block_hash, - ).expect("FATAL: failed to query chainstate DB"); - - SortitionDB::revalidate_snapshot_with_block(sort_tx, &valid_sn.sortition_id, &canonical_ch, &canonical_bhh, canonical_height, Some(block_known)).unwrap_or_else(|_| panic!("FATAL: failed to revalidate sortition {}", - valid_sn.sortition_id)); - } - - // recalculate highest valid sortition with revalidated snapshots - highest_valid_sortition_id = if sortition_height > last_invalidate_start_block - 1 { - let invalidate_sn = SortitionDB::get_ancestor_snapshot_tx( - sort_tx, - last_invalidate_start_block - 1, - sortition_tip, - ) - .expect("FATAL: failed to query the sortition DB") - .unwrap_or_else(|| panic!("BUG: no ancestral sortition at height {}", - last_invalidate_start_block - 1)); - - valid_sortitions - .last() - .unwrap_or(&invalidate_sn) - .sortition_id - .clone() - } - else { - sortition_tip.clone() - }; - - // recalculate highest valid stacks tip - let (canonical_ch, canonical_bhh, canonical_height) = Self::find_highest_stacks_block_with_compatible_affirmation_map(&heaviest_am, &highest_valid_sortition_id, &self.burnchain_blocks_db, sort_tx, chainstate_db_conn) - .expect("FATAL: could not find a valid parent Stacks block"); - - let stacks_am = inner_static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - last_2_05_rc, - &sort_tx.find_sortition_tip_affirmation_map(&highest_valid_sortition_id).expect("FATAL: failed to query stacks DB"), - sort_tx, - &canonical_ch, - &canonical_bhh - ) - .expect("FATAL: failed to query stacks DB"); - - debug!("Canonical Stacks tip after invalidations and revalidations is {}/{} height {} am `{}`", &canonical_ch, &canonical_bhh, canonical_height, &stacks_am); - - // update dirty canonical block pointers. - let dirty_snapshots = SortitionDB::find_snapshots_with_dirty_canonical_block_pointers(sort_tx, canonical_height) - .expect("FATAL: failed to find dirty snapshots"); - - for dirty_sort_id in dirty_snapshots.iter() { - test_debug!("Revalidate dirty snapshot {}", dirty_sort_id); - - let dirty_sort_sn = SortitionDB::get_block_snapshot(sort_tx, dirty_sort_id) - .expect("FATAL: failed to query sortition DB") - .expect("FATAL: no such dirty sortition"); - - let block_known = StacksChainState::is_stacks_block_processed( - chainstate_db_conn, - &dirty_sort_sn.consensus_hash, - &dirty_sort_sn.winning_stacks_block_hash, - ).expect("FATAL: failed to query chainstate DB"); - - SortitionDB::revalidate_snapshot_with_block(sort_tx, dirty_sort_id, &canonical_ch, &canonical_bhh, canonical_height, Some(block_known)).unwrap_or_else(|_| panic!("FATAL: failed to revalidate dirty sortition {}", - dirty_sort_id)); - } - - // recalculate highest valid stacks tip once more - let (canonical_ch, canonical_bhh, canonical_height) = Self::find_highest_stacks_block_with_compatible_affirmation_map(&heaviest_am, &highest_valid_sortition_id, &self.burnchain_blocks_db, sort_tx, chainstate_db_conn) - .expect("FATAL: could not find a valid parent Stacks block"); - - let stacks_am = inner_static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - last_2_05_rc, - &sort_tx.find_sortition_tip_affirmation_map(&highest_valid_sortition_id).expect("FATAL: failed to query stacks DB"), - sort_tx, - &canonical_ch, - &canonical_bhh - ) - .expect("FATAL: failed to query stacks DB"); - - debug!("Canonical Stacks tip after invalidations, revalidations, and processed dirty snapshots is {}/{} height {} am `{}`", &canonical_ch, &canonical_bhh, canonical_height, &stacks_am); - - let highest_valid_sn = SortitionDB::get_block_snapshot(sort_tx, &highest_valid_sortition_id) - .expect("FATAL: failed to query sortition ID") - .expect("FATAL: highest valid sortition ID does not have a snapshot"); - - let block_known = StacksChainState::is_stacks_block_processed( - chainstate_db_conn, - &highest_valid_sn.consensus_hash, - &highest_valid_sn.winning_stacks_block_hash, - ).expect("FATAL: failed to query chainstate DB"); - - SortitionDB::revalidate_snapshot_with_block(sort_tx, &highest_valid_sortition_id, &canonical_ch, &canonical_bhh, canonical_height, Some(block_known)).unwrap_or_else(|_| panic!("FATAL: failed to revalidate highest valid sortition {}", - &highest_valid_sortition_id)); - }, - )?; - - let ic = self.sortition_db.index_conn(); - - let mut chainstate_db_tx = self.chain_state_db.db_tx_begin()?; - for (burn_header, invalidation_height) in stacks_blocks_to_unorphan { - // permit re-processing of any associated stacks blocks if they're - // orphaned - forget_orphan_stacks_blocks( - &ic, - &mut chainstate_db_tx, - &burn_header, - invalidation_height, - )?; - } - - // un-orphan blocks that had been orphaned but were tied to this now-revalidated sortition history - Self::undo_stacks_block_orphaning( - self.burnchain_blocks_db.conn(), - &self.burnchain_indexer, - &ic, - &mut chainstate_db_tx, - first_invalidate_start_block, - last_invalidate_start_block, - )?; - - // by holding this lock as long as we do, we ensure that the sortition DB's - // view of the canonical stacks chain tip can't get changed (since no - // Stacks blocks can be processed). - chainstate_db_tx.commit().map_err(DBError::SqliteError)?; - - let highest_valid_snapshot = SortitionDB::get_block_snapshot( - self.sortition_db.conn(), - &highest_valid_sortition_id, - )? - .expect("FATAL: highest valid sortition doesn't exist"); - - let stacks_tip_affirmation_map = static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db, - &highest_valid_snapshot.sortition_id, - &highest_valid_snapshot.canonical_stacks_tip_consensus_hash, - &highest_valid_snapshot.canonical_stacks_tip_hash, - )?; - - debug!( - "Highest valid sortition (changed) is {} ({} in height {}, affirmation map {}); Stacks tip is {}/{} height {} (affirmation map {}); heaviest AM is {}", - &highest_valid_snapshot.sortition_id, - &highest_valid_snapshot.burn_header_hash, - highest_valid_snapshot.block_height, - &self.sortition_db.find_sortition_tip_affirmation_map(&highest_valid_snapshot.sortition_id)?, - &highest_valid_snapshot.canonical_stacks_tip_consensus_hash, - &highest_valid_snapshot.canonical_stacks_tip_hash, - highest_valid_snapshot.canonical_stacks_tip_height, - &stacks_tip_affirmation_map, - &heaviest_am - ); - - self.canonical_sortition_tip = Some(highest_valid_snapshot.sortition_id); - } else { - let highest_valid_snapshot = - SortitionDB::get_block_snapshot(self.sortition_db.conn(), sortition_tip)? - .expect("FATAL: highest valid sortition doesn't exist"); - - let stacks_tip_affirmation_map = static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db, - &highest_valid_snapshot.sortition_id, - &highest_valid_snapshot.canonical_stacks_tip_consensus_hash, - &highest_valid_snapshot.canonical_stacks_tip_hash, - )?; - - debug!( - "Highest valid sortition (not changed) is {} ({} in height {}, affirmation map {}); Stacks tip is {}/{} height {} (affirmation map {}); heaviest AM is {}", - &highest_valid_snapshot.sortition_id, - &highest_valid_snapshot.burn_header_hash, - highest_valid_snapshot.block_height, - &self.sortition_db.find_sortition_tip_affirmation_map(&highest_valid_snapshot.sortition_id)?, - &highest_valid_snapshot.canonical_stacks_tip_consensus_hash, - &highest_valid_snapshot.canonical_stacks_tip_hash, - highest_valid_snapshot.canonical_stacks_tip_height, - &stacks_tip_affirmation_map, - &heaviest_am - ); - } - - Ok(()) - } - - /// Use the network's affirmations to re-interpret our local PoX anchor block status into what - /// the network affirmed was their PoX anchor block statuses. - /// If we're blocked on receiving a new anchor block that we don't have (i.e. the network - /// affirmed that it exists), then indicate so by returning its hash. - fn reinterpret_affirmed_pox_anchor_block_status( - &self, - canonical_affirmation_map: &AffirmationMap, - header: &BurnchainBlockHeader, - rc_info: &mut RewardCycleInfo, - ) -> Result, Error> { - // re-calculate the reward cycle info's anchor block status, based on what - // the network has affirmed in each prepare phase. - - // is this anchor block affirmed? Only process it if so! - let new_reward_cycle = self - .burnchain - .block_height_to_reward_cycle(header.block_height) - .expect("BUG: processed block before start of epoch 2.1"); - - test_debug!( - "Verify affirmation against PoX info in reward cycle {} canonical affirmation map {}", - new_reward_cycle, - canonical_affirmation_map - ); - - let new_status = if new_reward_cycle > 0 - && new_reward_cycle <= (canonical_affirmation_map.len() as u64) - { - let affirmed_rc = new_reward_cycle - 1; - - // we're processing an anchor block from an earlier reward cycle, - // meaning that we're in the middle of an affirmation reorg. - let affirmation = canonical_affirmation_map - .at(affirmed_rc) - .expect("BUG: checked index overflow") - .to_owned(); - test_debug!("Affirmation '{affirmation}' for anchor block of previous reward cycle {affirmed_rc} canonical affirmation map {canonical_affirmation_map}"); - - // switch reward cycle info assessment based on what the network - // affirmed. - match &rc_info.anchor_status { - PoxAnchorBlockStatus::SelectedAndKnown(block_hash, txid, reward_set) => { - match affirmation { - AffirmationMapEntry::PoxAnchorBlockPresent => { - // matches affirmation - PoxAnchorBlockStatus::SelectedAndKnown( - block_hash.clone(), - txid.clone(), - reward_set.clone(), - ) - } - AffirmationMapEntry::PoxAnchorBlockAbsent => { - // network actually affirms that this anchor block - // is absent. - warn!("Chose PoX anchor block for reward cycle {affirmed_rc}, but it is affirmed absent by the network"; "affirmation map" => %&canonical_affirmation_map); - PoxAnchorBlockStatus::SelectedAndUnknown( - block_hash.clone(), - txid.clone(), - ) - } - AffirmationMapEntry::Nothing => { - // no anchor block selected either way - PoxAnchorBlockStatus::NotSelected - } - } - } - PoxAnchorBlockStatus::SelectedAndUnknown(ref block_hash, ref txid) => { - match affirmation { - AffirmationMapEntry::PoxAnchorBlockPresent => { - // the network affirms that this anchor block - // exists, but we don't have it locally. Stop - // processing here and wait for it to arrive, via - // the downloader. - info!("Anchor block {block_hash} (txid {txid}) for reward cycle {affirmed_rc} is affirmed by the network ({canonical_affirmation_map}), but must be downloaded"); - return Ok(Some(block_hash.clone())); - } - AffirmationMapEntry::PoxAnchorBlockAbsent => { - // matches affirmation - PoxAnchorBlockStatus::SelectedAndUnknown( - block_hash.clone(), - txid.clone(), - ) - } - AffirmationMapEntry::Nothing => { - // no anchor block selected either way - PoxAnchorBlockStatus::NotSelected - } - } - } - PoxAnchorBlockStatus::NotSelected => { - // no anchor block selected either way - PoxAnchorBlockStatus::NotSelected - } - } - } else { - // no-op: our view of the set of anchor blocks is consistent with - // the canonical affirmation map, so the status of this new anchor - // block is whatever it was calculated to be. - rc_info.anchor_status.clone() - }; - - // update new status - debug!( - "Update anchor block status for reward cycle {} from {:?} to {:?}", - new_reward_cycle, &rc_info.anchor_status, &new_status - ); - rc_info.anchor_status = new_status; - Ok(None) - } - /// Try to revalidate a sortition if it exists already. This can happen if the node flip/flops /// between two PoX forks. /// @@ -2329,7 +1740,6 @@ impl< fn check_missing_anchor_block( &self, header: &BurnchainBlockHeader, - canonical_affirmation_map: &AffirmationMap, rc_info: &mut RewardCycleInfo, ) -> Result, Error> { let cur_epoch = @@ -2338,45 +1748,18 @@ impl< panic!("BUG: no epoch defined at height {}", header.block_height) }); - if self.config.assume_present_anchor_blocks { + if cur_epoch.epoch_id >= StacksEpochId::Epoch21 || self.config.assume_present_anchor_blocks + { // anchor blocks are always assumed to be present in the chain history, // so report its absence if we don't have it. if let PoxAnchorBlockStatus::SelectedAndUnknown(missing_anchor_block, _) = &rc_info.anchor_status { - info!( - "Currently missing PoX anchor block {}, which is assumed to be present", - &missing_anchor_block - ); + info!("Currently missing PoX anchor block {missing_anchor_block}, which is assumed to be present"); return Ok(Some(missing_anchor_block.clone())); } } - if cur_epoch.epoch_id >= StacksEpochId::Epoch21 || self.config.always_use_affirmation_maps { - // potentially have an anchor block, but only process the next reward cycle (and - // subsequent reward cycles) with it if the prepare-phase block-commits affirm its - // presence. This only gets checked in Stacks 2.1 or later (unless overridden - // in the config) - - // NOTE: this mutates rc_info if it returns None - if let Some(missing_anchor_block) = self.reinterpret_affirmed_pox_anchor_block_status( - canonical_affirmation_map, - header, - rc_info, - )? { - if self.config.require_affirmed_anchor_blocks { - // missing this anchor block -- cannot proceed until we have it - info!( - "Burnchain block processing stops due to missing affirmed anchor stacks block hash {missing_anchor_block}" - ); - return Ok(Some(missing_anchor_block)); - } else { - // this and descendant sortitions might already exist - info!("Burnchain block processing will continue in spite of missing affirmed anchor stacks block hash {missing_anchor_block}"); - } - } - } - test_debug!( "Reward cycle info at height {}: {:?}", &header.block_height, @@ -2411,10 +1794,7 @@ impl< let canonical_snapshot = match self.canonical_sortition_tip.as_ref() { Some(sn_tip) => SortitionDB::get_block_snapshot(self.sortition_db.conn(), sn_tip)? .unwrap_or_else(|| { - panic!( - "FATAL: do not have previously-calculated highest valid sortition tip {}", - sn_tip - ) + panic!("FATAL: do not have previously-calculated highest valid sortition tip {sn_tip}") }), None => SortitionDB::get_canonical_burn_chain_tip(self.sortition_db.conn())?, }; @@ -2464,50 +1844,16 @@ impl< let last_2_05_rc = self.sortition_db.get_last_epoch_2_05_reward_cycle()?; - // first, see if the canonical affirmation map has changed. If so, this will wind back the - // canonical sortition tip. - // - // only do this if affirmation maps are supported in this epoch. - let before_canonical_snapshot = match self.canonical_sortition_tip.as_ref() { - Some(sn_tip) => SortitionDB::get_block_snapshot(self.sortition_db.conn(), sn_tip)? - .unwrap_or_else(|| { - panic!( - "FATAL: do not have previously-calculated highest valid sortition tip {}", - sn_tip - ) - }), - None => SortitionDB::get_canonical_burn_chain_tip(self.sortition_db.conn())?, - }; - let cur_epoch = SortitionDB::get_stacks_epoch( - self.sortition_db.conn(), - before_canonical_snapshot.block_height, - )? - .unwrap_or_else(|| { - panic!( - "BUG: no epoch defined at height {}", - before_canonical_snapshot.block_height - ) - }); - - if self.affirmation_maps_active(&cur_epoch.epoch_id) { - self.handle_affirmation_reorg()?; - } - // Retrieve canonical burnchain chain tip from the BurnchainBlocksDB let canonical_snapshot = match self.canonical_sortition_tip.as_ref() { Some(sn_tip) => SortitionDB::get_block_snapshot(self.sortition_db.conn(), sn_tip)? .unwrap_or_else(|| { - panic!( - "FATAL: do not have previously-calculated highest valid sortition tip {}", - sn_tip - ) + panic!("FATAL: do not have previously-calculated highest valid sortition tip {sn_tip}") }), None => SortitionDB::get_canonical_burn_chain_tip(self.sortition_db.conn())?, }; let canonical_burnchain_tip = self.burnchain_blocks_db.get_canonical_chain_tip()?; - let canonical_affirmation_map = - self.get_canonical_affirmation_map(&canonical_snapshot.sortition_id)?; let heaviest_am = self.get_heaviest_affirmation_map(&canonical_snapshot.sortition_id)?; @@ -2594,8 +1940,8 @@ impl< .unwrap_or(u64::MAX); debug!( - "Process burn block {} reward cycle {} in {}", - header.block_height, reward_cycle, &self.burnchain.working_dir, + "Process burn block {} reward cycle {reward_cycle} in {}", + header.block_height, &self.burnchain.working_dir, ); // calculate paid rewards during this burnchain block if we announce @@ -2616,12 +1962,9 @@ impl< if let Some(rc_info) = reward_cycle_info.as_mut() { if let Some(missing_anchor_block) = - self.check_missing_anchor_block(&header, &canonical_affirmation_map, rc_info)? + self.check_missing_anchor_block(&header, rc_info)? { - info!( - "Burnchain block processing stops due to missing affirmed anchor stacks block hash {}", - &missing_anchor_block - ); + info!("Burnchain block processing stops due to missing affirmed anchor stacks block hash {missing_anchor_block}"); return Ok(Some(missing_anchor_block)); } } @@ -3101,148 +2444,6 @@ impl< Ok(()) } - /// Verify that a PoX anchor block candidate is affirmed by the network. - /// Returns Ok(Some(pox_anchor)) if so. - /// Returns Ok(None) if not. - /// Returns Err(Error::NotPoXAnchorBlock) if this block got F*w confirmations but is not the - /// heaviest-confirmed burnchain block. - fn check_pox_anchor_affirmation( - &self, - pox_anchor: &BlockHeaderHash, - winner_snapshot: &BlockSnapshot, - ) -> Result, Error> { - if BurnchainDB::is_anchor_block( - self.burnchain_blocks_db.conn(), - &winner_snapshot.burn_header_hash, - &winner_snapshot.winning_block_txid, - )? { - // affirmed? - let canonical_sortition_tip = self.canonical_sortition_tip.clone().expect( - "FAIL: processing a new Stacks block, but don't have a canonical sortition tip", - ); - let heaviest_am = self.get_heaviest_affirmation_map(&canonical_sortition_tip)?; - - let commit = BurnchainDB::get_block_commit( - self.burnchain_blocks_db.conn(), - &winner_snapshot.burn_header_hash, - &winner_snapshot.winning_block_txid, - )? - .expect("BUG: no commit metadata in DB for existing commit"); - - let commit_md = BurnchainDB::get_commit_metadata( - self.burnchain_blocks_db.conn(), - &winner_snapshot.burn_header_hash, - &winner_snapshot.winning_block_txid, - )? - .expect("BUG: no commit metadata in DB for existing commit"); - - let reward_cycle = commit_md - .anchor_block - .expect("BUG: anchor block commit has no anchor block reward cycle"); - - if heaviest_am - .at(reward_cycle) - .unwrap_or(&AffirmationMapEntry::PoxAnchorBlockPresent) - == &AffirmationMapEntry::PoxAnchorBlockPresent - { - // yup, we're expecting this - debug!("Discovered an old anchor block: {}", pox_anchor; - "height" => commit.block_height, - "burn_block_hash" => %commit.burn_header_hash, - "stacks_block_hash" => %commit.block_header_hash, - "reward_cycle" => reward_cycle, - "heaviest_affirmation_map" => %heaviest_am - ); - info!("Discovered an old anchor block: {}", pox_anchor; - "height" => commit.block_height, - "burn_block_hash" => %commit.burn_header_hash, - "stacks_block_hash" => %commit.block_header_hash, - "reward_cycle" => reward_cycle - ); - return Ok(Some(pox_anchor.clone())); - } else { - // nope -- can ignore - debug!("Discovered unaffirmed old anchor block: {}", pox_anchor; - "height" => commit.block_height, - "burn_block_hash" => %commit.burn_header_hash, - "stacks_block_hash" => %commit.block_header_hash, - "reward_cycle" => reward_cycle, - "heaviest_affirmation_map" => %heaviest_am - ); - return Ok(None); - } - } else { - debug!("Stacks block {} received F*w confirmations but is not the heaviest-confirmed burnchain block, so treating as non-anchor block", pox_anchor); - return Err(Error::NotPoXAnchorBlock); - } - } - - /// Figure out what to do with a newly-discovered anchor block, based on the canonical - /// affirmation map. If the anchor block is affirmed, then returns Some(anchor-block-hash). - /// Otherwise, returns None. - /// - /// Returning Some(...) means "we need to go and process the reward cycle info from this anchor - /// block." - /// - /// Returning None means "we can keep processing Stacks blocks" - #[cfg_attr(test, mutants::skip)] - fn consider_pox_anchor( - &self, - pox_anchor: &BlockHeaderHash, - pox_anchor_snapshot: &BlockSnapshot, - ) -> Result, Error> { - // use affirmation maps even if they're not supported yet. - // if the chain is healthy, this won't cause a chain split. - match self.check_pox_anchor_affirmation(pox_anchor, pox_anchor_snapshot) { - Ok(Some(pox_anchor)) => { - // yup, affirmed. Report it for subsequent reward cycle calculation. - let block_id = StacksBlockId::new(&pox_anchor_snapshot.consensus_hash, &pox_anchor); - if !StacksChainState::has_stacks_block(self.chain_state_db.db(), &block_id)? { - debug!( - "Have NOT processed anchor block {}/{}", - &pox_anchor_snapshot.consensus_hash, pox_anchor - ); - } else { - // already have it - debug!( - "Already have processed anchor block {}/{}", - &pox_anchor_snapshot.consensus_hash, pox_anchor - ); - } - return Ok(Some(pox_anchor)); - } - Ok(None) => { - // unaffirmed old anchor block, so no rewind is needed. - debug!( - "Unaffirmed old anchor block {}/{}", - &pox_anchor_snapshot.consensus_hash, pox_anchor - ); - return Ok(None); - } - Err(Error::NotPoXAnchorBlock) => { - // what epoch is this block in? - let cur_epoch = SortitionDB::get_stacks_epoch( - self.sortition_db.conn(), - pox_anchor_snapshot.block_height, - )? - .unwrap_or_else(|| { - panic!( - "BUG: no epoch defined at height {}", - pox_anchor_snapshot.block_height - ) - }); - if cur_epoch.epoch_id < StacksEpochId::Epoch21 { - panic!("FATAL: found Stacks block that 2.0/2.05 rules would treat as an anchor block, but that 2.1+ would not"); - } - return Ok(None); - } - Err(e) => { - error!("Failed to check PoX affirmation: {:?}", &e); - return Err(e); - } - } - } - /// /// Process any ready staging blocks until there are either: /// * there are no more to process @@ -3316,13 +2517,6 @@ impl< ); let block_hash = block_receipt.header.anchored_header.block_hash(); - let winner_snapshot = SortitionDB::get_block_snapshot_for_winning_stacks_block( - &self.sortition_db.index_conn(), - &canonical_sortition_tip, - &block_hash, - ) - .expect("FAIL: could not find block snapshot for winning block hash") - .expect("FAIL: could not find block snapshot for winning block hash"); // update cost estimator if let Some(ref mut estimator) = self.cost_estimator { @@ -3363,37 +2557,9 @@ impl< .sortition_db .is_stacks_block_pox_anchor(&block_hash, &canonical_sortition_tip)? { - debug!( - "Discovered PoX anchor block {} off of canonical sortition tip {}", - &block_hash, &canonical_sortition_tip - ); + debug!("Discovered PoX anchor block {block_hash} off of canonical sortition tip {canonical_sortition_tip}"); - // what epoch is this block in? - let cur_epoch = SortitionDB::get_stacks_epoch( - self.sortition_db.conn(), - winner_snapshot.block_height, - )? - .unwrap_or_else(|| { - panic!( - "BUG: no epoch defined at height {}", - winner_snapshot.block_height - ) - }); - - if self.affirmation_maps_active(&cur_epoch.epoch_id) { - if let Some(pox_anchor) = - self.consider_pox_anchor(&pox_anchor, &winner_snapshot)? - { - return Ok(Some(pox_anchor)); - } - } else { - // 2.0/2.05 behavior: only consult the sortition DB - // if, just after processing the block, we _know_ that this block is a pox anchor, that means - // that sortitions have already begun processing that didn't know about this pox anchor. - // we need to trigger an unwind - info!("Discovered an old anchor block: {}", &pox_anchor); - return Ok(Some(pox_anchor)); - } + return Ok(Some(pox_anchor)); } } } @@ -3434,8 +2600,7 @@ impl< let mut prep_end = self .sortition_db .get_prepare_end_for(sortition_id, &block_id)? - .unwrap_or_else(|| panic!("FAIL: expected to get a sortition for a chosen anchor block {}, but not found.", - &block_id)); + .unwrap_or_else(|| panic!("FAIL: expected to get a sortition for a chosen anchor block {block_id}, but not found.")); // was this block a pox anchor for an even earlier reward cycle? while let Some(older_prep_end) = self From c695609858eca93f063c5fca7a54bc876c32bb97 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 15:12:34 -0700 Subject: [PATCH 02/35] Remove check_chainstate_against_burnchain_affirmations Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 119 -------------------- 1 file changed, 119 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 7cb0adf654..9a00462f1d 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1334,125 +1334,6 @@ impl< return Ok((FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, 0)); } - /// Did the network affirm a different history of sortitions than what our sortition DB and - /// stacks DB indicate? This checks both the affirmation map represented by the Stacks chain - /// tip and the affirmation map represented by the sortition tip against the heaviest - /// affirmation map. Both checks are necessary, because both Stacks and sortition state may - /// need to be invalidated in order to process the new set of sortitions and Stacks blocks that - /// are consistent with the heaviest affirmation map. - /// - /// If so, then return the reward cycle at which they diverged. - /// If not, return None. - fn check_chainstate_against_burnchain_affirmations(&self) -> Result, Error> { - let canonical_burnchain_tip = self.burnchain_blocks_db.get_canonical_chain_tip()?; - let (canonical_ch, canonical_bhh) = - SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortition_db.conn())?; - - let sortition_tip = match &self.canonical_sortition_tip { - Some(tip) => tip.clone(), - None => { - let sn = - SortitionDB::get_canonical_burn_chain_tip(self.burnchain_blocks_db.conn())?; - sn.sortition_id - } - }; - let stacks_tip_affirmation_map = static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db, - &sortition_tip, - &canonical_ch, - &canonical_bhh, - )?; - - let sortition_tip_affirmation_map = self - .sortition_db - .find_sortition_tip_affirmation_map(&sortition_tip)?; - - let heaviest_am = self.get_heaviest_affirmation_map(&sortition_tip)?; - - let canonical_affirmation_map = self.get_canonical_affirmation_map(&sortition_tip)?; - - debug!( - "Heaviest anchor block affirmation map is `{}` at height {}, Stacks tip is `{}`, sortition tip is `{}`, canonical is `{}`", - &heaviest_am, - canonical_burnchain_tip.block_height, - &stacks_tip_affirmation_map, - &sortition_tip_affirmation_map, - &canonical_affirmation_map, - ); - - // NOTE: a.find_divergence(b) will be `Some(..)` even if a and b have the same prefix, - // but b happens to be longer. So, we need to check both `stacks_tip_affirmation_map` - // and `heaviest_am` against each other depending on their lengths. - let stacks_changed_reward_cycle_opt = { - if heaviest_am.len() <= stacks_tip_affirmation_map.len() { - stacks_tip_affirmation_map.find_divergence(&heaviest_am) - } else { - heaviest_am.find_divergence(&stacks_tip_affirmation_map) - } - }; - - let mut sortition_changed_reward_cycle_opt = { - if heaviest_am.len() <= sortition_tip_affirmation_map.len() { - sortition_tip_affirmation_map.find_divergence(&heaviest_am) - } else { - heaviest_am.find_divergence(&sortition_tip_affirmation_map) - } - }; - - if sortition_changed_reward_cycle_opt.is_none() - && sortition_tip_affirmation_map.len() >= heaviest_am.len() - && sortition_tip_affirmation_map.len() <= canonical_affirmation_map.len() - { - if let Some(divergence_rc) = - canonical_affirmation_map.find_divergence(&sortition_tip_affirmation_map) - { - if divergence_rc + 1 >= (heaviest_am.len() as u64) { - // this can arise if there are unaffirmed PoX anchor blocks that are not - // reflected in the sortiiton affirmation map - debug!("Update sortition-changed reward cycle to {} from canonical affirmation map `{}` (sortition AM is `{}`)", - divergence_rc, &canonical_affirmation_map, &sortition_tip_affirmation_map); - - sortition_changed_reward_cycle_opt = Some(divergence_rc); - } - } - } - - // find the lowest of the two - let lowest_changed_reward_cycle_opt = match ( - stacks_changed_reward_cycle_opt, - sortition_changed_reward_cycle_opt, - ) { - (Some(a), Some(b)) => { - if a < b { - Some(a) - } else { - Some(b) - } - } - (Some(a), None) => Some(a), - (None, Some(b)) => Some(b), - (None, None) => None, - }; - - // did the canonical affirmation map change? - if let Some(changed_reward_cycle) = lowest_changed_reward_cycle_opt { - let current_reward_cycle = self - .burnchain - .block_height_to_reward_cycle(canonical_burnchain_tip.block_height) - .unwrap_or(0); - if changed_reward_cycle < current_reward_cycle { - info!("Sortition anchor block affirmation map `{}` and/or Stacks affirmation map `{}` is no longer compatible with heaviest affirmation map {} in reward cycles {}-{}", - &sortition_tip_affirmation_map, &stacks_tip_affirmation_map, &heaviest_am, changed_reward_cycle, current_reward_cycle); - - return Ok(Some(changed_reward_cycle)); - } - } - - // no reorog - Ok(None) - } - /// Find valid sortitions between two given heights, and given the correct affirmation map. /// Returns a height-sorted list of block snapshots whose affirmation maps are cosnistent with /// the correct affirmation map. From da251cfb81582e0b6460466af808f9aae369d3b3 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 15:26:17 -0700 Subject: [PATCH 03/35] Remove undo_stacks_block_orphaning Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 42 --------------------- 1 file changed, 42 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 9a00462f1d..938ef47a52 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1510,48 +1510,6 @@ impl< } } - /// Forget that stacks blocks for now-invalidated sortitions are orphaned, because they might - /// now be valid. In particular, this applies to a Stacks block that got mined in two PoX - /// forks. This can happen at most once between the two forks, but we need to ensure that the - /// block can be re-processed in that event. - fn undo_stacks_block_orphaning( - burnchain_conn: &DBConn, - burnchain_indexer: &B, - ic: &SortitionDBConn, - chainstate_db_tx: &mut DBTx, - first_invalidate_start_block: u64, - last_invalidate_start_block: u64, - ) -> Result<(), Error> { - debug!( - "Clear all orphans in burn range {} - {}", - first_invalidate_start_block, last_invalidate_start_block - ); - for burn_height in first_invalidate_start_block..(last_invalidate_start_block + 1) { - let burn_header = match BurnchainDB::get_burnchain_header( - burnchain_conn, - burnchain_indexer, - burn_height, - )? { - Some(hdr) => hdr, - None => { - continue; - } - }; - - debug!( - "Clear all orphans at {},{}", - &burn_header.block_hash, burn_header.block_height - ); - forget_orphan_stacks_blocks( - ic, - chainstate_db_tx, - &burn_header.block_hash, - burn_height.saturating_sub(1), - )?; - } - Ok(()) - } - /// Try to revalidate a sortition if it exists already. This can happen if the node flip/flops /// between two PoX forks. /// From fac38a77acc4965c06c2d7f1071189e5cdbf617b Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 15:47:52 -0700 Subject: [PATCH 04/35] Remove affirmation_maps_active Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 938ef47a52..4e1e684359 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -34,9 +34,7 @@ use crate::burnchains::db::{BurnchainBlockData, BurnchainDB, BurnchainHeaderRead use crate::burnchains::{ Burnchain, BurnchainBlockHeader, Error as BurnchainError, PoxConstants, Txid, }; -use crate::chainstate::burn::db::sortdb::{ - SortitionDB, SortitionDBConn, SortitionDBTx, SortitionHandleTx, -}; +use crate::chainstate::burn::db::sortdb::{SortitionDB, SortitionDBTx, SortitionHandleTx}; use crate::chainstate::burn::operations::leader_block_commit::RewardSetInfo; use crate::chainstate::burn::operations::BlockstackOperationType; use crate::chainstate::burn::{BlockSnapshot, ConsensusHash}; @@ -1658,16 +1656,6 @@ impl< }) } - /// Are affirmation maps active during the epoch? - fn affirmation_maps_active(&self, epoch: &StacksEpochId) -> bool { - if *epoch >= StacksEpochId::Epoch21 { - return true; - } else if self.config.always_use_affirmation_maps { - return true; - } - return false; - } - // TODO: add tests from mutation testing results #4852 #[cfg_attr(test, mutants::skip)] /// Handle a new burnchain block, optionally rolling back the canonical PoX sortition history From 4c7abfc5b17b46863619f5b35c89549b1ac14393 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 16:12:59 -0700 Subject: [PATCH 05/35] Remove find_invalid_and_revalidated_sortitions, get_snapshots_and_affirmation_maps_at_height, and find_valid_sortitions Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 197 -------------------- 1 file changed, 197 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 4e1e684359..b8ae9bb86f 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1130,27 +1130,6 @@ impl< } } - /// Get all block snapshots and their affirmation maps at a given burnchain block height. - fn get_snapshots_and_affirmation_maps_at_height( - &self, - height: u64, - ) -> Result, Error> { - let sort_ids = SortitionDB::get_sortition_ids_at_height(self.sortition_db.conn(), height)?; - let mut ret = Vec::with_capacity(sort_ids.len()); - - for sort_id in sort_ids.iter() { - let sn = SortitionDB::get_block_snapshot(self.sortition_db.conn(), sort_id)? - .expect("FATAL: have sortition ID without snapshot"); - - let sort_am = self - .sortition_db - .find_sortition_tip_affirmation_map(sort_id)?; - ret.push((sn, sort_am)); - } - - Ok(ret) - } - fn get_heaviest_affirmation_map( &self, sortition_tip: &SortitionId, @@ -1332,182 +1311,6 @@ impl< return Ok((FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, 0)); } - /// Find valid sortitions between two given heights, and given the correct affirmation map. - /// Returns a height-sorted list of block snapshots whose affirmation maps are cosnistent with - /// the correct affirmation map. - fn find_valid_sortitions( - &self, - compare_am: &AffirmationMap, - start_height: u64, - end_height: u64, - ) -> Result<(u64, Vec), Error> { - // careful -- we might have already procesed sortitions in this - // reward cycle with this PoX ID, but that were never confirmed - // by a subsequent prepare phase. - let mut last_invalidate_start_block = start_height; - let mut valid_sortitions = vec![]; - for height in start_height..(end_height + 1) { - let snapshots_and_ams = self.get_snapshots_and_affirmation_maps_at_height(height)?; - let num_sns = snapshots_and_ams.len(); - debug!("{} snapshots at {}", num_sns, height); - - let mut found = false; - for (sn, sn_am) in snapshots_and_ams.into_iter() { - debug!( - "Snapshot {} height {} has AM `{sn_am}` (is prefix of `{compare_am}`?: {})", - &sn.sortition_id, - sn.block_height, - &compare_am.has_prefix(&sn_am), - ); - if compare_am.has_prefix(&sn_am) { - // have already processed this sortitoin - debug!("Already processed sortition {} at height {} with AM `{sn_am}` on comparative affirmation map {compare_am}", &sn.sortition_id, sn.block_height); - found = true; - last_invalidate_start_block = height; - debug!( - "last_invalidate_start_block = {}", - last_invalidate_start_block - ); - valid_sortitions.push(sn); - break; - } - } - if !found && num_sns > 0 { - // there are snapshots, and they're all diverged - debug!( - "No snapshot at height {} has an affirmation map that is a prefix of `{}`", - height, &compare_am - ); - break; - } - } - Ok((last_invalidate_start_block, valid_sortitions)) - } - - /// Find out which sortitions will need to be invalidated as part of a PoX reorg, and which - /// ones will need to be re-validated. - /// - /// Returns (first-invalidation-height, last-invalidation-height, revalidation-sort-ids). - /// * All sortitions in the range [first-invalidation-height, last-invalidation-height) must be - /// invalidated, since they are no longer consistent with the heaviest affirmation map. These - /// heights fall into the reward cycles identified by `changed_reward_cycle` and - /// `current_reward_cycle`. - /// * The sortitions identified by `revalidate-sort-ids` are sortitions whose heights come - /// at or after `last-invalidation-height` but are now valid again (i.e. because they are - /// consistent with the heaviest affirmation map). - fn find_invalid_and_revalidated_sortitions( - &self, - compare_am: &AffirmationMap, - changed_reward_cycle: u64, - current_reward_cycle: u64, - ) -> Result)>, Error> { - // find the lowest reward cycle we have to reprocess (which starts at burn - // block rc_start_block). - - // burn chain height at which we'll invalidate *all* sortitions - let mut last_invalidate_start_block = 0; - - // burn chain height at which we'll re-try orphaned Stacks blocks, and - // revalidate the sortitions that were previously invalid but have now been - // made valid. - let mut first_invalidate_start_block = 0; - - // set of sortitions that are currently invalid, but could need to be reset - // as valid. - let mut valid_sortitions = vec![]; - - let canonical_burnchain_tip = self.burnchain_blocks_db.get_canonical_chain_tip()?; - let mut diverged = false; - for rc in changed_reward_cycle..current_reward_cycle { - debug!( - "Find invalidated and revalidated sortitions at reward cycle {}", - rc - ); - - last_invalidate_start_block = self.burnchain.reward_cycle_to_block_height(rc); - first_invalidate_start_block = last_invalidate_start_block; - - // + 1 because the first sortition of a reward cycle is congruent to 1 mod - // reward_cycle_length. - let sort_ids = SortitionDB::get_sortition_ids_at_height( - self.sortition_db.conn(), - last_invalidate_start_block + 1, - )?; - - // find the sortition ID with the shortest affirmation map that is NOT a prefix - // of the heaviest affirmation map - let mut found_diverged = false; - for sort_id in sort_ids.iter() { - let sort_am = self - .sortition_db - .find_sortition_tip_affirmation_map(sort_id)?; - - debug!( - "Compare {compare_am} as prefix of {sort_am}? {}", - compare_am.has_prefix(&sort_am) - ); - if compare_am.has_prefix(&sort_am) { - continue; - } - - let mut prior_compare_am = compare_am.clone(); - prior_compare_am.pop(); - - let mut prior_sort_am = sort_am.clone(); - prior_sort_am.pop(); - - debug!( - "Compare {} as a prior prefix of {}? {}", - &prior_compare_am, - &prior_sort_am, - prior_compare_am.has_prefix(&prior_sort_am) - ); - if prior_compare_am.has_prefix(&prior_sort_am) { - // this is the first reward cycle where history diverged. - found_diverged = true; - debug!("{sort_am} diverges from {compare_am}"); - - // careful -- we might have already procesed sortitions in this - // reward cycle with this PoX ID, but that were never confirmed - // by a subsequent prepare phase. - let (new_last_invalidate_start_block, mut next_valid_sortitions) = self - .find_valid_sortitions( - compare_am, - last_invalidate_start_block, - canonical_burnchain_tip.block_height, - )?; - last_invalidate_start_block = new_last_invalidate_start_block; - valid_sortitions.append(&mut next_valid_sortitions); - break; - } - } - - if !found_diverged { - continue; - } - - // we may have processed some sortitions correctly within this reward - // cycle. Advance forward until we find one that we haven't. - info!( - "Re-playing sortitions starting within reward cycle {} burn height {}", - rc, last_invalidate_start_block - ); - - diverged = true; - break; - } - - if diverged { - Ok(Some(( - first_invalidate_start_block, - last_invalidate_start_block, - valid_sortitions, - ))) - } else { - Ok(None) - } - } - /// Try to revalidate a sortition if it exists already. This can happen if the node flip/flops /// between two PoX forks. /// From 32aa1a642e5108315db6b5b3d433687042b238f2 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 16:24:32 -0700 Subject: [PATCH 06/35] Remove check for compatible_stacks_blocks in handle_new_epoch2_burnchain_block Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 42 ++------------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index b8ae9bb86f..975881451f 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1333,8 +1333,7 @@ impl< let parent_pox = { let mut sortition_db_handle = SortitionHandleTx::begin(&mut self.sortition_db, &parent_sort_id)?; - let parent_pox = sortition_db_handle.get_pox_id()?; - parent_pox + sortition_db_handle.get_pox_id()? }; let new_sortition_id = @@ -1345,8 +1344,8 @@ impl< if let Some(sortition) = sortition_opt { // existing sortition -- go revalidate it info!( - "Revalidate already-processed snapshot {} height {} to have canonical tip {}/{} height {}", - &new_sortition_id, sortition.block_height, + "Revalidate already-processed snapshot {new_sortition_id} height {} to have canonical tip {}/{} height {}", + sortition.block_height, &canonical_snapshot.canonical_stacks_tip_consensus_hash, &canonical_snapshot.canonical_stacks_tip_hash, canonical_snapshot.canonical_stacks_tip_height, @@ -1684,44 +1683,11 @@ impl< // don't process this burnchain block again in this recursive call. already_processed_burn_blocks.insert(next_snapshot.burn_header_hash); - let mut compatible_stacks_blocks = vec![]; - { - // get borrow checker to drop sort_tx - let mut sort_tx = self.sortition_db.tx_begin()?; - for (ch, bhh, height) in stacks_blocks_to_reaccept.into_iter() { - debug!( - "Check if Stacks block {}/{} height {} is compatible with `{}`", - &ch, &bhh, height, &heaviest_am - ); - - let am = inner_static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - last_2_05_rc, - &sort_tx.find_sortition_tip_affirmation_map(&next_snapshot.sortition_id)?, - &sort_tx, - &ch, - &bhh, - )?; - if StacksChainState::is_block_compatible_with_affirmation_map( - &am, - &heaviest_am, - )? { - debug!( - "Stacks block {}/{} height {} is compatible with `{}`; will reaccept", - &ch, &bhh, height, &heaviest_am - ); - compatible_stacks_blocks.push((ch, bhh, height)); - } else { - debug!("Stacks block {}/{} height {} is NOT compatible with `{}`; will NOT reaccept", &ch, &bhh, height, &heaviest_am); - } - } - } - // reaccept any stacks blocks let mut sortition_db_handle = SortitionHandleTx::begin(&mut self.sortition_db, &next_snapshot.sortition_id)?; - for (ch, bhh, height) in compatible_stacks_blocks.into_iter() { + for (ch, bhh, height) in stacks_blocks_to_reaccept.into_iter() { debug!("Re-accept Stacks block {}/{} height {}", &ch, &bhh, height); revalidated_stacks_block = true; sortition_db_handle.set_stacks_block_accepted(&ch, &bhh, height)?; From 2d57bce026236b89bf0e1c88a65331b09b1a752d Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 23 Jul 2025 17:02:59 -0700 Subject: [PATCH 07/35] Remove find_highest_stacks_block_with_compatible_affirmation_map Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 233 -------------------- 1 file changed, 233 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 975881451f..9bba474011 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1156,161 +1156,6 @@ impl< sortition_tip, ) } - - /// Find the canonical Stacks tip at a given sortition, whose affirmation map is compatible - /// with the heaviest affirmation map. - fn find_highest_stacks_block_with_compatible_affirmation_map( - heaviest_am: &AffirmationMap, - sort_tip: &SortitionId, - burnchain_db: &BurnchainDB, - sort_tx: &mut SortitionDBTx, - chainstate_conn: &DBConn, - ) -> Result<(ConsensusHash, BlockHeaderHash, u64), Error> { - let mut search_height = StacksChainState::get_max_header_height(chainstate_conn)?; - let last_2_05_rc = SortitionDB::static_get_last_epoch_2_05_reward_cycle( - sort_tx, - sort_tx.context.first_block_height, - &sort_tx.context.pox_constants, - )?; - let sort_am = sort_tx.find_sortition_tip_affirmation_map(sort_tip)?; - loop { - let mut search_weight = StacksChainState::get_max_affirmation_weight_at_height( - chainstate_conn, - search_height, - )? as i64; - while search_weight >= 0 { - let all_headers = StacksChainState::get_all_headers_at_height_and_weight( - chainstate_conn, - search_height, - search_weight as u64, - )?; - debug!( - "Headers with weight {} height {}: {}", - search_weight, - search_height, - all_headers.len() - ); - - search_weight -= 1; - - for hdr in all_headers { - // load this block's affirmation map - let am = match inner_static_get_stacks_tip_affirmation_map( - burnchain_db, - last_2_05_rc, - &sort_am, - sort_tx, - &hdr.consensus_hash, - &hdr.anchored_header.block_hash(), - ) { - Ok(am) => am, - Err(Error::ChainstateError(ChainstateError::DBError( - DBError::InvalidPoxSortition, - ))) => { - debug!( - "Stacks tip {}/{} is not on a valid sortition", - &hdr.consensus_hash, - &hdr.anchored_header.block_hash() - ); - continue; - } - Err(e) => { - error!("Failed to query affirmation map: {e:?}"); - return Err(e); - } - }; - - // must be compatible with the heaviest AM - match StacksChainState::is_block_compatible_with_affirmation_map( - &am, - heaviest_am, - ) { - Ok(compat) => { - if !compat { - debug!("Stacks tip {}/{} affirmation map {} is incompatible with heaviest affirmation map {}", - &hdr.consensus_hash, &hdr.anchored_header.block_hash(), &am, &heaviest_am); - continue; - } - } - Err(ChainstateError::DBError(DBError::InvalidPoxSortition)) => { - debug!( - "Stacks tip {}/{} affirmation map {} is not on a valid sortition", - &hdr.consensus_hash, - &hdr.anchored_header.block_hash(), - &am - ); - continue; - } - Err(e) => { - error!("Failed to query affirmation compatibility: {:?}", &e); - return Err(e.into()); - } - } - - // must reside on this sortition fork - let ancestor_sn = match SortitionDB::get_ancestor_snapshot_tx( - sort_tx, - hdr.burn_header_height.into(), - sort_tip, - ) { - Ok(Some(sn)) => sn, - Ok(None) | Err(DBError::InvalidPoxSortition) => { - debug!("Stacks tip {}/{} affirmation map {} is not on a chain tipped by sortition {}", - &hdr.consensus_hash, &hdr.anchored_header.block_hash(), &am, sort_tip); - continue; - } - Err(e) => { - error!( - "Failed to query snapshot ancestor at height {} from {}: {:?}", - hdr.burn_header_height, sort_tip, &e - ); - return Err(e.into()); - } - }; - if !ancestor_sn.sortition - || ancestor_sn.winning_stacks_block_hash != hdr.anchored_header.block_hash() - || ancestor_sn.consensus_hash != hdr.consensus_hash - { - debug!( - "Stacks tip {}/{} affirmation map {} is not attched to {},{}", - &hdr.consensus_hash, - &hdr.anchored_header.block_hash(), - &am, - &ancestor_sn.burn_header_hash, - ancestor_sn.block_height - ); - continue; - } - - // found it! - debug!( - "Canonical Stacks tip of {} is now {}/{} height {} burn height {} AM `{}` weight {}", - sort_tip, - &hdr.consensus_hash, - &hdr.anchored_header.block_hash(), - hdr.stacks_block_height, - hdr.burn_header_height, - &am, - am.weight() - ); - return Ok(( - hdr.consensus_hash, - hdr.anchored_header.block_hash(), - hdr.stacks_block_height, - )); - } - } - if search_height == 0 { - break; - } else { - search_height -= 1; - } - } - - // empty chainstate - return Ok((FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH, 0)); - } - /// Try to revalidate a sortition if it exists already. This can happen if the node flip/flops /// between two PoX forks. /// @@ -1483,9 +1328,6 @@ impl< }; let canonical_burnchain_tip = self.burnchain_blocks_db.get_canonical_chain_tip()?; - - let heaviest_am = self.get_heaviest_affirmation_map(&canonical_snapshot.sortition_id)?; - debug!("Handle new canonical burnchain tip"; "height" => %canonical_burnchain_tip.block_height, "block_hash" => %canonical_burnchain_tip.block_hash.to_string()); @@ -1754,81 +1596,6 @@ impl< } } - // make sure our memoized canonical stacks tip is correct - let chainstate_db_conn = self.chain_state_db.db(); - let mut sort_tx = self.sortition_db.tx_begin()?; - - // Retrieve canonical burnchain chain tip from the BurnchainBlocksDB - let canonical_snapshot = match self.canonical_sortition_tip.as_ref() { - Some(sn_tip) => { - SortitionDB::get_block_snapshot(&sort_tx, sn_tip)?.unwrap_or_else(|| { - panic!( - "FATAL: do not have previously-calculated highest valid sortition tip {}", - sn_tip - ) - }) - } - None => SortitionDB::get_canonical_burn_chain_tip(&sort_tx)?, - }; - let highest_valid_sortition_id = canonical_snapshot.sortition_id; - - let (canonical_ch, canonical_bhh, canonical_height) = - Self::find_highest_stacks_block_with_compatible_affirmation_map( - &heaviest_am, - &highest_valid_sortition_id, - &self.burnchain_blocks_db, - &mut sort_tx, - chainstate_db_conn, - ) - .expect("FATAL: could not find a valid parent Stacks block"); - - let stacks_am = inner_static_get_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - last_2_05_rc, - &sort_tx.find_sortition_tip_affirmation_map(&highest_valid_sortition_id)?, - &sort_tx, - &canonical_ch, - &canonical_bhh, - ) - .expect("FATAL: failed to query stacks DB"); - - debug!( - "Canonical Stacks tip after burnchain processing is {}/{} height {} am `{}`", - &canonical_ch, &canonical_bhh, canonical_height, &stacks_am - ); - debug!( - "Canonical sortition tip after burnchain processing is {},{}", - &highest_valid_sortition_id, canonical_snapshot.block_height - ); - - let highest_valid_sn = - SortitionDB::get_block_snapshot(&sort_tx, &highest_valid_sortition_id)? - .expect("FATAL: no snapshot for highest valid sortition ID"); - - let block_known = StacksChainState::is_stacks_block_processed( - chainstate_db_conn, - &highest_valid_sn.consensus_hash, - &highest_valid_sn.winning_stacks_block_hash, - ) - .expect("FATAL: failed to query chainstate DB"); - - SortitionDB::revalidate_snapshot_with_block( - &sort_tx, - &highest_valid_sortition_id, - &canonical_ch, - &canonical_bhh, - canonical_height, - Some(block_known), - ) - .unwrap_or_else(|_| { - panic!( - "FATAL: failed to revalidate highest valid sortition {}", - &highest_valid_sortition_id - ) - }); - - sort_tx.commit()?; - debug!("Done handling new burnchain blocks"); Ok(None) From 4f43f7f962e8dfa901a40284992d6fb9572b72ce Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 24 Jul 2025 09:45:23 -0700 Subject: [PATCH 08/35] Remove consolidate_affirmation_maps, *_get_*_affirmation_map, is_block_compatible_with_affirmation_map Signed-off-by: Jacinta Ferrant --- stacks-node/src/run_loop/neon.rs | 190 +------------------ stackslib/src/chainstate/coordinator/mod.rs | 164 +--------------- stackslib/src/chainstate/stacks/db/blocks.rs | 23 --- stackslib/src/net/api/getinfo.rs | 8 +- stackslib/src/net/api/tests/mod.rs | 28 +-- stackslib/src/net/inv/epoch2x.rs | 23 +-- stackslib/src/net/mod.rs | 3 +- stackslib/src/net/p2p.rs | 65 +------ stackslib/src/net/tests/inv/epoch2x.rs | 2 +- 9 files changed, 25 insertions(+), 481 deletions(-) diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index bdb370527d..a4c9b7787a 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -13,9 +13,8 @@ use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash}; use stacks::chainstate::coordinator::comm::{CoordinatorChannels, CoordinatorReceivers}; use stacks::chainstate::coordinator::{ - migrate_chainstate_dbs, static_get_canonical_affirmation_map, - static_get_heaviest_affirmation_map, static_get_stacks_tip_affirmation_map, ChainsCoordinator, - ChainsCoordinatorConfig, CoordinatorCommunication, Error as coord_error, + migrate_chainstate_dbs, ChainsCoordinator, ChainsCoordinatorConfig, CoordinatorCommunication, + Error as coord_error, }; use stacks::chainstate::stacks::db::{ChainStateBootData, StacksChainState}; use stacks::chainstate::stacks::miner::{signal_mining_blocked, signal_mining_ready, MinerStatus}; @@ -794,8 +793,6 @@ impl RunLoop { fn drive_pox_reorg_stacks_block_processing( globals: &Globals, config: &Config, - burnchain: &Burnchain, - sortdb: &SortitionDB, last_stacks_pox_reorg_recover_time: &mut u128, ) { let miner_config = config.get_miner_config(); @@ -812,92 +809,10 @@ impl RunLoop { return; } - // compare stacks and heaviest AMs - let burnchain_db = burnchain - .open_burnchain_db(false) - .expect("FATAL: failed to open burnchain DB"); - - let sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) - .expect("FATAL: could not read sortition DB"); - - let indexer = make_bitcoin_indexer(config, Some(globals.should_keep_running.clone())); - - let heaviest_affirmation_map = match static_get_heaviest_affirmation_map( - burnchain, - &indexer, - &burnchain_db, - sortdb, - &sn.sortition_id, - ) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find heaviest affirmation map: {e:?}"); - return; - } - }; - - let highest_sn = SortitionDB::get_highest_known_burn_chain_tip(sortdb.conn()) - .expect("FATAL: could not read sortition DB"); - - let canonical_burnchain_tip = burnchain_db - .get_canonical_chain_tip() - .expect("FATAL: could not read burnchain DB"); - - let sortition_tip_affirmation_map = - match SortitionDB::find_sortition_tip_affirmation_map(sortdb, &sn.sortition_id) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find sortition affirmation map: {e:?}"); - return; - } - }; - - let stacks_tip_affirmation_map = static_get_stacks_tip_affirmation_map( - &burnchain_db, - sortdb, - &sn.sortition_id, - &sn.canonical_stacks_tip_consensus_hash, - &sn.canonical_stacks_tip_hash, - ) - .expect("FATAL: could not query stacks DB"); - - if stacks_tip_affirmation_map.len() < heaviest_affirmation_map.len() - || stacks_tip_affirmation_map - .find_divergence(&heaviest_affirmation_map) - .is_some() - { - // the sortition affirmation map might also be inconsistent, so we'll need to fix that - // (i.e. the underlying sortitions) before we can fix the stacks fork - if sortition_tip_affirmation_map.len() < heaviest_affirmation_map.len() - || sortition_tip_affirmation_map - .find_divergence(&heaviest_affirmation_map) - .is_some() - { - debug!("Drive burn block processing: possible PoX reorg (sortition tip: {sortition_tip_affirmation_map}, heaviest: {heaviest_affirmation_map})"); - globals.coord().announce_new_burn_block(); - } else if highest_sn.block_height == sn.block_height - && sn.block_height == canonical_burnchain_tip.block_height - { - // need to force an affirmation reorg because there will be no more burn block - // announcements. - debug!("Drive burn block processing: possible PoX reorg (sortition tip: {sortition_tip_affirmation_map}, heaviest: {heaviest_affirmation_map}, burn height {})", sn.block_height); - globals.coord().announce_new_burn_block(); - } - - debug!( - "Drive stacks block processing: possible PoX reorg (stacks tip: {stacks_tip_affirmation_map}, heaviest: {heaviest_affirmation_map})" - ); - globals.coord().announce_new_stacks_block(); - } else { - debug!( - "Drive stacks block processing: no need (stacks tip: {stacks_tip_affirmation_map}, heaviest: {heaviest_affirmation_map})" - ); - - // announce a new stacks block to force the chains coordinator - // to wake up anyways. this isn't free, so we have to make sure - // the chain-liveness thread doesn't wake up too often - globals.coord().announce_new_stacks_block(); - } + // announce a new stacks block to force the chains coordinator + // to wake up anyways. this isn't free, so we have to make sure + // the chain-liveness thread doesn't wake up too often + globals.coord().announce_new_stacks_block(); *last_stacks_pox_reorg_recover_time = get_epoch_time_secs().into(); } @@ -913,7 +828,6 @@ impl RunLoop { config: &Config, burnchain: &Burnchain, sortdb: &SortitionDB, - chain_state_db: &StacksChainState, last_burn_pox_reorg_recover_time: &mut u128, last_announce_time: &mut u128, ) { @@ -953,82 +867,6 @@ impl RunLoop { return; } - // NOTE: this could be lower than the highest_sn - let sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) - .expect("FATAL: could not read sortition DB"); - - let sortition_tip_affirmation_map = - match SortitionDB::find_sortition_tip_affirmation_map(sortdb, &sn.sortition_id) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find sortition affirmation map: {e:?}"); - return; - } - }; - - let indexer = make_bitcoin_indexer(config, Some(globals.should_keep_running.clone())); - - let heaviest_affirmation_map = match static_get_heaviest_affirmation_map( - burnchain, - &indexer, - &burnchain_db, - sortdb, - &sn.sortition_id, - ) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find heaviest affirmation map: {e:?}"); - return; - } - }; - - let canonical_affirmation_map = match static_get_canonical_affirmation_map( - burnchain, - &indexer, - &burnchain_db, - sortdb, - chain_state_db, - &sn.sortition_id, - ) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find canonical affirmation map: {e:?}"); - return; - } - }; - - if sortition_tip_affirmation_map.len() < heaviest_affirmation_map.len() - || sortition_tip_affirmation_map - .find_divergence(&heaviest_affirmation_map) - .is_some() - || sn.block_height < highest_sn.block_height - { - debug!("Drive burn block processing: possible PoX reorg (sortition tip: {sortition_tip_affirmation_map}, heaviest: {heaviest_affirmation_map}, {} = heaviest_affirmation_map.len() - && sortition_tip_affirmation_map.len() <= canonical_affirmation_map.len() - { - if let Some(divergence_rc) = - canonical_affirmation_map.find_divergence(&sortition_tip_affirmation_map) - { - if divergence_rc + 1 >= (heaviest_affirmation_map.len() as u64) { - // we have unaffirmed PoX anchor blocks that are not yet processed in the sortition history - debug!("Drive burnchain processing: possible PoX reorg from unprocessed anchor block(s) (sortition tip: {sortition_tip_affirmation_map}, heaviest: {heaviest_affirmation_map}, canonical: {canonical_affirmation_map})"); - globals.coord().announce_new_burn_block(); - globals.coord().announce_new_stacks_block(); - *last_announce_time = get_epoch_time_secs().into(); - } - } - } else { - debug!( - "Drive burn block processing: no need (sortition tip: {sortition_tip_affirmation_map}, heaviest: {heaviest_affirmation_map}, {} AffirmationMap { - let mut am_entries = vec![]; - for i in 0..last_2_05_rc { - if let Some(am_entry) = sort_am.affirmations.get(i) { - am_entries.push(*am_entry); - } else { - return AffirmationMap::new(am_entries); - } - } - for am_entry in given_am.affirmations.iter().skip(last_2_05_rc) { - am_entries.push(*am_entry); - } - - AffirmationMap::new(am_entries) -} - -/// Get the heaviest affirmation map, when considering epochs. -/// * In epoch 2.05 and prior, the heaviest AM was the sortition AM. -/// * In epoch 2.1, the reward cycles prior to the 2.1 boundary remain the sortition AM. -pub fn static_get_heaviest_affirmation_map( - burnchain: &Burnchain, - indexer: &B, - burnchain_blocks_db: &BurnchainDB, - sortition_db: &SortitionDB, - sortition_tip: &SortitionId, -) -> Result { - let last_2_05_rc = sortition_db.get_last_epoch_2_05_reward_cycle()? as usize; - - let sort_am = sortition_db.find_sortition_tip_affirmation_map(sortition_tip)?; - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_blocks_db.conn(), - burnchain, - indexer, - )?; - - Ok(consolidate_affirmation_maps( - heaviest_am, - &sort_am, - last_2_05_rc, - )) -} - -/// Get the canonical affirmation map, when considering epochs. -/// * In epoch 2.05 and prior, the heaviest AM was the sortition AM. -/// * In epoch 2.1, the reward cycles prior to the 2.1 boundary remain the sortition AM. -pub fn static_get_canonical_affirmation_map( - burnchain: &Burnchain, - indexer: &B, - burnchain_blocks_db: &BurnchainDB, - sortition_db: &SortitionDB, - chain_state_db: &StacksChainState, - sortition_tip: &SortitionId, -) -> Result { - let last_2_05_rc = sortition_db.get_last_epoch_2_05_reward_cycle()? as usize; - - let sort_am = sortition_db.find_sortition_tip_affirmation_map(sortition_tip)?; - - let canonical_am = StacksChainState::find_canonical_affirmation_map( - burnchain, - indexer, - burnchain_blocks_db, - chain_state_db, - )?; - - Ok(consolidate_affirmation_maps( - canonical_am, - &sort_am, - last_2_05_rc, - )) -} - -fn inner_static_get_stacks_tip_affirmation_map( - burnchain_blocks_db: &BurnchainDB, - last_2_05_rc: u64, - sort_am: &AffirmationMap, - sortdb_conn: &DBConn, - canonical_ch: &ConsensusHash, - canonical_bhh: &BlockHeaderHash, -) -> Result { - let last_2_05_rc = last_2_05_rc as usize; - - let stacks_am = StacksChainState::find_stacks_tip_affirmation_map( - burnchain_blocks_db, - sortdb_conn, - canonical_ch, - canonical_bhh, - )?; - - Ok(consolidate_affirmation_maps( - stacks_am, - sort_am, - last_2_05_rc, - )) -} - -/// Get the canonical Stacks tip affirmation map, when considering epochs. -/// * In epoch 2.05 and prior, the heaviest AM was the sortition AM. -/// * In epoch 2.1, the reward cycles prior to the 2.1 boundary remain the sortition AM -pub fn static_get_stacks_tip_affirmation_map( - burnchain_blocks_db: &BurnchainDB, - sortition_db: &SortitionDB, - sortition_tip: &SortitionId, - canonical_ch: &ConsensusHash, - canonical_bhh: &BlockHeaderHash, -) -> Result { - let last_2_05_rc = sortition_db.get_last_epoch_2_05_reward_cycle()?; - let sort_am = sortition_db.find_sortition_tip_affirmation_map(sortition_tip)?; - inner_static_get_stacks_tip_affirmation_map( - burnchain_blocks_db, - last_2_05_rc, - &sort_am, - sortition_db.conn(), - canonical_ch, - canonical_bhh, - ) -} - impl< T: BlockEventDispatcher, N: CoordinatorNotices, @@ -1130,32 +998,6 @@ impl< } } - fn get_heaviest_affirmation_map( - &self, - sortition_tip: &SortitionId, - ) -> Result { - static_get_heaviest_affirmation_map( - &self.burnchain, - &self.burnchain_indexer, - &self.burnchain_blocks_db, - &self.sortition_db, - sortition_tip, - ) - } - - fn get_canonical_affirmation_map( - &self, - sortition_tip: &SortitionId, - ) -> Result { - static_get_canonical_affirmation_map( - &self.burnchain, - &self.burnchain_indexer, - &self.burnchain_blocks_db, - &self.sortition_db, - &self.chain_state_db, - sortition_tip, - ) - } /// Try to revalidate a sortition if it exists already. This can happen if the node flip/flops /// between two PoX forks. /// @@ -1316,8 +1158,6 @@ impl< ) -> Result, Error> { debug!("Handle new burnchain block"); - let last_2_05_rc = self.sortition_db.get_last_epoch_2_05_reward_cycle()?; - // Retrieve canonical burnchain chain tip from the BurnchainBlocksDB let canonical_snapshot = match self.canonical_sortition_tip.as_ref() { Some(sn_tip) => SortitionDB::get_block_snapshot(self.sortition_db.conn(), sn_tip)? diff --git a/stackslib/src/chainstate/stacks/db/blocks.rs b/stackslib/src/chainstate/stacks/db/blocks.rs index 7fe642ee71..6bb07fd94a 100644 --- a/stackslib/src/chainstate/stacks/db/blocks.rs +++ b/stackslib/src/chainstate/stacks/db/blocks.rs @@ -2230,29 +2230,6 @@ impl StacksChainState { ) } - /// Is a block compatible with the heaviest affirmation map? - pub fn is_block_compatible_with_affirmation_map( - stacks_tip_affirmation_map: &AffirmationMap, - heaviest_am: &AffirmationMap, - ) -> Result { - // NOTE: a.find_divergence(b) will be `Some(..)` even if a and b have the same prefix, - // but b happens to be longer. So, we need to check both `stacks_tip_affirmation_map` - // and `heaviest_am` against each other depending on their lengths. - if (stacks_tip_affirmation_map.len() > heaviest_am.len() - && stacks_tip_affirmation_map - .find_divergence(heaviest_am) - .is_some()) - || (stacks_tip_affirmation_map.len() <= heaviest_am.len() - && heaviest_am - .find_divergence(stacks_tip_affirmation_map) - .is_some()) - { - return Ok(false); - } else { - return Ok(true); - } - } - /// Delete a microblock's data from the DB fn delete_microblock_data( tx: &mut DBTx, diff --git a/stackslib/src/net/api/getinfo.rs b/stackslib/src/net/api/getinfo.rs index acdcf1f10f..311b8c38ca 100644 --- a/stackslib/src/net/api/getinfo.rs +++ b/stackslib/src/net/api/getinfo.rs @@ -147,10 +147,10 @@ impl RPCPeerInfoData { node_public_key: Some(public_key_buf), node_public_key_hash: Some(public_key_hash), affirmations: Some(RPCAffirmationData { - heaviest: network.heaviest_affirmation_map.clone(), - stacks_tip: network.stacks_tip_affirmation_map.clone(), - sortition_tip: network.sortition_tip_affirmation_map.clone(), - tentative_best: network.tentative_best_affirmation_map.clone(), + heaviest: AffirmationMap::empty(), + stacks_tip: AffirmationMap::empty(), + sortition_tip: AffirmationMap::empty(), + tentative_best: AffirmationMap::empty(), }), last_pox_anchor: Some(RPCLastPoxAnchorData { anchor_block_hash: network.last_anchor_block_hash.clone(), diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs index b227af0763..762082468f 100644 --- a/stackslib/src/net/api/tests/mod.rs +++ b/stackslib/src/net/api/tests/mod.rs @@ -681,12 +681,7 @@ impl<'a> TestRPC<'a> { let mut peer_1_stacks_node = peer_1.stacks_node.take().unwrap(); let _ = peer_1 .network - .refresh_burnchain_view( - &peer_1_indexer, - &peer_1_sortdb, - &mut peer_1_stacks_node.chainstate, - false, - ) + .refresh_burnchain_view(&peer_1_sortdb, &mut peer_1_stacks_node.chainstate, false) .unwrap(); peer_1.sortdb = Some(peer_1_sortdb); peer_1.stacks_node = Some(peer_1_stacks_node); @@ -695,12 +690,7 @@ impl<'a> TestRPC<'a> { let mut peer_2_stacks_node = peer_2.stacks_node.take().unwrap(); let _ = peer_2 .network - .refresh_burnchain_view( - &peer_2_indexer, - &peer_2_sortdb, - &mut peer_2_stacks_node.chainstate, - false, - ) + .refresh_burnchain_view(&peer_2_sortdb, &mut peer_2_stacks_node.chainstate, false) .unwrap(); peer_2.sortdb = Some(peer_2_sortdb); peer_2.stacks_node = Some(peer_2_stacks_node); @@ -1174,12 +1164,7 @@ impl<'a> TestRPC<'a> { let _ = peer_2 .network - .refresh_burnchain_view( - &peer_2_indexer, - &peer_2_sortdb, - &mut peer_2_stacks_node.chainstate, - false, - ) + .refresh_burnchain_view(&peer_2_sortdb, &mut peer_2_stacks_node.chainstate, false) .unwrap(); if unconfirmed_state { @@ -1225,12 +1210,7 @@ impl<'a> TestRPC<'a> { let _ = peer_1 .network - .refresh_burnchain_view( - &peer_1_indexer, - &peer_1_sortdb, - &mut peer_1_stacks_node.chainstate, - false, - ) + .refresh_burnchain_view(&peer_1_sortdb, &mut peer_1_stacks_node.chainstate, false) .unwrap(); if unconfirmed_state { diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index 06e21e416f..2815cd06da 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -1808,14 +1808,7 @@ impl PeerNetwork { /// Determine at which reward cycle to begin scanning inventories pub(crate) fn get_block_scan_start(&self, sortdb: &SortitionDB) -> u64 { - // see if the stacks tip affirmation map and heaviest affirmation map diverge. If so, then - // start scaning at the reward cycle just before that. - let am_rescan_rc = self - .stacks_tip_affirmation_map - .find_inv_search(&self.heaviest_affirmation_map); - - // affirmation maps are compatible, so just resume scanning off of wherever we are at the - // tip. + // Resume scanning off of wherever we are at the tip. // NOTE: This code path only works in Stacks 2.x, but that's okay because this whole state // machine is only used in Stacks 2.x let (consensus_hash, _) = SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn()) @@ -1834,19 +1827,9 @@ impl PeerNetwork { .block_height_to_reward_cycle(stacks_tip_burn_block_height) .unwrap_or(0); - let start_reward_cycle = - stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles); + let rescan_rc = stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles); - let rescan_rc = cmp::min(am_rescan_rc, start_reward_cycle); - - test_debug!( - "begin blocks inv scan at {} = min({},{}) stacks_tip_am={} heaviest_am={}", - rescan_rc, - am_rescan_rc, - start_reward_cycle, - &self.stacks_tip_affirmation_map, - &self.heaviest_affirmation_map - ); + test_debug!("begin blocks inv scan at {rescan_rc}"); rescan_rc } diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index dff1812593..a3d2df8959 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -37,7 +37,6 @@ use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey}; use {rusqlite, url}; use self::dns::*; -use crate::burnchains::affirmation::AffirmationMap; use crate::burnchains::{Burnchain, Error as burnchain_error, Txid}; use crate::chainstate::burn::db::sortdb::SortitionDB; use crate::chainstate::burn::ConsensusHash; @@ -3530,7 +3529,7 @@ pub mod test { let indexer = BitcoinIndexer::new_unit_test(&self.config.burnchain.working_dir); self.network - .refresh_burnchain_view(&indexer, &sortdb, &mut stacks_node.chainstate, false) + .refresh_burnchain_view(&sortdb, &mut stacks_node.chainstate, false) .unwrap(); self.sortdb = Some(sortdb); diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index 3a6e5501ce..23a6bd295d 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -38,10 +38,7 @@ use crate::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::{Burnchain, BurnchainView}; 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, OnChainRewardSetProvider, RewardCycleInfo, -}; +use crate::chainstate::coordinator::{OnChainRewardSetProvider, RewardCycleInfo}; use crate::chainstate::nakamoto::coordinator::load_nakamoto_reward_set; use crate::chainstate::stacks::boot::RewardSet; use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState}; @@ -487,10 +484,6 @@ pub struct PeerNetwork { pub current_reward_sets: BTreeMap, // information about the state of the network's anchor blocks - pub heaviest_affirmation_map: AffirmationMap, - pub stacks_tip_affirmation_map: AffirmationMap, - pub sortition_tip_affirmation_map: AffirmationMap, - pub tentative_best_affirmation_map: AffirmationMap, pub last_anchor_block_hash: BlockHeaderHash, pub last_anchor_block_txid: Txid, @@ -692,10 +685,6 @@ impl PeerNetwork { chain_view, chain_view_stable_consensus_hash: ConsensusHash([0u8; 20]), ast_rules: ASTRules::Typical, - heaviest_affirmation_map: AffirmationMap::empty(), - stacks_tip_affirmation_map: AffirmationMap::empty(), - sortition_tip_affirmation_map: AffirmationMap::empty(), - tentative_best_affirmation_map: AffirmationMap::empty(), last_anchor_block_hash: BlockHeaderHash([0x00; 32]), last_anchor_block_txid: Txid([0x00; 32]), burnchain_tip: BlockSnapshot::initial( @@ -4716,9 +4705,8 @@ impl PeerNetwork { /// * hint to the download state machine to start looking for the new block at the new /// stable sortition height /// * hint to the antientropy protocol to reset to the latest reward cycle - pub fn refresh_burnchain_view( + pub fn refresh_burnchain_view( &mut self, - indexer: &B, sortdb: &SortitionDB, chainstate: &mut StacksChainState, ibd: bool, @@ -4878,38 +4866,6 @@ impl PeerNetwork { // update tx validation information self.ast_rules = SortitionDB::get_ast_rules(sortdb.conn(), canonical_sn.block_height)?; - if self.get_current_epoch().epoch_id < StacksEpochId::Epoch30 { - // update heaviest affirmation map view - self.heaviest_affirmation_map = static_get_heaviest_affirmation_map( - &self.burnchain, - indexer, - &self.burnchain_db, - sortdb, - &canonical_sn.sortition_id, - ) - .map_err(|_| { - net_error::Transient("Unable to query heaviest affirmation map".to_string()) - })?; - - self.tentative_best_affirmation_map = static_get_canonical_affirmation_map( - &self.burnchain, - indexer, - &self.burnchain_db, - sortdb, - chainstate, - &canonical_sn.sortition_id, - ) - .map_err(|_| { - net_error::Transient("Unable to query canonical affirmation map".to_string()) - })?; - - self.sortition_tip_affirmation_map = - SortitionDB::find_sortition_tip_affirmation_map( - sortdb, - &canonical_sn.sortition_id, - )?; - } - // update last anchor data let ih = sortdb.index_handle(&canonical_sn.sortition_id); self.last_anchor_block_hash = ih @@ -4932,21 +4888,6 @@ impl PeerNetwork { self.refresh_stacker_db_configs(sortdb, chainstate)?; } - if stacks_tip_changed && self.get_current_epoch().epoch_id < StacksEpochId::Epoch30 { - // update stacks tip affirmation map view - // (NOTE: this check has to happen _after_ self.chain_view gets updated!) - self.stacks_tip_affirmation_map = static_get_stacks_tip_affirmation_map( - &self.burnchain_db, - sortdb, - &canonical_sn.sortition_id, - &canonical_sn.canonical_stacks_tip_consensus_hash, - &canonical_sn.canonical_stacks_tip_hash, - ) - .map_err(|_| { - net_error::Transient("Unable to query stacks tip affirmation map".to_string()) - })?; - } - // can't fail after this point let mut ret = PendingMessages::new(); if burnchain_tip_changed { @@ -5519,7 +5460,7 @@ impl PeerNetwork { // update burnchain view, before handling any HTTP connections let unsolicited_buffered_messages = - match self.refresh_burnchain_view(indexer, sortdb, chainstate, ibd) { + match self.refresh_burnchain_view(sortdb, chainstate, ibd) { Ok(msgs) => msgs, Err(e) => { warn!("Failed to refresh burnchain view: {:?}", &e); diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index e9d994e703..1f3d2091c3 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -777,7 +777,7 @@ fn test_sync_inv_make_inv_messages() { .with_network_state(|sortdb, chainstate, network, _relayer, _mempool| { network.refresh_local_peer().unwrap(); network - .refresh_burnchain_view(&indexer, sortdb, chainstate, false) + .refresh_burnchain_view(sortdb, chainstate, false) .unwrap(); network.refresh_sortition_view(sortdb).unwrap(); Ok(()) From d1fdb69d2b918b2b62b94aaed30b3307d392b444 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 24 Jul 2025 12:00:13 -0700 Subject: [PATCH 09/35] Fix failing test_inv_sync_start_reward_cycle Signed-off-by: Jacinta Ferrant --- stackslib/src/net/tests/inv/epoch2x.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index 1f3d2091c3..23105b3c51 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -1254,7 +1254,7 @@ fn test_inv_sync_start_reward_cycle() { let block_scan_start = peer_1 .network .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); - assert_eq!(block_scan_start, 7); + assert_eq!(block_scan_start, 8); peer_1.network.connection_opts.inv_reward_cycles = 1; From 36c427bbfaab3c50118b9e451f60fb7f2d340d95 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 24 Jul 2025 12:12:14 -0700 Subject: [PATCH 10/35] Remove AffirmationMaps from getinfo endpoint Signed-off-by: Jacinta Ferrant --- stacks-signer/src/client/mod.rs | 1 - stackslib/src/net/api/getinfo.rs | 19 ------------------- 2 files changed, 20 deletions(-) diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index 974297cb9e..04fda6c3d6 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -341,7 +341,6 @@ pub(crate) mod tests { genesis_chainstate_hash: Sha256Sum::zero(), node_public_key: Some(public_key_buf), node_public_key_hash: Some(public_key_hash), - affirmations: None, last_pox_anchor: None, stackerdbs: Some( stackerdb_contract_ids diff --git a/stackslib/src/net/api/getinfo.rs b/stackslib/src/net/api/getinfo.rs index 311b8c38ca..40eedfb4c5 100644 --- a/stackslib/src/net/api/getinfo.rs +++ b/stackslib/src/net/api/getinfo.rs @@ -22,7 +22,6 @@ use stacks_common::types::net::PeerHost; use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::{Hash160, Sha256Sum}; -use crate::burnchains::affirmation::AffirmationMap; use crate::burnchains::Txid; use crate::chainstate::stacks::db::StacksChainState; use crate::net::http::{ @@ -44,15 +43,6 @@ impl RPCPeerInfoRequestHandler { Self {} } } - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct RPCAffirmationData { - pub heaviest: AffirmationMap, - pub stacks_tip: AffirmationMap, - pub sortition_tip: AffirmationMap, - pub tentative_best: AffirmationMap, -} - /// Information about the last PoX anchor block #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RPCLastPoxAnchorData { @@ -88,9 +78,6 @@ pub struct RPCPeerInfoData { pub node_public_key_hash: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub affirmations: Option, - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] pub last_pox_anchor: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -146,12 +133,6 @@ impl RPCPeerInfoData { genesis_chainstate_hash: genesis_chainstate_hash.clone(), node_public_key: Some(public_key_buf), node_public_key_hash: Some(public_key_hash), - affirmations: Some(RPCAffirmationData { - heaviest: AffirmationMap::empty(), - stacks_tip: AffirmationMap::empty(), - sortition_tip: AffirmationMap::empty(), - tentative_best: AffirmationMap::empty(), - }), last_pox_anchor: Some(RPCLastPoxAnchorData { anchor_block_hash: network.last_anchor_block_hash.clone(), anchor_block_txid: network.last_anchor_block_txid.clone(), From 70b773a324c53d175c07300712372e4a8f3b284e Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 28 Jul 2025 11:12:02 -0700 Subject: [PATCH 11/35] Remove require_affirmed_anchor_blocks config options Signed-off-by: Jacinta Ferrant --- .../tests/fixtures/minimal_config.json | 3 +-- stacks-node/src/run_loop/nakamoto.rs | 3 --- stacks-node/src/run_loop/neon.rs | 3 --- stacks-node/src/tests/epoch_21.rs | 23 ------------------- stacks-node/src/tests/epoch_22.rs | 4 ---- stackslib/src/chainstate/coordinator/mod.rs | 5 ---- stackslib/src/config/mod.rs | 19 --------------- 7 files changed, 1 insertion(+), 59 deletions(-) diff --git a/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json b/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json index 3ede4781ca..0cce5ccbd4 100644 --- a/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json +++ b/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json @@ -48,7 +48,7 @@ }, { "name": "miner", - "description": "Flag indicating whether this node should activate its mining logic and attempt to\nproduce Stacks blocks. Setting this to `true` typically requires providing\nnecessary private keys (either [`NodeConfig::seed`] or [`MinerConfig::mining_key`]).\nIt also influences default behavior for settings like\n[`NodeConfig::require_affirmed_anchor_blocks`].", + "description": "Flag indicating whether this node should activate its mining logic and attempt to\nproduce Stacks blocks. Setting this to `true` typically requires providing\nnecessary private keys (either [`NodeConfig::seed`] or [`MinerConfig::mining_key`]).", "default_value": "`false`", "notes": null, "deprecated": null, @@ -75,6 +75,5 @@ "MinerConfig::mining_key": null, "NodeConfig::miner": null, "NodeConfig::mine_microblocks": null, - "NodeConfig::require_affirmed_anchor_blocks": null } } \ No newline at end of file diff --git a/stacks-node/src/run_loop/nakamoto.rs b/stacks-node/src/run_loop/nakamoto.rs index 3029d10754..acbca38974 100644 --- a/stacks-node/src/run_loop/nakamoto.rs +++ b/stacks-node/src/run_loop/nakamoto.rs @@ -325,9 +325,6 @@ impl RunLoop { let coord_config = ChainsCoordinatorConfig { assume_present_anchor_blocks: moved_config.node.assume_present_anchor_blocks, always_use_affirmation_maps: moved_config.node.always_use_affirmation_maps, - require_affirmed_anchor_blocks: moved_config - .node - .require_affirmed_anchor_blocks, txindex: moved_config.node.txindex, }; ChainsCoordinator::run( diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index a4c9b7787a..485b528b96 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -703,9 +703,6 @@ impl RunLoop { let coord_config = ChainsCoordinatorConfig { assume_present_anchor_blocks: moved_config.node.assume_present_anchor_blocks, always_use_affirmation_maps: moved_config.node.always_use_affirmation_maps, - require_affirmed_anchor_blocks: moved_config - .node - .require_affirmed_anchor_blocks, txindex: moved_config.node.txindex, }; ChainsCoordinator::run( diff --git a/stacks-node/src/tests/epoch_21.rs b/stacks-node/src/tests/epoch_21.rs index a9043d5e0c..52c4ace13d 100644 --- a/stacks-node/src/tests/epoch_21.rs +++ b/stacks-node/src/tests/epoch_21.rs @@ -2008,8 +2008,6 @@ fn test_pox_reorgs_three_flaps() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; - // make epoch 2.1 start in the middle of boot-up let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -2068,8 +2066,6 @@ fn test_pox_reorgs_three_flaps() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; @@ -2528,8 +2524,6 @@ fn test_pox_reorg_one_flap() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; - // make epoch 2.1 start in the middle of boot-up let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -2588,8 +2582,6 @@ fn test_pox_reorg_one_flap() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; @@ -2932,8 +2924,6 @@ fn test_pox_reorg_flap_duel() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; - // make epoch 2.1 start in the middle of boot-up let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -2992,8 +2982,6 @@ fn test_pox_reorg_flap_duel() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; @@ -3351,8 +3339,6 @@ fn test_pox_reorg_flap_reward_cycles() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; - // make epoch 2.1 start in the middle of boot-up let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -3411,8 +3397,6 @@ fn test_pox_reorg_flap_reward_cycles() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; @@ -3761,8 +3745,6 @@ fn test_pox_missing_five_anchor_blocks() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; - // make epoch 2.1 start in the middle of boot-up let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -3821,8 +3803,6 @@ fn test_pox_missing_five_anchor_blocks() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; @@ -4138,7 +4118,6 @@ fn test_sortition_divergence_pre_21() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; conf_template.node.always_use_affirmation_maps = false; // make epoch 2.1 start after we have created this error condition @@ -4199,8 +4178,6 @@ fn test_sortition_divergence_pre_21() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; conf.node.always_use_affirmation_maps = false; diff --git a/stacks-node/src/tests/epoch_22.rs b/stacks-node/src/tests/epoch_22.rs index 3a9df900b9..4f28ba3052 100644 --- a/stacks-node/src/tests/epoch_22.rs +++ b/stacks-node/src/tests/epoch_22.rs @@ -1256,8 +1256,6 @@ fn test_pox_reorg_one_flap() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.require_affirmed_anchor_blocks = false; - // make epoch 2.1 and 2.2 start in the middle of boot-up let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -1320,8 +1318,6 @@ fn test_pox_reorg_one_flap() { conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.require_affirmed_anchor_blocks = - conf_template.node.require_affirmed_anchor_blocks; // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 3126dfeb3a..a41b56e18d 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -188,9 +188,6 @@ pub struct ChainsCoordinatorConfig { /// true: use affirmation maps before 2.1 /// false: only use affirmation maps in 2.1 or later pub always_use_affirmation_maps: bool, - /// true: always wait for canonical anchor blocks, even if it stalls the chain - /// false: proceed to process new chain history even if we're missing an anchor block. - pub require_affirmed_anchor_blocks: bool, /// true: enable transactions indexing /// false: no transactions indexing pub txindex: bool, @@ -200,7 +197,6 @@ impl ChainsCoordinatorConfig { pub fn new() -> ChainsCoordinatorConfig { ChainsCoordinatorConfig { always_use_affirmation_maps: true, - require_affirmed_anchor_blocks: true, assume_present_anchor_blocks: true, txindex: false, } @@ -209,7 +205,6 @@ impl ChainsCoordinatorConfig { pub fn test_new(txindex: bool) -> ChainsCoordinatorConfig { ChainsCoordinatorConfig { always_use_affirmation_maps: false, - require_affirmed_anchor_blocks: false, assume_present_anchor_blocks: false, txindex, } diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index 4f3c03c94f..419ee3bd56 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -2116,8 +2116,6 @@ pub struct NodeConfig { /// Flag indicating whether this node should activate its mining logic and attempt to /// produce Stacks blocks. Setting this to `true` typically requires providing /// necessary private keys (either [`NodeConfig::seed`] or [`MinerConfig::mining_key`]). - /// It also influences default behavior for settings like - /// [`NodeConfig::require_affirmed_anchor_blocks`]. /// --- /// @default: `false` pub miner: bool, @@ -2242,18 +2240,6 @@ pub struct NodeConfig { /// --- /// @default: `true` pub always_use_affirmation_maps: bool, - /// Controls if the node must wait for locally missing but burnchain-affirmed PoX - /// anchor blocks. If an anchor block is confirmed by the affirmation map but not - /// yet processed by this node: - /// - If `true`: Burnchain processing halts until the affirmed block is acquired. - /// Ensures strict adherence to the affirmed canonical chain, typical for - /// followers. - /// - If `false`: Burnchain processing continues without waiting. Allows miners to - /// operate optimistically but may necessitate unwinding later if the affirmed - /// block alters the chain state. - /// --- - /// @default: Derived from the inverse of [`NodeConfig::miner`] value. - pub require_affirmed_anchor_blocks: bool, /// Controls if the node must strictly wait for any PoX anchor block selected by /// the core consensus mechanism. /// - If `true`: Halts burnchain processing immediately whenever a selected anchor @@ -2578,7 +2564,6 @@ impl Default for NodeConfig { pox_sync_sample_secs: 30, use_test_genesis_chainstate: None, always_use_affirmation_maps: true, - require_affirmed_anchor_blocks: true, assume_present_anchor_blocks: true, fault_injection_block_push_fail_probability: None, fault_injection_hide_blocks: false, @@ -3918,7 +3903,6 @@ pub struct NodeConfigFile { pub pox_sync_sample_secs: Option, pub use_test_genesis_chainstate: Option, pub always_use_affirmation_maps: Option, - pub require_affirmed_anchor_blocks: Option, pub assume_present_anchor_blocks: Option, /// At most, how often should the chain-liveness thread /// wake up the chains-coordinator. Defaults to 300s (5 min). @@ -4000,9 +3984,6 @@ impl NodeConfigFile { always_use_affirmation_maps: self .always_use_affirmation_maps .unwrap_or(default_node_config.always_use_affirmation_maps), - // miners should always try to mine, even if they don't have the anchored - // blocks in the canonical affirmation map. Followers, however, can stall. - require_affirmed_anchor_blocks: self.require_affirmed_anchor_blocks.unwrap_or(!miner), // as of epoch 3.0, all prepare phases have anchor blocks. // at the start of epoch 3.0, the chain stalls without anchor blocks. // only set this to false if you're doing some very extreme testing. From 86d281c38562f83290aeab768521883d1e395f32 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 28 Jul 2025 11:41:16 -0700 Subject: [PATCH 12/35] Remove always_use_affirmation_maps config option Signed-off-by: Jacinta Ferrant --- stacks-node/src/main.rs | 1 - stacks-node/src/neon_node.rs | 1 - stacks-node/src/run_loop/nakamoto.rs | 1 - stacks-node/src/run_loop/neon.rs | 1 - stacks-node/src/tests/epoch_21.rs | 4 ---- stacks-node/src/tests/neon_integrations.rs | 15 ------------- stackslib/src/chainstate/coordinator/mod.rs | 24 ++++++--------------- stackslib/src/config/mod.rs | 17 --------------- stackslib/src/main.rs | 1 - stackslib/src/net/mod.rs | 1 - 10 files changed, 7 insertions(+), 59 deletions(-) diff --git a/stacks-node/src/main.rs b/stacks-node/src/main.rs index 9ffbcd8bb4..c1b7c9e99a 100644 --- a/stacks-node/src/main.rs +++ b/stacks-node/src/main.rs @@ -139,7 +139,6 @@ fn cli_get_miner_spend( &mut sortdb, &burnchain, &OnChainRewardSetProvider(no_dispatcher), - config.node.always_use_affirmation_maps, ) .unwrap(); diff --git a/stacks-node/src/neon_node.rs b/stacks-node/src/neon_node.rs index 1594b885f4..8cb59fe7e7 100644 --- a/stacks-node/src/neon_node.rs +++ b/stacks-node/src/neon_node.rs @@ -2109,7 +2109,6 @@ impl BlockMinerThread { burn_db, &self.burnchain, &OnChainRewardSetProvider::new(), - self.config.node.always_use_affirmation_maps, ) { Ok(x) => x, Err(e) => { diff --git a/stacks-node/src/run_loop/nakamoto.rs b/stacks-node/src/run_loop/nakamoto.rs index acbca38974..c20436f768 100644 --- a/stacks-node/src/run_loop/nakamoto.rs +++ b/stacks-node/src/run_loop/nakamoto.rs @@ -324,7 +324,6 @@ impl RunLoop { let coord_config = ChainsCoordinatorConfig { assume_present_anchor_blocks: moved_config.node.assume_present_anchor_blocks, - always_use_affirmation_maps: moved_config.node.always_use_affirmation_maps, txindex: moved_config.node.txindex, }; ChainsCoordinator::run( diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index 485b528b96..f30b0d9209 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -702,7 +702,6 @@ impl RunLoop { let coord_config = ChainsCoordinatorConfig { assume_present_anchor_blocks: moved_config.node.assume_present_anchor_blocks, - always_use_affirmation_maps: moved_config.node.always_use_affirmation_maps, txindex: moved_config.node.txindex, }; ChainsCoordinator::run( diff --git a/stacks-node/src/tests/epoch_21.rs b/stacks-node/src/tests/epoch_21.rs index 52c4ace13d..16aa232806 100644 --- a/stacks-node/src/tests/epoch_21.rs +++ b/stacks-node/src/tests/epoch_21.rs @@ -4118,8 +4118,6 @@ fn test_sortition_divergence_pre_21() { conf_template.node.wait_time_for_blocks = 1_000; conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - conf_template.node.always_use_affirmation_maps = false; - // make epoch 2.1 start after we have created this error condition let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); epochs[StacksEpochId::Epoch20].end_height = 101; @@ -4179,8 +4177,6 @@ fn test_sortition_divergence_pre_21() { conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - conf.node.always_use_affirmation_maps = false; - // multiple nodes so they must download from each other conf.miner.wait_for_block_download = true; diff --git a/stacks-node/src/tests/neon_integrations.rs b/stacks-node/src/tests/neon_integrations.rs index 2557ef6534..3df12f8c10 100644 --- a/stacks-node/src/tests/neon_integrations.rs +++ b/stacks-node/src/tests/neon_integrations.rs @@ -5171,9 +5171,6 @@ fn pox_integration_test() { test_observer::spawn(); test_observer::register_any(&mut conf); - // required for testing post-sunset behavior - conf.node.always_use_affirmation_maps = false; - let first_bal = 6_000_000_000 * u64::from(core::MICROSTACKS_PER_STACKS); let second_bal = 2_000_000_000 * u64::from(core::MICROSTACKS_PER_STACKS); let third_bal = 2_000_000_000 * u64::from(core::MICROSTACKS_PER_STACKS); @@ -5677,8 +5674,6 @@ fn atlas_integration_test() { .initial_balances .push(initial_balance_user_1.clone()); - conf_bootstrap_node.node.always_use_affirmation_maps = false; - // Prepare the config of the follower node let (mut conf_follower_node, _) = neon_integration_test_conf(); let bootstrap_node_url = format!( @@ -5703,8 +5698,6 @@ fn atlas_integration_test() { disable_retries: false, }); - conf_follower_node.node.always_use_affirmation_maps = false; - // Our 2 nodes will share the bitcoind node let mut btcd_controller = BitcoinCoreController::new(conf_bootstrap_node.clone()); btcd_controller @@ -6210,8 +6203,6 @@ fn antientropy_integration_test() { conf_bootstrap_node.burnchain.max_rbf = 1000000; conf_bootstrap_node.node.wait_time_for_blocks = 1_000; - conf_bootstrap_node.node.always_use_affirmation_maps = false; - // Prepare the config of the follower node let (mut conf_follower_node, _) = neon_integration_test_conf(); let bootstrap_node_url = format!( @@ -6246,8 +6237,6 @@ fn antientropy_integration_test() { conf_follower_node.burnchain.max_rbf = 1000000; conf_follower_node.node.wait_time_for_blocks = 1_000; - conf_follower_node.node.always_use_affirmation_maps = false; - // Our 2 nodes will share the bitcoind node let mut btcd_controller = BitcoinCoreController::new(conf_bootstrap_node.clone()); btcd_controller @@ -6484,8 +6473,6 @@ fn atlas_stress_integration_test() { conf_bootstrap_node.burnchain.max_rbf = 1000000; conf_bootstrap_node.node.wait_time_for_blocks = 1_000; - conf_bootstrap_node.node.always_use_affirmation_maps = false; - let user_1 = users.pop().unwrap(); let initial_balance_user_1 = initial_balances.pop().unwrap(); @@ -7944,8 +7931,6 @@ fn spawn_follower_node( conf.connection_options.inv_sync_interval = 3; - conf.node.always_use_affirmation_maps = false; - let mut run_loop = neon::RunLoop::new(conf.clone()); let blocks_processed = run_loop.get_blocks_processed_arc(); let channel = run_loop.get_coordinator_channel().unwrap(); diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index a41b56e18d..605e1df45f 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -185,9 +185,6 @@ pub struct ChainsCoordinatorConfig { /// true: assume all anchor blocks are present, and block chain sync until they arrive /// false: process sortitions in reward cycles without anchor blocks pub assume_present_anchor_blocks: bool, - /// true: use affirmation maps before 2.1 - /// false: only use affirmation maps in 2.1 or later - pub always_use_affirmation_maps: bool, /// true: enable transactions indexing /// false: no transactions indexing pub txindex: bool, @@ -196,7 +193,6 @@ pub struct ChainsCoordinatorConfig { impl ChainsCoordinatorConfig { pub fn new() -> ChainsCoordinatorConfig { ChainsCoordinatorConfig { - always_use_affirmation_maps: true, assume_present_anchor_blocks: true, txindex: false, } @@ -204,7 +200,6 @@ impl ChainsCoordinatorConfig { pub fn test_new(txindex: bool) -> ChainsCoordinatorConfig { ChainsCoordinatorConfig { - always_use_affirmation_maps: false, assume_present_anchor_blocks: false, txindex, } @@ -716,7 +711,6 @@ pub fn get_next_recipients( sort_db: &mut SortitionDB, burnchain: &Burnchain, provider: &U, - always_use_affirmation_maps: bool, ) -> Result, Error> { let burnchain_db = BurnchainDB::open(&burnchain.get_burnchaindb_path(), false)?; let reward_cycle_info = get_reward_cycle_info( @@ -728,7 +722,6 @@ pub fn get_next_recipients( chain_state, sort_db, provider, - always_use_affirmation_maps, )?; sort_db .get_next_block_recipients(burnchain, sortition_tip, reward_cycle_info.as_ref()) @@ -749,7 +742,6 @@ pub fn get_reward_cycle_info( chain_state: &mut StacksChainState, sort_db: &mut SortitionDB, provider: &U, - always_use_affirmation_maps: bool, ) -> 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)); @@ -780,13 +772,13 @@ pub fn get_reward_cycle_info( let reward_cycle_info = { let ic = sort_db.index_handle(sortition_tip); - let burnchain_db_conn_opt = - if epoch_at_height.epoch_id >= StacksEpochId::Epoch21 || always_use_affirmation_maps { - // use the new block-commit-based PoX anchor block selection rules - Some(burnchain_db.conn()) - } else { - None - }; + // TODO: always use block-commit-based PoX anchor block selection rules? + let burnchain_db_conn_opt = if epoch_at_height.epoch_id >= StacksEpochId::Epoch21 { + // use the new block-commit-based PoX anchor block selection rules + Some(burnchain_db.conn()) + } else { + None + }; ic.get_chosen_pox_anchor(burnchain_db_conn_opt, parent_bhh, &burnchain.pox_constants) }?; @@ -1459,7 +1451,6 @@ impl< &mut self.chain_state_db, &mut self.sortition_db, &self.reward_set_provider, - self.config.always_use_affirmation_maps, ) } @@ -1954,7 +1945,6 @@ impl SortitionDBMigrator { &mut chainstate, sort_db, &OnChainRewardSetProvider::new(), - true, ) .map_err(|e| DBError::Other(format!("get_reward_cycle_info: {:?}", &e))); diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index 419ee3bd56..66058ab549 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -2228,18 +2228,6 @@ pub struct NodeConfig { /// @notes: /// - This is intended strictly for testing purposes and is disallowed on mainnet. pub use_test_genesis_chainstate: Option, - /// Controls if Stacks Epoch 2.1+ affirmation map logic should be applied even - /// before Epoch 2.1. - /// - If `true` (default), the node consistently uses the newer (Epoch 2.1) rules - /// for PoX anchor block validation and affirmation-based reorg handling, even in - /// earlier epochs. - /// - If `false`, the node strictly follows the rules defined for the specific epoch - /// it is currently processing, only applying 2.1+ logic from Epoch 2.1 onwards. - /// Differences in this setting between nodes prior to Epoch 2.1 could lead to - /// consensus forks. - /// --- - /// @default: `true` - pub always_use_affirmation_maps: bool, /// Controls if the node must strictly wait for any PoX anchor block selected by /// the core consensus mechanism. /// - If `true`: Halts burnchain processing immediately whenever a selected anchor @@ -2563,7 +2551,6 @@ impl Default for NodeConfig { marf_defer_hashing: true, pox_sync_sample_secs: 30, use_test_genesis_chainstate: None, - always_use_affirmation_maps: true, assume_present_anchor_blocks: true, fault_injection_block_push_fail_probability: None, fault_injection_hide_blocks: false, @@ -3902,7 +3889,6 @@ pub struct NodeConfigFile { pub marf_defer_hashing: Option, pub pox_sync_sample_secs: Option, pub use_test_genesis_chainstate: Option, - pub always_use_affirmation_maps: Option, pub assume_present_anchor_blocks: Option, /// At most, how often should the chain-liveness thread /// wake up the chains-coordinator. Defaults to 300s (5 min). @@ -3981,9 +3967,6 @@ impl NodeConfigFile { .pox_sync_sample_secs .unwrap_or(default_node_config.pox_sync_sample_secs), use_test_genesis_chainstate: self.use_test_genesis_chainstate, - always_use_affirmation_maps: self - .always_use_affirmation_maps - .unwrap_or(default_node_config.always_use_affirmation_maps), // as of epoch 3.0, all prepare phases have anchor blocks. // at the start of epoch 3.0, the chain stalls without anchor blocks. // only set this to false if you're doing some very extreme testing. diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 8961eb413e..031b7d4339 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1946,7 +1946,6 @@ fn analyze_sortition_mev(argv: Vec) { &mut chainstate, &mut sortdb, &OnChainRewardSetProvider::new(), - false, ) .unwrap(); diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index a3d2df8959..25a55720b4 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -4426,7 +4426,6 @@ pub mod test { &mut sortdb, &self.config.burnchain, &OnChainRewardSetProvider::new(), - true, ) { Ok(recipients) => { block_commit_op.commit_outs = match recipients { From e40e0cf8d18d5e27dba3cd8e803de4a120c43255 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 28 Jul 2025 13:36:27 -0700 Subject: [PATCH 13/35] Remove assume_present_anchor_blocks config option Signed-off-by: Jacinta Ferrant --- stacks-node/src/run_loop/nakamoto.rs | 1 - stacks-node/src/run_loop/neon.rs | 1 - stackslib/src/chainstate/coordinator/mod.rs | 17 ++++----------- stackslib/src/config/mod.rs | 24 --------------------- 4 files changed, 4 insertions(+), 39 deletions(-) diff --git a/stacks-node/src/run_loop/nakamoto.rs b/stacks-node/src/run_loop/nakamoto.rs index c20436f768..362d314d24 100644 --- a/stacks-node/src/run_loop/nakamoto.rs +++ b/stacks-node/src/run_loop/nakamoto.rs @@ -323,7 +323,6 @@ impl RunLoop { let mut fee_estimator = moved_config.make_fee_estimator(); let coord_config = ChainsCoordinatorConfig { - assume_present_anchor_blocks: moved_config.node.assume_present_anchor_blocks, txindex: moved_config.node.txindex, }; ChainsCoordinator::run( diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index f30b0d9209..30937bf555 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -701,7 +701,6 @@ impl RunLoop { let mut fee_estimator = moved_config.make_fee_estimator(); let coord_config = ChainsCoordinatorConfig { - assume_present_anchor_blocks: moved_config.node.assume_present_anchor_blocks, txindex: moved_config.node.txindex, }; ChainsCoordinator::run( diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 605e1df45f..cf69f6aa44 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -182,9 +182,6 @@ pub trait BlockEventDispatcher { } pub struct ChainsCoordinatorConfig { - /// true: assume all anchor blocks are present, and block chain sync until they arrive - /// false: process sortitions in reward cycles without anchor blocks - pub assume_present_anchor_blocks: bool, /// true: enable transactions indexing /// false: no transactions indexing pub txindex: bool, @@ -192,17 +189,11 @@ pub struct ChainsCoordinatorConfig { impl ChainsCoordinatorConfig { pub fn new() -> ChainsCoordinatorConfig { - ChainsCoordinatorConfig { - assume_present_anchor_blocks: true, - txindex: false, - } + ChainsCoordinatorConfig { txindex: false } } pub fn test_new(txindex: bool) -> ChainsCoordinatorConfig { - ChainsCoordinatorConfig { - assume_present_anchor_blocks: false, - txindex, - } + ChainsCoordinatorConfig { txindex } } } @@ -1061,8 +1052,8 @@ impl< panic!("BUG: no epoch defined at height {}", header.block_height) }); - if cur_epoch.epoch_id >= StacksEpochId::Epoch21 || self.config.assume_present_anchor_blocks - { + // TODO: Always stop if the anchor block is not present + if cur_epoch.epoch_id >= StacksEpochId::Epoch21 { // anchor blocks are always assumed to be present in the chain history, // so report its absence if we don't have it. if let PoxAnchorBlockStatus::SelectedAndUnknown(missing_anchor_block, _) = diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index 66058ab549..ae11518792 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -2228,24 +2228,6 @@ pub struct NodeConfig { /// @notes: /// - This is intended strictly for testing purposes and is disallowed on mainnet. pub use_test_genesis_chainstate: Option, - /// Controls if the node must strictly wait for any PoX anchor block selected by - /// the core consensus mechanism. - /// - If `true`: Halts burnchain processing immediately whenever a selected anchor - /// block is missing locally (`SelectedAndUnknown` status), regardless of - /// affirmation status. - /// - If `false` (primarily for testing): Skips this immediate halt, allowing - /// processing to proceed to affirmation map checks. - /// Normal operation requires this to be `true`; setting to `false` will likely - /// break consensus adherence. - /// --- - /// @default: `true` - /// @notes: - /// - This parameter cannot be set via the configuration file; it must be modified - /// programmatically. - /// - This is intended strictly for testing purposes. - /// - The halt check runs *before* affirmation checks. - /// - In Nakamoto (Epoch 3.0+), all prepare phases have anchor blocks. - pub assume_present_anchor_blocks: bool, /// Fault injection setting for testing purposes. If set to `Some(p)`, where `p` is /// between 0 and 100, the node will have a `p` percent chance of intentionally /// *not* pushing a newly processed block to its peers. @@ -2551,7 +2533,6 @@ impl Default for NodeConfig { marf_defer_hashing: true, pox_sync_sample_secs: 30, use_test_genesis_chainstate: None, - assume_present_anchor_blocks: true, fault_injection_block_push_fail_probability: None, fault_injection_hide_blocks: false, chain_liveness_poll_time_secs: 300, @@ -3889,7 +3870,6 @@ pub struct NodeConfigFile { pub marf_defer_hashing: Option, pub pox_sync_sample_secs: Option, pub use_test_genesis_chainstate: Option, - pub assume_present_anchor_blocks: Option, /// At most, how often should the chain-liveness thread /// wake up the chains-coordinator. Defaults to 300s (5 min). pub chain_liveness_poll_time_secs: Option, @@ -3967,10 +3947,6 @@ impl NodeConfigFile { .pox_sync_sample_secs .unwrap_or(default_node_config.pox_sync_sample_secs), use_test_genesis_chainstate: self.use_test_genesis_chainstate, - // as of epoch 3.0, all prepare phases have anchor blocks. - // at the start of epoch 3.0, the chain stalls without anchor blocks. - // only set this to false if you're doing some very extreme testing. - assume_present_anchor_blocks: true, // chainstate fault_injection activation for hide_blocks. // you can't set this in the config file. fault_injection_hide_blocks: false, From b72a6ab4bf25e530f97d679ffb6bdcc345817446 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Jul 2025 10:05:51 -0700 Subject: [PATCH 14/35] check_missing_anchor_block should stall if missing anchor block regardless of epoch Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 32 +++------ stackslib/src/chainstate/coordinator/tests.rs | 67 +++++-------------- 2 files changed, 27 insertions(+), 72 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index cf69f6aa44..1470035cbf 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1034,7 +1034,7 @@ impl< } /// Check to see if the discovery of a PoX anchor block means it's time to process a new reward - /// cycle. Based on the canonical affirmation map, this may not always be the case. + /// cycle. /// /// This mutates `rc_info` to be the affirmed anchor block status. /// @@ -1043,31 +1043,21 @@ impl< /// Returns Ok(None) if not fn check_missing_anchor_block( &self, - header: &BurnchainBlockHeader, + _header: &BurnchainBlockHeader, rc_info: &mut RewardCycleInfo, ) -> Result, Error> { - let cur_epoch = - SortitionDB::get_stacks_epoch(self.sortition_db.conn(), header.block_height)? - .unwrap_or_else(|| { - panic!("BUG: no epoch defined at height {}", header.block_height) - }); - - // TODO: Always stop if the anchor block is not present - if cur_epoch.epoch_id >= StacksEpochId::Epoch21 { - // anchor blocks are always assumed to be present in the chain history, - // so report its absence if we don't have it. - if let PoxAnchorBlockStatus::SelectedAndUnknown(missing_anchor_block, _) = - &rc_info.anchor_status - { - info!("Currently missing PoX anchor block {missing_anchor_block}, which is assumed to be present"); - return Ok(Some(missing_anchor_block.clone())); - } + // anchor blocks are always assumed to be present in the chain history, + // so report its absence if we don't have it. + if let PoxAnchorBlockStatus::SelectedAndUnknown(missing_anchor_block, _) = + &rc_info.anchor_status + { + info!("Currently missing PoX anchor block {missing_anchor_block}, which is assumed to be present"); + return Ok(Some(missing_anchor_block.clone())); } test_debug!( - "Reward cycle info at height {}: {:?}", - &header.block_height, - &rc_info + "Reward cycle info at height {}: {rc_info:?}", + &_header.block_height ); Ok(None) } diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index 4887bcad4f..7d3ce51c4d 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -6448,7 +6448,7 @@ fn test_pox_fork_out_of_order() { let burnchain_blinded = get_burnchain_db(path_blinded, None); let b = get_burnchain(path, None); - eprintln!("Making block {}", ix); + eprintln!("Making block {ix}"); let (op, block) = if ix == 0 { make_genesis_block( &b, @@ -6461,9 +6461,7 @@ fn test_pox_fork_out_of_order() { ix as u32, ) } else { - let parent = if ix == 1 { - stacks_blocks[0].1.header.block_hash() - } else if ix == 6 { + let parent = if ix == 1 || ix == 6 { stacks_blocks[0].1.header.block_hash() } else if ix == 11 { stacks_blocks[5].1.header.block_hash() @@ -6506,8 +6504,7 @@ fn test_pox_fork_out_of_order() { let ic = sort_db.index_handle_at_tip(); let bhh = ic.get_last_anchor_block_hash().unwrap().unwrap(); eprintln!( - "Anchor block={}, selected at height={}", - &bhh, + "Anchor block={bhh}, selected at height={}", SortitionDB::get_block_snapshot_for_winning_stacks_block( &sort_db.index_conn(), &ic.context.chain_tip, @@ -6537,7 +6534,7 @@ fn test_pox_fork_out_of_order() { // load the block into staging let block_hash = block.header.block_hash(); - eprintln!("Block hash={}, ix={}", &block_hash, ix); + eprintln!("Block hash={block_hash}, ix={ix}"); assert_eq!(&tip.winning_stacks_block_hash, &block_hash); stacks_blocks.push((tip.sortition_id.clone(), block.clone())); @@ -6560,10 +6557,11 @@ fn test_pox_fork_out_of_order() { assert_eq!(&pox_id.to_string(), "11111"); } + // Because we no longer continue processing without an anchor block, the blinded signer has not advanced. { let ic = sort_db_blind.index_handle_at_tip(); let pox_id = ic.get_pox_id().unwrap(); - assert_eq!(&pox_id.to_string(), "11000"); + assert_eq!(&pox_id.to_string(), "11"); } // now, we reveal to the blinded coordinator, but out of order. @@ -6586,30 +6584,19 @@ fn test_pox_fork_out_of_order() { { let ic = sort_db_blind.index_handle_at_tip(); let pox_id = ic.get_pox_id().unwrap(); - assert_eq!(&pox_id.to_string(), "11110"); + assert_eq!(&pox_id.to_string(), "1111"); } let block_height = eval_at_chain_tip(path_blinded, &sort_db_blind, "block-height"); assert_eq!(block_height, Value::UInt(1)); // reveal [6-10] - for (_sort_id, block) in stacks_blocks[6..=10].iter() { - // cannot use sort_id from stacks_blocks, because the blinded coordinator - // has different sortition_id's for blocks 6-10 (because it's missing - // the 2nd anchor block). - let sort_id = SortitionDB::get_block_snapshot_for_winning_stacks_block( - &sort_db_blind.index_conn(), - &SortitionDB::get_canonical_sortition_tip(sort_db_blind.conn()).unwrap(), - &block.header.block_hash(), - ) - .unwrap() - .unwrap() - .sortition_id; + for (sort_id, block) in stacks_blocks[6..=10].iter() { reveal_block( path_blinded, &sort_db_blind, &mut coord_blind, - &sort_id, + sort_id, block, ); } @@ -6617,7 +6604,7 @@ fn test_pox_fork_out_of_order() { { let ic = sort_db_blind.index_handle_at_tip(); let pox_id = ic.get_pox_id().unwrap(); - assert_eq!(&pox_id.to_string(), "11110"); + assert_eq!(&pox_id.to_string(), "1111"); } let block_height = eval_at_chain_tip(path_blinded, &sort_db_blind, "block-height"); @@ -6637,19 +6624,7 @@ fn test_pox_fork_out_of_order() { ); // reveal [1-5] - for (_sort_id, block) in stacks_blocks[1..=5].iter() { - // cannot use sort_id from stacks_blocks, because the blinded coordinator - // has different sortition_id's for blocks 6-10 (because it's missing - // the 2nd anchor block). - let sort_id = SortitionDB::get_block_snapshot_for_winning_stacks_block( - &sort_db_blind.index_conn(), - &SortitionDB::get_canonical_sortition_tip(sort_db_blind.conn()).unwrap(), - &block.header.block_hash(), - ) - .unwrap() - .unwrap() - .sortition_id; - + for (sort_id, block) in stacks_blocks[1..=5].iter() { // before processing the last of these blocks, the stacks_block[9] should still // be the canonical tip let block_hash = eval_at_chain_tip( @@ -6670,7 +6645,7 @@ fn test_pox_fork_out_of_order() { path_blinded, &sort_db_blind, &mut coord_blind, - &sort_id, + sort_id, block, ); } @@ -6685,24 +6660,12 @@ fn test_pox_fork_out_of_order() { assert_eq!(block_height, Value::UInt(6)); // reveal [11-14] - for (_sort_id, block) in stacks_blocks[11..].iter() { - // cannot use sort_id from stacks_blocks, because the blinded coordinator - // has different sortition_id's for blocks 6-10 (because it's missing - // the 2nd anchor block). - let sort_id = SortitionDB::get_block_snapshot_for_winning_stacks_block( - &sort_db_blind.index_conn(), - &SortitionDB::get_canonical_sortition_tip(sort_db_blind.conn()).unwrap(), - &block.header.block_hash(), - ) - .unwrap() - .unwrap() - .sortition_id; - + for (sort_id, block) in stacks_blocks[11..].iter() { reveal_block( path_blinded, &sort_db_blind, &mut coord_blind, - &sort_id, + sort_id, block, ); } @@ -6765,6 +6728,8 @@ fn reveal_block Date: Tue, 29 Jul 2025 10:39:11 -0700 Subject: [PATCH 15/35] Fix test_pox_no_anchor_selected Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/tests.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index 7d3ce51c4d..2d0335f193 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -6234,7 +6234,7 @@ fn test_pox_no_anchor_selected() { let burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); let burnchain_blinded = get_burnchain_db(path_blinded, None); - eprintln!("Making block {}", ix); + eprintln!("Making block {ix}"); let (op, block) = if ix == 0 { make_genesis_block( &b, @@ -6300,8 +6300,7 @@ fn test_pox_no_anchor_selected() { let ic = sort_db.index_handle_at_tip(); let bhh = ic.get_last_anchor_block_hash().unwrap().unwrap(); eprintln!( - "Anchor block={}, selected at height={}", - &bhh, + "Anchor block={bhh}, selected at height={}", SortitionDB::get_block_snapshot_for_winning_stacks_block( &sort_db.index_conn(), &ic.context.chain_tip, @@ -6333,7 +6332,7 @@ fn test_pox_no_anchor_selected() { // load the block into staging let block_hash = block.header.block_hash(); - eprintln!("Block hash={}, ix={}", &block_hash, ix); + eprintln!("Block hash={block_hash}, ix={ix}"); assert_eq!(&tip.winning_stacks_block_hash, &block_hash); stacks_blocks.push((tip.sortition_id.clone(), block.clone())); @@ -6356,10 +6355,12 @@ fn test_pox_no_anchor_selected() { assert_eq!(&pox_id.to_string(), "1111"); } + // Because there is a missing anchor block, the blinded coordinator will not advance on any + // fork that does not build upon one { let ic = sort_db_blind.index_handle_at_tip(); let pox_id = ic.get_pox_id().unwrap(); - assert_eq!(&pox_id.to_string(), "1101"); + assert_eq!(&pox_id.to_string(), "11"); } for (sort_id, block) in stacks_blocks.iter() { From ea15d55984a38540fa6d90444d35522dc6f2ef96 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Jul 2025 10:39:54 -0700 Subject: [PATCH 16/35] Fix test_pox_no_anchor_selected Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index 2d0335f193..9d4063d469 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -6355,7 +6355,7 @@ fn test_pox_no_anchor_selected() { assert_eq!(&pox_id.to_string(), "1111"); } - // Because there is a missing anchor block, the blinded coordinator will not advance on any + // Because there is a missing anchor block, the blinded coordinator will not advance on any // fork that does not build upon one { let ic = sort_db_blind.index_handle_at_tip(); From 33fbe1882b085965cf496763fa0afb4c5b86dd57 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Jul 2025 11:03:22 -0700 Subject: [PATCH 17/35] Fix test_simple_setup Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/tests.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index 9d4063d469..b441f2214c 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -2175,7 +2175,7 @@ fn test_simple_setup() { let pox_id = ic.get_pox_id().unwrap(); assert_eq!( &pox_id.to_string(), - "110000000000", + "11", "PoX ID should reflect the initial 'known' reward cycle at genesis" ); } @@ -2203,12 +2203,8 @@ fn test_simple_setup() { pox_id_string.push('1'); } - println!("=> {}", pox_id_string); - assert_eq!( - pox_id_at_tip.to_string(), - // right-pad pox_id_string to 11 characters - format!("1{:0<11}", pox_id_string) - ); + println!("=> {pox_id_string}"); + assert_eq!(pox_id_at_tip.to_string(), format!("1{pox_id_string}")); } } From cd1b4cc150be6dbe17ef97af73188235de3f699d Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Jul 2025 14:03:01 -0700 Subject: [PATCH 18/35] Remove affirmation_overrides config Signed-off-by: Jacinta Ferrant --- stacks-node/src/run_loop/neon.rs | 12 +- stackslib/src/config/mod.rs | 214 +------------------------------ 2 files changed, 4 insertions(+), 222 deletions(-) diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index 30937bf555..11ee5bbb00 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -1028,19 +1028,9 @@ impl RunLoop { let liveness_thread = self.spawn_chain_liveness_thread(globals.clone()); // Wait for all pending sortitions to process - let mut burnchain_db = burnchain_config + let burnchain_db = burnchain_config .open_burnchain_db(true) .expect("FATAL: failed to open burnchain DB"); - if !self.config.burnchain.affirmation_overrides.is_empty() { - let tx = burnchain_db - .tx_begin() - .expect("FATAL: failed to begin burnchain DB tx"); - for (reward_cycle, affirmation) in self.config.burnchain.affirmation_overrides.iter() { - tx.set_override_affirmation_map(*reward_cycle, affirmation.clone()).unwrap_or_else(|_| panic!("FATAL: failed to set affirmation override ({affirmation}) for reward cycle {reward_cycle}")); - } - tx.commit() - .expect("FATAL: failed to commit burnchain DB tx"); - } let burnchain_db_tip = burnchain_db .get_canonical_chain_tip() .expect("FATAL: failed to query burnchain DB"); diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index ae11518792..2be8ad94d7 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -36,9 +36,8 @@ use stacks_common::util::get_epoch_time_ms; use stacks_common::util::hash::hex_bytes; use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey}; -use crate::burnchains::affirmation::AffirmationMap; use crate::burnchains::bitcoin::BitcoinNetworkType; -use crate::burnchains::{Burnchain, MagicBytes, PoxConstants, BLOCKSTACK_MAGIC_MAINNET}; +use crate::burnchains::{Burnchain, MagicBytes, BLOCKSTACK_MAGIC_MAINNET}; use crate::chainstate::nakamoto::signer_set::NakamotoSigners; use crate::chainstate::stacks::boot::MINERS_NAME; use crate::chainstate::stacks::index::marf::MARFOpenOpts; @@ -49,8 +48,7 @@ use crate::config::chain_data::MinerStats; use crate::core::mempool::{MemPoolWalkSettings, MemPoolWalkStrategy, MemPoolWalkTxTypes}; use crate::core::{ MemPoolDB, StacksEpoch, StacksEpochExtension, StacksEpochId, - BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT, BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT, - BITCOIN_TESTNET_STACKS_25_REORGED_HEIGHT, CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, + CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, STACKS_EPOCHS_REGTEST, STACKS_EPOCHS_TESTNET, }; use crate::cost_estimates::fee_medians::WeightedMedianFeeRateEstimator; @@ -229,7 +227,7 @@ impl ConfigFile { } pub fn xenon() -> ConfigFile { - let mut burnchain = BurnchainConfigFile { + let burnchain = BurnchainConfigFile { mode: Some("xenon".to_string()), rpc_port: Some(18332), peer_port: Some(18333), @@ -238,8 +236,6 @@ impl ConfigFile { ..BurnchainConfigFile::default() }; - burnchain.add_affirmation_overrides_xenon(); - let node = NodeConfigFile { bootstrap_node: Some("029266faff4c8e0ca4f934f34996a96af481df94a89b0c9bd515f3536a95682ddc@seed.testnet.hiro.so:30444".to_string()), miner: Some(false), @@ -1587,25 +1583,6 @@ pub struct BurnchainConfig { /// @default: `None` /// @deprecated: This setting is ignored in Epoch 3.0+. pub ast_precheck_size_height: Option, - /// Overrides for the burnchain block affirmation map for specific reward cycles. - /// Allows manually setting the miner affirmation ('p'resent/'n'ot-present/'a'bsent) - /// map for a given cycle, bypassing the map normally derived from sortition results. - /// - /// Special defaults are added when [`BurnchainConfig::mode`] is "xenon", but - /// config entries take precedence. At startup, these overrides are written to - /// the `BurnchainDB` (`overrides` table). - /// --- - /// @default: Empty map - /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode. - /// @notes: - /// - Primarily used for testing or recovering from network issues. - /// - Configured as a list `[[burnchain.affirmation_overrides]]` in TOML, each with - /// `reward_cycle` (integer) and `affirmation` (string of 'p'/'n'/'a', length `reward_cycle - 1`). - /// @toml_example: | - /// [[burnchain.affirmation_overrides]] - /// reward_cycle = 413 - /// affirmation = "pna..." # Must be 412 chars long - pub affirmation_overrides: HashMap, /// Fault injection setting for testing. Introduces an artificial delay (in /// milliseconds) before processing each burnchain block download. Simulates a /// slow burnchain connection. @@ -1672,7 +1649,6 @@ impl BurnchainConfig { sunset_end: None, wallet_name: "".to_string(), ast_precheck_size_height: None, - affirmation_overrides: HashMap::new(), fault_injection_burnchain_block_delay: 0, max_unspent_utxos: Some(1024), } @@ -1773,76 +1749,11 @@ pub struct BurnchainConfigFile { pub sunset_end: Option, pub wallet_name: Option, pub ast_precheck_size_height: Option, - pub affirmation_overrides: Option>, pub fault_injection_burnchain_block_delay: Option, pub max_unspent_utxos: Option, } impl BurnchainConfigFile { - /// Add affirmation overrides required to sync Xenon Testnet node. - /// - /// The Xenon Testnet Stacks 2.4 activation height occurred before the finalized SIP-024 updates and release of the stacks-node versioned 2.4.0.0.0. - /// This caused the Stacks Xenon testnet to undergo a deep reorg when 2.4.0.0.0 was finalized. This deep reorg meant that 3 reward cycles were - /// invalidated, which requires overrides in the affirmation map to continue correct operation. Those overrides are required for cycles 413, 414, and 415. - #[allow(clippy::indexing_slicing)] // bad affirmation map override should panic - pub fn add_affirmation_overrides_xenon(&mut self) { - let mut default_overrides = vec![ - AffirmationOverride { - reward_cycle: 413, - affirmation: "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa".to_string() - }, - AffirmationOverride { - reward_cycle: 414, - affirmation: "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpaa".to_string() - }, - AffirmationOverride { - reward_cycle: 415, - affirmation: "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpaaa".to_string() - }]; - - // Now compute the 2.5 overrides. - let affirmations_pre_2_5 = "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpaaaapppppppnnnnnnnnnnnnnnnnnnnnnnnpnppnppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnpnpppppppppnppnnnnnnnnnnnnnnnnnnnnnnnnnppnppppppppp"; - let xenon_pox_consts = PoxConstants::testnet_default(); - let last_present_cycle = xenon_pox_consts - .block_height_to_reward_cycle( - BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT, - BITCOIN_TESTNET_STACKS_25_BURN_HEIGHT, - ) - .unwrap(); - assert_eq!( - u64::try_from(affirmations_pre_2_5.len()).unwrap(), - last_present_cycle - 1 - ); - let last_override = xenon_pox_consts - .block_height_to_reward_cycle( - BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT, - BITCOIN_TESTNET_STACKS_25_REORGED_HEIGHT, - ) - .unwrap(); - let override_values = ["a", "n"]; - - for (override_index, reward_cycle) in (last_present_cycle + 1..=last_override).enumerate() { - assert!(override_values.len() > override_index); - let overrides = override_values[..(override_index + 1)].join(""); - let affirmation = format!("{affirmations_pre_2_5}{overrides}"); - default_overrides.push(AffirmationOverride { - reward_cycle, - affirmation, - }); - } - - if let Some(affirmation_overrides) = self.affirmation_overrides.as_mut() { - for affirmation in default_overrides { - // insert at front, so that the hashmap uses the configured overrides - // instead of the defaults (the configured overrides will write over the - // the defaults because they come later in the list). - affirmation_overrides.insert(0, affirmation); - } - } else { - self.affirmation_overrides = Some(default_overrides); - }; - } - fn into_config_default( mut self, default_burnchain_config: BurnchainConfig, @@ -1851,7 +1762,6 @@ impl BurnchainConfigFile { if self.magic_bytes.is_none() { self.magic_bytes = ConfigFile::xenon().burnchain.unwrap().magic_bytes; } - self.add_affirmation_overrides_xenon(); } let mode = self.mode.unwrap_or(default_burnchain_config.mode); @@ -1870,25 +1780,6 @@ impl BurnchainConfigFile { } } - let mut affirmation_overrides = HashMap::new(); - if let Some(aos) = self.affirmation_overrides { - for ao in aos { - let Some(affirmation_map) = AffirmationMap::decode(&ao.affirmation) else { - return Err(format!( - "Invalid affirmation override for reward cycle {}: {}", - ao.reward_cycle, ao.affirmation - )); - }; - if u64::try_from(affirmation_map.len()).unwrap() != ao.reward_cycle - 1 { - return Err(format!( - "Invalid affirmation override for reward cycle {}. Map len = {}, but expected {}.", - ao.reward_cycle, affirmation_map.len(), ao.reward_cycle - 1, - )); - } - affirmation_overrides.insert(ao.reward_cycle, affirmation_map); - } - } - let mut config = BurnchainConfig { chain: self.chain.unwrap_or(default_burnchain_config.chain), chain_id: match self.chain_id { @@ -1993,7 +1884,6 @@ impl BurnchainConfigFile { pox_prepare_length: self .pox_prepare_length .or(default_burnchain_config.pox_prepare_length), - affirmation_overrides, fault_injection_burnchain_block_delay: self .fault_injection_burnchain_block_delay .unwrap_or(default_burnchain_config.fault_injection_burnchain_block_delay), @@ -4844,104 +4734,6 @@ mod tests { ); } - #[test] - fn should_load_affirmation_map() { - let affirmation_string = "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa"; - let affirmation = - AffirmationMap::decode(affirmation_string).expect("Failed to decode affirmation map"); - let config = Config::from_config_file( - ConfigFile::from_str(&format!( - r#" - [[burnchain.affirmation_overrides]] - reward_cycle = 413 - affirmation = "{affirmation_string}" - "# - )) - .expect("Expected to be able to parse config file from string"), - false, - ) - .expect("Expected to be able to parse affirmation map from file"); - - assert_eq!(config.burnchain.affirmation_overrides.len(), 1); - assert_eq!(config.burnchain.affirmation_overrides.get(&0), None); - assert_eq!( - config.burnchain.affirmation_overrides.get(&413), - Some(&affirmation) - ); - } - - #[test] - fn should_fail_to_load_invalid_affirmation_map() { - let bad_affirmation_string = "bad_map"; - let file = ConfigFile::from_str(&format!( - r#" - [[burnchain.affirmation_overrides]] - reward_cycle = 1 - affirmation = "{bad_affirmation_string}" - "# - )) - .expect("Expected to be able to parse config file from string"); - - assert!(Config::from_config_file(file, false).is_err()); - } - - #[test] - fn should_load_empty_affirmation_map() { - let config = Config::from_config_file( - ConfigFile::from_str(r#""#) - .expect("Expected to be able to parse config file from string"), - false, - ) - .expect("Expected to be able to parse affirmation map from file"); - - assert!(config.burnchain.affirmation_overrides.is_empty()); - } - - #[test] - fn should_include_xenon_default_affirmation_overrides() { - let config = Config::from_config_file( - ConfigFile::from_str( - r#" - [burnchain] - chain = "bitcoin" - mode = "xenon" - "#, - ) - .expect("Expected to be able to parse config file from string"), - false, - ) - .expect("Expected to be able to parse affirmation map from file"); - // Should default add xenon affirmation overrides - assert_eq!(config.burnchain.affirmation_overrides.len(), 5); - } - - #[test] - fn should_override_xenon_default_affirmation_overrides() { - let affirmation_string = "aaapnnnnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnpppppnnnnnnnnnnnnnnnnnnnnnnnpppppppppppppppnnnnnnnnnnnnnnnnnnnnnnnppppppppppnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnppppppppnnnnnnnnnnnnnnnnnnnnnnnppnppnnnnnnnnnnnnnnnnnnnnnnnppppnnnnnnnnnnnnnnnnnnnnnnnnnppppppnnnnnnnnnnnnnnnnnnnnnnnnnppnnnnnnnnnnnnnnnnnnnnnnnnnpppppppnnnnnnnnnnnnnnnnnnnnnnnnnnpnnnnnnnnnnnnnnnnnnnnnnnnnpppnppppppppppppppnnppppnpa"; - let affirmation = - AffirmationMap::decode(affirmation_string).expect("Failed to decode affirmation map"); - - let config = Config::from_config_file( - ConfigFile::from_str(&format!( - r#" - [burnchain] - chain = "bitcoin" - mode = "xenon" - - [[burnchain.affirmation_overrides]] - reward_cycle = 413 - affirmation = "{affirmation_string}" - "#, - )) - .expect("Expected to be able to parse config file from string"), - false, - ) - .expect("Expected to be able to parse affirmation map from file"); - // Should default add xenon affirmation overrides, but overwrite with the configured one above - assert_eq!(config.burnchain.affirmation_overrides.len(), 5); - assert_eq!(config.burnchain.affirmation_overrides[&413], affirmation); - } - #[test] fn test_into_config_default_chain_id() { // Helper function to create BurnchainConfigFile with mode and optional chain_id From b74cf107062a6e64465cf5a4f64f89c13108c280 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Jul 2025 17:03:15 -0700 Subject: [PATCH 19/35] Fix test_generate_markdown_with_real_fixture_data Signed-off-by: Jacinta Ferrant --- .../tests/fixtures/minimal_config.json | 2 +- stackslib/src/config/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json b/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json index 0cce5ccbd4..7d0b8edd52 100644 --- a/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json +++ b/contrib/tools/config-docs-generator/tests/fixtures/minimal_config.json @@ -74,6 +74,6 @@ "referenced_constants": { "MinerConfig::mining_key": null, "NodeConfig::miner": null, - "NodeConfig::mine_microblocks": null, + "NodeConfig::mine_microblocks": null } } \ No newline at end of file diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index 2be8ad94d7..43d3772454 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -47,9 +47,9 @@ use crate::chainstate::stacks::MAX_BLOCK_LEN; use crate::config::chain_data::MinerStats; use crate::core::mempool::{MemPoolWalkSettings, MemPoolWalkStrategy, MemPoolWalkTxTypes}; use crate::core::{ - MemPoolDB, StacksEpoch, StacksEpochExtension, StacksEpochId, - CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, - PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, STACKS_EPOCHS_REGTEST, STACKS_EPOCHS_TESTNET, + MemPoolDB, StacksEpoch, StacksEpochExtension, StacksEpochId, CHAIN_ID_MAINNET, + CHAIN_ID_TESTNET, PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, STACKS_EPOCHS_REGTEST, + STACKS_EPOCHS_TESTNET, }; use crate::cost_estimates::fee_medians::WeightedMedianFeeRateEstimator; use crate::cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; From e11434517ef18bb321b283a0b86ab03209879023 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Jul 2025 17:03:34 -0700 Subject: [PATCH 20/35] Fix test_get_block_availability Signed-off-by: Jacinta Ferrant --- stackslib/src/net/tests/download/epoch2x.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/stackslib/src/net/tests/download/epoch2x.rs b/stackslib/src/net/tests/download/epoch2x.rs index 3d13ae5b06..b1462a1ec9 100644 --- a/stackslib/src/net/tests/download/epoch2x.rs +++ b/stackslib/src/net/tests/download/epoch2x.rs @@ -101,8 +101,10 @@ fn test_get_block_availability() { TestPeer::set_ops_burn_header_hash(&mut burn_ops, &burn_header_hash); - peer_1.next_burnchain_block_raw(burn_ops); - + // We do not have the anchor block for peer 1, therefore it cannot advance its tip. + if i < 6 { + peer_1.next_burnchain_block_raw(burn_ops); + } let sn = SortitionDB::get_canonical_burn_chain_tip(peer_2.sortdb.as_ref().unwrap().conn()) .unwrap(); From 53bf9ea46fe25f56111d4201bce85ef69b6cfeb9 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 7 Aug 2025 10:26:46 -0700 Subject: [PATCH 21/35] Remove TODO comment Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/coordinator/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 1470035cbf..885a23f4af 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -763,7 +763,6 @@ pub fn get_reward_cycle_info( let reward_cycle_info = { let ic = sort_db.index_handle(sortition_tip); - // TODO: always use block-commit-based PoX anchor block selection rules? let burnchain_db_conn_opt = if epoch_at_height.epoch_id >= StacksEpochId::Epoch21 { // use the new block-commit-based PoX anchor block selection rules Some(burnchain_db.conn()) From 9cad3868a67e6371ad27f413a719bc941783cd28 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 7 Aug 2025 10:31:51 -0700 Subject: [PATCH 22/35] Make test_nakamoto_inv_sync_across_epoch_change panic instead of timing out CI Signed-off-by: Jacinta Ferrant --- stackslib/src/net/tests/inv/nakamoto.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stackslib/src/net/tests/inv/nakamoto.rs b/stackslib/src/net/tests/inv/nakamoto.rs index df85ea84db..a510c992c8 100644 --- a/stackslib/src/net/tests/inv/nakamoto.rs +++ b/stackslib/src/net/tests/inv/nakamoto.rs @@ -1008,6 +1008,8 @@ fn test_nakamoto_inv_sync_across_epoch_change() { .block_height_to_reward_cycle(tip.block_height) .unwrap(); + let timeout = std::time::Duration::from_secs(30); + let start = std::time::Instant::now(); // run peer and other_peer until they connect loop { let _ = peer.step_with_ibd(false); @@ -1019,6 +1021,10 @@ fn test_nakamoto_inv_sync_across_epoch_change() { if event_ids.count() > 0 && other_event_ids.count() > 0 { break; } + assert!( + start.elapsed() < timeout, + "Timed out waiting for peer's to connect" + ); } debug!("Peers are connected"); @@ -1036,6 +1042,7 @@ fn test_nakamoto_inv_sync_across_epoch_change() { let burn_tip_start = peer.network.get_current_epoch().start_height; + let start = std::time::Instant::now(); while inv_1_count < num_epoch2_blocks || inv_2_count < num_epoch2_blocks || highest_rc_1 < total_rcs @@ -1098,6 +1105,11 @@ fn test_nakamoto_inv_sync_across_epoch_change() { info!( "Nakamoto state machine: Peer 1: {highest_rc_1}, Peer 2: {highest_rc_2} (total {total_rcs})" ); + + assert!( + start.elapsed() < timeout, + "Timed out waiting for inv sync across epoch. Ran {round} rounds." + ); } } From 3bf47ddd5aa524a81a78b8d153ebdd3e78791b31 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 22 Aug 2025 14:07:35 -0700 Subject: [PATCH 23/35] Remove drive_chain_livenes which was meant to address potential bug in affirmation map flow around failure to announce blocks Signed-off-by: Jacinta Ferrant --- stacks-node/src/run_loop/neon.rs | 160 +------------------------------ 1 file changed, 1 insertion(+), 159 deletions(-) diff --git a/stacks-node/src/run_loop/neon.rs b/stacks-node/src/run_loop/neon.rs index 11ee5bbb00..2b2ec97857 100644 --- a/stacks-node/src/run_loop/neon.rs +++ b/stacks-node/src/run_loop/neon.rs @@ -3,8 +3,8 @@ use std::sync::atomic::AtomicU64; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::sync_channel; use std::sync::{Arc, Mutex}; +use std::thread; use std::thread::JoinHandle; -use std::{cmp, thread}; use libc; use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType}; @@ -27,7 +27,6 @@ use stacks_common::deps_common::ctrlc as termination; use stacks_common::deps_common::ctrlc::SignalId; use stacks_common::types::PublicKey; use stacks_common::util::hash::Hash160; -use stacks_common::util::{get_epoch_time_secs, sleep_ms}; use stx_genesis::GenesisData; use super::RunLoopCallbacks; @@ -65,12 +64,6 @@ pub struct RunLoopCounter(pub Arc); #[derive(Clone)] pub struct RunLoopCounter(); -#[cfg(test)] -const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 30; - -#[cfg(not(test))] -const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 300; - impl Default for RunLoopCounter { #[cfg(test)] fn default() -> Self { @@ -781,153 +774,6 @@ impl RunLoop { ) } - /// Wake up and drive stacks block processing if there's been a PoX reorg. - /// Be careful not to saturate calls to announce new stacks blocks, because that will disable - /// mining (which would prevent a miner attempting to fix a hidden PoX anchor block from making - /// progress). - fn drive_pox_reorg_stacks_block_processing( - globals: &Globals, - config: &Config, - last_stacks_pox_reorg_recover_time: &mut u128, - ) { - let miner_config = config.get_miner_config(); - let delay = cmp::max( - config.node.chain_liveness_poll_time_secs, - cmp::max( - miner_config.first_attempt_time_ms, - miner_config.subsequent_attempt_time_ms, - ) / 1000, - ); - - if *last_stacks_pox_reorg_recover_time + (delay as u128) >= get_epoch_time_secs().into() { - // too soon - return; - } - - // announce a new stacks block to force the chains coordinator - // to wake up anyways. this isn't free, so we have to make sure - // the chain-liveness thread doesn't wake up too often - globals.coord().announce_new_stacks_block(); - - *last_stacks_pox_reorg_recover_time = get_epoch_time_secs().into(); - } - - /// Wake up and drive sortition processing if there's been a PoX reorg. - /// Be careful not to saturate calls to announce new burn blocks, because that will disable - /// mining (which would prevent a miner attempting to fix a hidden PoX anchor block from making - /// progress). - /// - /// only call if no in ibd - fn drive_pox_reorg_burn_block_processing( - globals: &Globals, - config: &Config, - burnchain: &Burnchain, - sortdb: &SortitionDB, - last_burn_pox_reorg_recover_time: &mut u128, - last_announce_time: &mut u128, - ) { - let miner_config = config.get_miner_config(); - let delay = cmp::max( - config.node.chain_liveness_poll_time_secs, - cmp::max( - miner_config.first_attempt_time_ms, - miner_config.subsequent_attempt_time_ms, - ) / 1000, - ); - - if *last_burn_pox_reorg_recover_time + (delay as u128) >= get_epoch_time_secs().into() { - // too soon - return; - } - - // compare sortition and heaviest AMs - let burnchain_db = burnchain - .open_burnchain_db(false) - .expect("FATAL: failed to open burnchain DB"); - - let highest_sn = SortitionDB::get_highest_known_burn_chain_tip(sortdb.conn()) - .expect("FATAL: could not read sortition DB"); - - let canonical_burnchain_tip = burnchain_db - .get_canonical_chain_tip() - .expect("FATAL: could not read burnchain DB"); - - if canonical_burnchain_tip.block_height > highest_sn.block_height { - // still processing sortitions - test_debug!( - "Drive burn block processing: still processing sortitions ({} > {})", - canonical_burnchain_tip.block_height, - highest_sn.block_height - ); - return; - } - - *last_burn_pox_reorg_recover_time = get_epoch_time_secs().into(); - - // unconditionally bump every 5 minutes, just in case. - // this can get the node un-stuck if we're short on sortition processing but are unable to - // sync with the remote node because it keeps NACK'ing us, leading to a runloop stall. - if *last_announce_time + (UNCONDITIONAL_CHAIN_LIVENESS_CHECK as u128) - < get_epoch_time_secs().into() - { - debug!("Drive burnchain processing: unconditional bump"); - globals.coord().announce_new_burn_block(); - globals.coord().announce_new_stacks_block(); - *last_announce_time = get_epoch_time_secs().into(); - } - } - - /// In a separate thread, periodically drive coordinator liveness by checking to see if there's - /// a pending reorg and if so, waking up the coordinator to go and process new blocks - fn drive_chain_liveness( - globals: Globals, - config: Config, - burnchain: Burnchain, - sortdb: SortitionDB, - ) { - let mut last_burn_pox_reorg_recover_time = 0; - let mut last_stacks_pox_reorg_recover_time = 0; - let mut last_burn_announce_time = 0; - - debug!("Chain-liveness thread start!"); - - while globals.keep_running() { - debug!("Chain-liveness checkup"); - Self::drive_pox_reorg_burn_block_processing( - &globals, - &config, - &burnchain, - &sortdb, - &mut last_burn_pox_reorg_recover_time, - &mut last_burn_announce_time, - ); - Self::drive_pox_reorg_stacks_block_processing( - &globals, - &config, - &mut last_stacks_pox_reorg_recover_time, - ); - - sleep_ms(3000); - } - - debug!("Chain-liveness thread exit!"); - } - - /// Spawn a thread to drive chain liveness - fn spawn_chain_liveness_thread(&self, globals: Globals) -> JoinHandle<()> { - let config = self.config.clone(); - let burnchain = self.get_burnchain(); - let sortdb = burnchain - .open_sortition_db(true) - .expect("FATAL: could not open sortition DB"); - - thread::Builder::new() - .name(format!("chain-liveness-{}", config.node.rpc_bind)) - .stack_size(BLOCK_PROCESSOR_STACK_SIZE) - .spawn(move || Self::drive_chain_liveness(globals, config, burnchain, sortdb)) - .expect("FATAL: failed to spawn chain liveness thread") - } - /// Starts the node runloop. /// /// This function will block by looping infinitely. @@ -1025,7 +871,6 @@ impl RunLoop { // Boot up the p2p network and relayer, and figure out how many sortitions we have so far // (it could be non-zero if the node is resuming from chainstate) let mut node = StacksNode::spawn(self, globals.clone(), relay_recv); - let liveness_thread = self.spawn_chain_liveness_thread(globals.clone()); // Wait for all pending sortitions to process let burnchain_db = burnchain_config @@ -1061,7 +906,6 @@ impl RunLoop { globals.coord().stop_chains_coordinator(); coordinator_thread_handle.join().unwrap(); let peer_network = node.join(); - liveness_thread.join().unwrap(); // Data that will be passed to Nakamoto run loop // Only gets transfered on clean shutdown of neon run loop @@ -1185,7 +1029,6 @@ impl RunLoop { globals.coord().stop_chains_coordinator(); coordinator_thread_handle.join().unwrap(); let peer_network = node.join(); - liveness_thread.join().unwrap(); // Data that will be passed to Nakamoto run loop // Only gets transfered on clean shutdown of neon run loop @@ -1258,7 +1101,6 @@ impl RunLoop { globals.coord().stop_chains_coordinator(); coordinator_thread_handle.join().unwrap(); let peer_network = node.join(); - liveness_thread.join().unwrap(); // Data that will be passed to Nakamoto run loop // Only gets transfered on clean shutdown of neon run loop From 5ed4860239b2b6dfecab1adecbf82085832d70b0 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 25 Aug 2025 13:02:18 -0700 Subject: [PATCH 24/35] CRC: should include prior reward cycle for get_block_scan_start Signed-off-by: Jacinta Ferrant --- stackslib/src/net/inv/epoch2x.rs | 7 ++++++- stackslib/src/net/tests/inv/epoch2x.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index 2815cd06da..ce14adc303 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -1827,7 +1827,12 @@ impl PeerNetwork { .block_height_to_reward_cycle(stacks_tip_burn_block_height) .unwrap_or(0); - let rescan_rc = stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles); + // Always want to do the last reward cycle AND the current reward cycle (we could be still looking for the current reward cycles anchor block which is mined in the prior reward cycle) + let prior_rc = stacks_tip_rc.saturating_sub(1); + let rescan_rc = std::cmp::min( + stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles), + prior_rc, + ); test_debug!("begin blocks inv scan at {rescan_rc}"); rescan_rc diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index 23105b3c51..1f3d2091c3 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -1254,7 +1254,7 @@ fn test_inv_sync_start_reward_cycle() { let block_scan_start = peer_1 .network .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); - assert_eq!(block_scan_start, 8); + assert_eq!(block_scan_start, 7); peer_1.network.connection_opts.inv_reward_cycles = 1; From ae4167827384faa9702d2de9f18cd6631c7fb53a Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 25 Aug 2025 13:37:42 -0700 Subject: [PATCH 25/35] WIP: remove process_affirmation_maps and always call get_chosen_pox_anchor_v205 Signed-off-by: Jacinta Ferrant --- stacks-node/src/node.rs | 13 +- stackslib/src/burnchains/affirmation.rs | 89 - stackslib/src/burnchains/burnchain.rs | 38 - stackslib/src/burnchains/tests/affirmation.rs | 1714 ----------------- stackslib/src/chainstate/burn/db/sortdb.rs | 56 +- stackslib/src/chainstate/coordinator/mod.rs | 19 +- stackslib/src/chainstate/coordinator/tests.rs | 27 - .../src/chainstate/nakamoto/tests/mod.rs | 1 - .../src/chainstate/stacks/db/accounts.rs | 1 - stackslib/src/chainstate/stacks/db/blocks.rs | 108 +- stackslib/src/chainstate/stacks/db/headers.rs | 31 +- stackslib/src/chainstate/stacks/db/mod.rs | 3 - .../stacks/tests/chain_histories.rs | 75 +- stackslib/src/cli.rs | 17 - stackslib/src/config/mod.rs | 6 - stackslib/src/core/mod.rs | 5 - stackslib/src/core/tests/mod.rs | 1 - stackslib/src/main.rs | 7 +- stackslib/src/net/inv/epoch2x.rs | 13 +- stackslib/src/net/mod.rs | 8 - 20 files changed, 38 insertions(+), 2194 deletions(-) diff --git a/stacks-node/src/node.rs b/stacks-node/src/node.rs index 3ef15d45ab..cdef2a85ff 100644 --- a/stacks-node/src/node.rs +++ b/stacks-node/src/node.rs @@ -840,22 +840,13 @@ impl Node { parent_consensus_hash }; - let burnchain = self.config.get_burnchain(); - let burnchain_db = - BurnchainDB::connect(&burnchain.get_burnchaindb_path(), &burnchain, true) - .expect("FATAL: failed to connect to burnchain DB"); - let atlas_config = Self::make_atlas_config(); let mut processed_blocks = vec![]; loop { let mut process_blocks_at_tip = { let tx = db.tx_begin_at_tip(); - self.chain_state.process_blocks( - burnchain_db.conn(), - tx, - 1, - Some(&self.event_dispatcher), - ) + self.chain_state + .process_blocks(tx, 1, Some(&self.event_dispatcher)) }; match process_blocks_at_tip { Err(e) => panic!("Error while processing block - {e:?}"), diff --git a/stackslib/src/burnchains/affirmation.rs b/stackslib/src/burnchains/affirmation.rs index a890270668..79d986fda1 100644 --- a/stackslib/src/burnchains/affirmation.rs +++ b/stackslib/src/burnchains/affirmation.rs @@ -1158,92 +1158,3 @@ pub fn find_pox_anchor_block( .map(|(anchor_block_commit, descendancy, ..)| (anchor_block_commit, descendancy)), )) } - -/// Update a completed reward cycle's affirmation maps -pub fn update_pox_affirmation_maps( - burnchain_db: &mut BurnchainDB, - indexer: &B, - reward_cycle: u64, - burnchain: &Burnchain, -) -> Result<(), Error> { - debug!("Process PoX affirmations for reward cycle {}", reward_cycle); - - let tx = burnchain_db.tx_begin()?; - - let (prepare_ops, pox_anchor_block_info_opt) = - find_pox_anchor_block(&tx, reward_cycle, indexer, burnchain)?; - - if let Some((anchor_block, descendancy)) = pox_anchor_block_info_opt { - debug!( - "PoX anchor block elected in reward cycle {} for reward cycle {} is {}", - reward_cycle, - reward_cycle + 1, - &anchor_block.block_header_hash - ); - - // anchor block found for this upcoming reward cycle - tx.set_anchor_block(&anchor_block, reward_cycle + 1)?; - assert_eq!(descendancy.len(), prepare_ops.len()); - - // mark the prepare-phase commits that elected this next reward cycle's anchor block as - // having descended or not descended from this anchor block. - for (block_idx, block_ops) in prepare_ops.iter().enumerate() { - let descendancy = descendancy - .get(block_idx) - .ok_or_else(|| Error::ProcessorError)?; - assert_eq!(block_ops.len(), descendancy.len()); - - for (tx_idx, tx_op) in block_ops.iter().enumerate() { - test_debug!( - "Make affirmation map for block-commit at {},{}", - tx_op.block_height, - tx_op.vtxindex - ); - let is_tx_descendant = descendancy - .get(tx_idx) - .ok_or_else(|| Error::ProcessorError)?; - tx.make_prepare_phase_affirmation_map( - indexer, - burnchain, - reward_cycle + 1, - tx_op, - Some(&anchor_block), - *is_tx_descendant, - )?; - } - } - } else { - debug!("PoX anchor block selected in reward cycle {} is None. Reward cycle {} has no anchor block", reward_cycle, reward_cycle + 1); - - // anchor block not found for this upcoming reward cycle - tx.clear_anchor_block(reward_cycle + 1)?; - - // mark all prepare-phase commits as NOT having descended from the next reward cycle's anchor - // block as NOT having descended from any anchor block (since one was not chosen) - for block_ops in prepare_ops.iter() { - for tx_op in block_ops.iter() { - test_debug!( - "Make affirmation map for block-commit at {},{}", - tx_op.block_height, - tx_op.vtxindex - ); - tx.make_prepare_phase_affirmation_map( - indexer, - burnchain, - reward_cycle + 1, - tx_op, - None, - false, - )?; - } - } - } - - tx.commit()?; - debug!( - "Processed PoX affirmations for reward cycle {}", - reward_cycle - ); - - Ok(()) -} diff --git a/stackslib/src/burnchains/burnchain.rs b/stackslib/src/burnchains/burnchain.rs index 24af4362c0..9d0d8953be 100644 --- a/stackslib/src/burnchains/burnchain.rs +++ b/stackslib/src/burnchains/burnchain.rs @@ -33,7 +33,6 @@ use stacks_common::util::vrf::VRFPublicKey; use stacks_common::util::{get_epoch_time_ms, sleep_ms}; use super::EpochList; -use crate::burnchains::affirmation::update_pox_affirmation_maps; use crate::burnchains::bitcoin::BitcoinTxOutput; use crate::burnchains::db::{BurnchainDB, BurnchainHeaderReader}; use crate::burnchains::indexer::{ @@ -1067,48 +1066,11 @@ impl Burnchain { let _blockstack_txs = burnchain_db.store_new_burnchain_block(burnchain, indexer, block, epoch_id)?; - Burnchain::process_affirmation_maps( - burnchain, - burnchain_db, - indexer, - block.block_height(), - )?; let header = block.header(); Ok(header) } - /// Update the affirmation maps for the previous reward cycle's commits. - /// This is a no-op unless the given burnchain block height falls on a reward cycle boundary. In that - /// case, the previous reward cycle's block commits' affirmation maps are all re-calculated. - pub fn process_affirmation_maps( - burnchain: &Burnchain, - burnchain_db: &mut BurnchainDB, - indexer: &B, - block_height: u64, - ) -> Result<(), burnchain_error> { - let this_reward_cycle = burnchain - .block_height_to_reward_cycle(block_height) - .unwrap_or(0); - - let prev_reward_cycle = burnchain - .block_height_to_reward_cycle(block_height.saturating_sub(1)) - .unwrap_or(0); - - if this_reward_cycle != prev_reward_cycle { - // at reward cycle boundary - info!( - "Update PoX affirmation maps for reward cycle"; - "prev_reward_cycle" => %prev_reward_cycle, - "this_reward_cycle" => %this_reward_cycle, - "block_height" => %block_height, - "cycle_length" => %burnchain.pox_constants.reward_cycle_length, - ); - update_pox_affirmation_maps(burnchain_db, indexer, prev_reward_cycle, burnchain)?; - } - Ok(()) - } - /// Hand off the block to the ChainsCoordinator _and_ process the sortition /// *only* to be used by legacy stacks node interfaces, like the Helium node. /// diff --git a/stackslib/src/burnchains/tests/affirmation.rs b/stackslib/src/burnchains/tests/affirmation.rs index 7ccb11a501..3bb67184ce 100644 --- a/stackslib/src/burnchains/tests/affirmation.rs +++ b/stackslib/src/burnchains/tests/affirmation.rs @@ -1192,1717 +1192,3 @@ fn test_find_heaviest_parent_commit_many_commits() { assert_eq!(total_confs, 3); assert_eq!(total_burns, 1 + 2 + 3); } - -#[test] -fn test_update_pox_affirmation_maps_3_forks() { - // Create three forks, such that each subsequent reward cycle only affirms the first reward cycle's anchor - // block. That is, reward cycle 2 affirms reward cycle 1's anchor block; reward cycle 3 - // affirms reward cycle 1's anchor block but not 2's, and reward cycle 4 affirms reward cycle - // 1's anchor block but not 2's or 3's. Each affirmation map has the same weight, but verify - // that the canonical affirmation map is the *last-discovered* affirmation map (i.e. the one - // with the highest affirmed anchor block -- in this case, the fork in which reward cycle 4 - // affirms reward cycle 1's anchor block, but not 2's or 3's). - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(10, 5, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits_0) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None], - ); - - // no anchor blocks recorded, yet! - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: before update: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("n").unwrap()); - - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_none() - ); - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 0, &burnchain).unwrap(); - - // there's only one anchor block in the chain so far - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - - // the anchor block itself affirms nothing, since it isn't built on an anchor block - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: after update: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("p").unwrap()); - - let anchor_block_0 = - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .unwrap() - .0; - eprintln!("anchor block 1 at height {}", anchor_block_0.block_height); - assert!(anchor_block_0.block_height < commits_0[7][0].as_ref().unwrap().block_height); - - // descend from a prepare-phase commit in rc 0, so affirms rc 0's anchor block - let (next_headers, commits_1) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[7][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 1, &burnchain).unwrap(); - - // there's two anchor blocks so far -- one for reward cycle 1, and one for reward cycle 2. - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=1: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("p").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pp").unwrap()); - - // descend from a prepare-phase commit in rc 0, so affirms rc 0's anchor block but not rc - // 1's - assert!(anchor_block_0.block_height < commits_0[6][0].as_ref().unwrap().block_height); - let (next_headers, commits_2) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[6][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 2, &burnchain).unwrap(); - - // there's three anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_some() - ); - - // there are two equivalently heavy affirmation maps, but the affirmation map discovered later - // is the heaviest. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=2: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("pa").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pap").unwrap()); - - // descend from a prepare-phase commit in rc 0, so affirms rc 0's anchor block, but not rc - // 1's or rc 2's - assert!(anchor_block_0.block_height < commits_0[8][0].as_ref().unwrap().block_height); - let (next_headers, commits_3) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[8][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 3, &burnchain).unwrap(); - - // there are three equivalently heavy affirmation maps, but the affirmation map discovered last - // is the heaviest. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=3: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("paa").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("paap").unwrap()); -} - -#[test] -fn test_update_pox_affirmation_maps_unique_anchor_block() { - // Verify that if two reward cycles choose the same anchor block, the second reward cycle to do - // so will actually have no anchor block at all (since a block-commit can be an anchor block - // for at most one reward cycle). - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(10, 5, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits_0) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None], - ); - - // no anchor blocks recorded, yet! - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: before update: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("n").unwrap()); - - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_none() - ); - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 0, &burnchain).unwrap(); - - // there's only one anchor block - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - - // the anchor block itself affirms nothing, since it isn't built on an anchor block - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: after update: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("p").unwrap()); - - let anchor_block_0 = - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .unwrap() - .0; - eprintln!("anchor block 1 at height {}", anchor_block_0.block_height); - assert!(anchor_block_0.block_height < commits_0[7][0].as_ref().unwrap().block_height); - - // try and select the same anchor block, twice - let mut dup_commits = commits_0.clone(); - for (i, cmts) in dup_commits.iter_mut().enumerate() { - let block_header = BurnchainBlockHeader { - block_height: (i + commits_0.len() + 1) as u64, - block_hash: next_burn_header_hash(), - parent_block_hash: headers - .last() - .map(|blk| blk.block_hash.clone()) - .unwrap_or(first_bhh.clone()), - num_txs: cmts.len() as u64, - timestamp: (i + commits_0.len()) as u64, - }; - - for cmt_opt in cmts.iter_mut() { - if let Some(cmt) = cmt_opt.as_mut() { - cmt.block_height = block_header.block_height; - cmt.parent_block_ptr = anchor_block_0.block_height as u32; - cmt.parent_vtxindex = anchor_block_0.vtxindex as u16; - cmt.burn_parent_modulus = - ((cmt.block_height - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8; - cmt.burn_header_hash = block_header.block_hash.clone(); - cmt.block_header_hash = next_block_hash(); - } - } - - headers.push(block_header.clone()); - - let cmt_ops: Vec = cmts - .iter() - .filter_map(|op| op.clone()) - .map(BlockstackOperationType::LeaderBlockCommit) - .collect(); - - burnchain_db - .store_new_burnchain_block_ops_unchecked(&burnchain, &headers, &block_header, &cmt_ops) - .unwrap(); - } - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 1, &burnchain).unwrap(); - - // there's still only one anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_none() - ); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=1: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pn").unwrap()); -} - -#[test] -fn test_update_pox_affirmation_maps_absent() { - // Create two fork histories, both of which affirm the *absence* of different anchor blocks, - // and both of which contain stretches of reward cycles in which no reward cycle was chosen. - // Verify that an affirmation map becomes canonical only by affirming the *presence* of more - // anchor blocks than others -- i.e. affirmation maps that grow by adding reward cycles in - // which there was no anchor block chosen do *not* increase in weight (and thus the canonical - // affirmation map does *not* change even though multiple reward cycles pass with no anchor - // block chosen). - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(10, 5, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // make two histories -- one with an anchor block, and one without. - let (next_headers, commits_0) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None, None], - ); - - // no anchor blocks recorded, yet! - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - assert_eq!(heaviest_am, AffirmationMap::empty()); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_none() - ); - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 0, &burnchain).unwrap(); - - // there's only one anchor block, and it's at vtxindex 1 (not 0) - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert_eq!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .unwrap() - .0 - .vtxindex, - 1 - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_none() - ); - - // the anchor block itself affirms nothing - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("p").unwrap()); - - for i in 5..10 { - let block_commit = BurnchainDB::get_block_commit( - burnchain_db.conn(), - &commits_0[i][0].as_ref().unwrap().burn_header_hash, - &commits_0[i][0].as_ref().unwrap().txid, - ) - .unwrap() - .unwrap(); - assert_eq!(block_commit.vtxindex, 0); - - let block_commit_metadata = BurnchainDB::get_commit_metadata( - burnchain_db.conn(), - &block_commit.burn_header_hash, - &block_commit.txid, - ) - .unwrap() - .unwrap(); - assert_eq!(block_commit_metadata.anchor_block_descendant, None); - } - - // build a second reward cycle off of a commit that does _not_ affirm the first anchor - // block - let (next_headers, commits_1) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[9][1].clone(), commits_0[9][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 1, &burnchain).unwrap(); - - // the second anchor block affirms that the first anchor block is missing. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=1: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("a").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("ap").unwrap()); - - // build a third reward cycle off of a commit in the second reward cycle, but make it so - // that there is no anchor block mined - let (next_headers, commits_2) = make_reward_cycle_without_anchor( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_1[9][0].clone(), commits_1[9][1].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 2, &burnchain).unwrap(); - - // there isn't a third anchor block - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - - // heaviest _anchor block_ affirmation map is unchanged. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=2: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("a").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("apn").unwrap()); - - // build a fourth reward cycle off of a commit in the third reward cycle, but make it so - // that there is no anchor block mined - assert!(commits_2[5][0].is_some()); - assert!(commits_2[5][1].is_some()); - let (next_headers, commits_3) = make_reward_cycle_without_anchor( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_2[5][0].clone(), commits_2[5][1].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 3, &burnchain).unwrap(); - - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_none() - ); - - // heaviest _anchor block_ affirmation map is unchanged. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=3: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("a").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("apnn").unwrap()); - - // make a fourth fifth cycle, again with a missing anchor block - assert!(commits_3[5][0].is_some()); - assert!(commits_3[5][1].is_some()); - let (next_headers, commits_4) = make_reward_cycle_without_anchor( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_3[5][0].clone(), commits_3[5][1].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 4, &burnchain).unwrap(); - - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 4) - .unwrap() - .is_none() - ); - - // heaviest _anchor block_ affirmation map advances - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=4: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("a").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("apnnn").unwrap()); - - // make a fifth reward cycle, but with an anchor block. Affirms the first anchor block by - // descending from a chain that descends from it. - assert!(commits_4[5][0].is_some()); - assert!(commits_4[5][1].is_some()); - let (next_headers, commits_5) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_4[5][1].clone(), commits_4[5][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 5, &burnchain).unwrap(); - - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 4) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 5) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 6) - .unwrap() - .is_some() - ); - - // heaviest _anchor block_ affirmation map advances, since the new anchor block affirms the - // last 4 reward cycles, including the anchor block mined in the first reward cycle - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=5: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - // anchor block was chosen in the last reward cycle, and in doing so created the heaviest - // affirmation map for an anchor block, so the canonical affirmation map is - // whatever that last anchor block affirmed - assert_eq!(heaviest_am, AffirmationMap::decode("pannn").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pannnp").unwrap()); - - // make a third history that affirms _nothing_. It should eventually overtake this last - // heaviest affirmation map - let mut start = vec![commits_0[3][1].clone()]; - for i in 0..6 { - let (next_headers, commits) = make_reward_cycle_with_vote( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - start, - false, - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 6 + i, &burnchain).unwrap(); - start = vec![commits[5][0].clone()]; - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc={}: heaviest = {}, canonical = {}", - 6 + i, - &heaviest_am, - &canonical_am - ); - } - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=11: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("pannn").unwrap()); - assert_eq!( - canonical_am, - AffirmationMap::decode("pannnpnnnnnn").unwrap() - ); - - // other affirmation map should be present - let unaffirmed_am = AffirmationMap::decode("aannnannnnnn").unwrap(); - let am_id = BurnchainDB::get_affirmation_map_id(burnchain_db.conn(), &unaffirmed_am) - .unwrap() - .unwrap(); - let weight = BurnchainDB::get_affirmation_weight(burnchain_db.conn(), am_id) - .unwrap() - .unwrap(); - assert_eq!(weight, 9); -} - -#[test] -fn test_update_pox_affirmation_maps_nothing() { - // Create a sequence of reward cycles that alternate between selecting (and affirming) an - // anchor block, and not selecting an anchor block at all. Verify that in all cases the - // canonical affirmation map is still the affirmation map with the most affirmed anchor blocks - // (`pn`), and verify that the heaviest affirmation map (given the unconfirmed anchor block oracle - // closure) can alternate between either `pnpn` or `pnan` based on whether or not the oracle - // declares an anchor block present or absent in the chain state. - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(10, 5, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits_0) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None], - ); - - // no anchor blocks recorded, yet! - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - assert_eq!(heaviest_am, AffirmationMap::empty()); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_none() - ); - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 0, &burnchain).unwrap(); - - // there's only one anchor block - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - - // the anchor block itself affirms nothing, since it isn't built on an anchor block - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("p").unwrap()); - - // build a second reward cycle off of the first, but with no anchor block - let (next_headers, commits_1) = make_reward_cycle_with_vote( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[9][0].clone()], - false, - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 1, &burnchain).unwrap(); - - // there's still one anchor block - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_none() - ); - - // second reward cycle doesn't have an anchor block, so there's no heaviest anchor block - // affirmation map yet - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=1: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pn").unwrap()); - - // build a 3rd reward cycle, but it affirms an anchor block - let last_commit_1 = { - let mut last_commit = None; - for i in 0..commits_1.len() { - if commits_1[i][0].is_some() { - last_commit = commits_1[i][0].clone(); - } - } - last_commit - }; - - let (next_headers, commits_2) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![last_commit_1], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 2, &burnchain).unwrap(); - - // there's two anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 4) - .unwrap() - .is_none() - ); - - // there's no anchor block in rc 1 - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=2: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("pn").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pnp").unwrap()); - - // build a fourth reward cycle, with no vote - let (next_headers, commits_3) = make_reward_cycle_with_vote( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_2[9][0].clone()], - false, - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 3, &burnchain).unwrap(); - - // there are three equivalently heavy affirmation maps, but the affirmation map discovered last - // is the heaviest. BUT THIS TIME, MAKE THE UNCONFIRMED ORACLE DENY THAT THIS LAST - // ANCHORED BLOCK EXISTS. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| false, - ) - .unwrap(); - eprintln!( - "rc=3 (deny): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("pn").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pnan").unwrap()); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=3 (exist): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("pn").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pnpn").unwrap()); -} - -#[test] -fn test_update_pox_affirmation_fork_2_cycles() { - // Create two forks, where miners work on each fork for two cycles (so, there are four reward - // cycles in total, but miners spend the first two reward cycles on fork 1 and the next two - // reward cycles on fork 2). The second fork does NOT affirm the anchor blocks in the first - // fork. Verify that the canonical affirmation map progresses from `paa` to `aap` once the - // second fork affirms two anchor blocks (note that ties in affirmation map weights are broken - // by most-recently-affirmed anchor block). - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(5, 2, 2, 25); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits_0) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None], - ); - - // no anchor blocks recorded, yet! - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - assert_eq!(heaviest_am, AffirmationMap::empty()); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_none() - ); - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 0, &burnchain).unwrap(); - - // there's only one anchor block - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - - // the anchor block itself affirms nothing, since it isn't built on an anchor block - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0 (true): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("p").unwrap()); - - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| false, - ) - .unwrap(); - eprintln!( - "rc=0 (false): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(canonical_am, AffirmationMap::decode("a").unwrap()); - - // build a second reward cycle off of the first - let (next_headers, commits_1) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[4][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 1, &burnchain).unwrap(); - - // there's two anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - - // the network affirms two anchor blocks, but the second anchor block only affirms the - // first anchor block. - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=1 (true): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("p").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pp").unwrap()); - - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| false, - ) - .unwrap(); - eprintln!( - "rc=1 (false): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(canonical_am, AffirmationMap::decode("pa").unwrap()); - - // build a third reward cycle off of the first, before the 2nd's anchor block - let (next_headers, commits_2) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[1][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 2, &burnchain).unwrap(); - - // there's four anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_some() - ); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=2 (true): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("p").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("ppp").unwrap()); - - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| false, - ) - .unwrap(); - eprintln!( - "rc=2 (false): heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(canonical_am, AffirmationMap::decode("paa").unwrap()); - - // build a fourth reward cycle off of the third - let (next_headers, commits_3) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_2[4][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 3, &burnchain).unwrap(); - - // there's four anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 4) - .unwrap() - .is_some() - ); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=3: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("aap").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("aapp").unwrap()); -} - -#[test] -fn test_update_pox_affirmation_fork_duel() { - // Create two forks where miners alternate between working on forks (i.e. selecting anchor - // blocks) at each reward cycle. That is, in odd reward cycles, miners work on fork #1, and in - // even reward cycles, they work on fork #2. Verify that the canonical affirmation map - // flip-flops between that of fork #1 and fork #2 as anchor blocks are subsequently affirmed. - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(5, 2, 2, 25); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits_0) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None], - ); - - // no anchor blocks recorded, yet! - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - assert_eq!(heaviest_am, AffirmationMap::empty()); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_none() - ); - - update_pox_affirmation_maps(&mut burnchain_db, &headers, 0, &burnchain).unwrap(); - - // there's only one anchor block - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - - // the anchor block itself affirms nothing, since it isn't built on an anchor block - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=0: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("p").unwrap()); - - // build a second reward cycle off of the first, but at the start - assert!(commits_0[1][0].is_some()); - let (next_headers, commits_1) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[1][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 1, &burnchain).unwrap(); - - // there's two anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - - // the network affirms two anchor blocks, but the second one wins - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=1: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("a").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("ap").unwrap()); - - // build a third reward cycle off of the first - assert!(commits_0[4][0].clone().unwrap().block_height == 5); - let (next_headers, commits_2) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_0[4][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 2, &burnchain).unwrap(); - - // there's four anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_some() - ); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=2: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("pa").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("pap").unwrap()); - - // build a fourth reward cycle off of the second - assert!(commits_1[4][0].clone().unwrap().block_height == 10); - let (next_headers, commits_3) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![commits_1[4][0].clone()], - ); - update_pox_affirmation_maps(&mut burnchain_db, &headers, 3, &burnchain).unwrap(); - - // there's four anchor blocks - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 0) - .unwrap() - .is_none() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 1) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 2) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 3) - .unwrap() - .is_some() - ); - assert!( - BurnchainDB::get_canonical_anchor_block_commit(burnchain_db.conn(), &headers, 4) - .unwrap() - .is_some() - ); - - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - ) - .unwrap(); - let canonical_am = BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - &burnchain, - &headers, - |_, _| true, - ) - .unwrap(); - eprintln!( - "rc=3: heaviest = {}, canonical = {}", - &heaviest_am, &canonical_am - ); - - assert_eq!(heaviest_am, AffirmationMap::decode("apa").unwrap()); - assert_eq!(canonical_am, AffirmationMap::decode("apap").unwrap()); -} diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 88fc138a40..72a276c541 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -2438,10 +2438,11 @@ impl<'a> SortitionHandleConn<'a> { prepare_end_bhh: &BurnchainHeaderHash, pox_consts: &PoxConstants, ) -> Result, CoordinatorError> { - match burnchain_db_conn { - Some(conn) => self.get_chosen_pox_anchor_v210(conn, prepare_end_bhh, pox_consts), - None => self.get_chosen_pox_anchor_v205(prepare_end_bhh, pox_consts), - } + // match burnchain_db_conn { + // Some(conn) => self.get_chosen_pox_anchor_v210(conn, prepare_end_bhh, pox_consts), + // None => self.get_chosen_pox_anchor_v205(prepare_end_bhh, pox_consts), + // } + self.get_chosen_pox_anchor_v205(prepare_end_bhh, pox_consts) } /// Use the epoch 2.1 method for choosing a PoX anchor block. @@ -3772,28 +3773,6 @@ impl SortitionDB { } } -impl SortitionDBTx<'_> { - pub fn find_sortition_tip_affirmation_map( - &mut self, - chain_tip: &SortitionId, - ) -> Result { - let affirmation_map = match self.get_indexed(chain_tip, db_keys::pox_affirmation_map())? { - Some(am_str) => { - AffirmationMap::decode(&am_str).expect("FATAL: corrupt affirmation map") - } - None => AffirmationMap::empty(), - }; - - // remove the first entry -- it's always `n` based on the way we construct it, while the - // heaviest affirmation map just has nothing. - if let Some(skipped_first_entry) = affirmation_map.as_slice().get(1..) { - Ok(AffirmationMap::new(skipped_first_entry.to_vec())) - } else { - Ok(AffirmationMap::empty()) - } - } -} - impl SortitionDBConn<'_> { pub fn as_handle<'b>(&'b self, chain_tip: &SortitionId) -> SortitionHandleConn<'b> { SortitionHandleConn::new( @@ -4654,23 +4633,6 @@ impl SortitionDB { } } - /// Find the affirmation map represented by a given sortition ID. - pub fn find_sortition_tip_affirmation_map( - &self, - tip_id: &SortitionId, - ) -> Result { - let ih = self.index_handle(tip_id); - let am = ih.get_sortition_affirmation_map()?; - - // remove the first entry -- it's always `n` based on the way we construct it, while the - // heaviest affirmation map just has nothing. - if let Some(skipped_first_entry) = am.as_slice().get(1..) { - Ok(AffirmationMap::new(skipped_first_entry.to_vec())) - } else { - Ok(AffirmationMap::empty()) - } - } - /// Get the list of Stack-STX operations processed in a given burnchain block. /// This will be the same list in each PoX fork; it's up to the Stacks block-processing logic /// to reject them. @@ -10736,15 +10698,7 @@ pub mod tests { commit_set[0].clone() }; let burn_header_hash = headers[i + 1].block_hash.clone(); - let burn_block_height = headers[i + 1].block_height; - Burnchain::process_affirmation_maps( - &burnchain, - &mut burnchain_db, - &headers, - burn_block_height, - ) - .unwrap(); test_append_snapshot_with_winner(&mut db, burn_header_hash, &commit_ops, None, winner); } diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index 885a23f4af..e976e5694e 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -1626,16 +1626,12 @@ impl< "FAIL: processing a new Stacks block, but don't have a canonical sortition tip", ); - let burnchain_db_conn = self.burnchain_blocks_db.conn(); let sortdb_handle = self .sortition_db .tx_handle_begin(&canonical_sortition_tip)?; - let mut processed_blocks = self.chain_state_db.process_blocks( - burnchain_db_conn, - sortdb_handle, - 1, - self.dispatcher, - )?; + let mut processed_blocks = + self.chain_state_db + .process_blocks(sortdb_handle, 1, self.dispatcher)?; while let Some(block_result) = processed_blocks.pop() { if block_result.0.is_none() && block_result.1.is_none() { @@ -1738,12 +1734,9 @@ impl< .sortition_db .tx_handle_begin(&canonical_sortition_tip)?; // Right before a block is set to processed, the event dispatcher will emit a new block event - processed_blocks = self.chain_state_db.process_blocks( - burnchain_db_conn, - sortdb_handle, - 1, - self.dispatcher, - )?; + processed_blocks = + self.chain_state_db + .process_blocks(sortdb_handle, 1, self.dispatcher)?; } Ok(None) diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index b441f2214c..463253d2cf 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -41,7 +41,6 @@ use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::Hash160; use stacks_common::util::vrf::*; -use crate::burnchains::affirmation::*; use crate::burnchains::bitcoin::indexer::BitcoinIndexer; use crate::burnchains::db::*; use crate::burnchains::*; @@ -191,36 +190,10 @@ fn produce_burn_block_do_not_set_height<'a, I: Iterator( - burnchain: &Burnchain, - indexer: &B, - burnchain_db: &BurnchainDB, - chainstate: &StacksChainState, - ) -> Result { - BurnchainDB::get_canonical_affirmation_map( - burnchain_db.conn(), - burnchain, - indexer, - |anchor_block_commit, _anchor_block_metadata| { - // if we don't have an unaffirmed anchor block, and we're no longer in the initial block - // download, then assume that it's absent. Otherwise, if we are in the initial block - // download but we don't have it yet, assume that it's present. - StacksChainState::has_stacks_block_for(chainstate.db(), anchor_block_commit) - }, - ) - .map_err(|e| e.into()) - } - - /// Get the affirmation map represented by the Stacks chain tip. - /// This is the private interface, to avoid having a public function take two db connections of the - /// same type. - fn inner_find_stacks_tip_affirmation_map( - burnchain_conn: &DBConn, - sort_db_conn: &DBConn, - tip_ch: &ConsensusHash, - tip_bhh: &BlockHeaderHash, - ) -> Result { - if let Some(leader_block_commit) = - SortitionDB::get_block_commit_for_stacks_block(sort_db_conn, tip_ch, tip_bhh)? - { - if let Some(am_id) = - BurnchainDB::get_block_commit_affirmation_id(burnchain_conn, &leader_block_commit)? - { - if let Some(am) = BurnchainDB::get_affirmation_map(burnchain_conn, am_id)? { - debug!( - "Stacks tip {}/{} (txid {}) has affirmation map '{}'", - tip_ch, tip_bhh, &leader_block_commit.txid, &am - ); - return Ok(am); - } else { - debug!( - "Stacks tip {}/{} (txid {}) affirmation map ID {} has no corresponding map", - tip_ch, tip_bhh, &leader_block_commit.txid, am_id - ); - } - } else { - debug!( - "No affirmation map for stacks tip {}/{} (txid {})", - tip_ch, tip_bhh, &leader_block_commit.txid - ); - } - } else { - debug!("No block-commit for stacks tip {}/{}", tip_ch, tip_bhh); - } - - Ok(AffirmationMap::empty()) - } - - /// Get the affirmation map represented by the Stacks chain tip. - /// This uses the 2.1 rules exclusively (i.e. only block-commits are considered). - pub fn find_stacks_tip_affirmation_map( - burnchain_db: &BurnchainDB, - sort_db_conn: &DBConn, - tip_ch: &ConsensusHash, - tip_bhh: &BlockHeaderHash, - ) -> Result { - Self::inner_find_stacks_tip_affirmation_map( - burnchain_db.conn(), - sort_db_conn, - tip_ch, - tip_bhh, - ) - } - /// Delete a microblock's data from the DB fn delete_microblock_data( tx: &mut DBTx, @@ -5395,7 +5315,6 @@ impl StacksChainState { microblocks: &[StacksMicroblock], // parent microblocks burnchain_commit_burn: u64, burnchain_sortition_burn: u64, - affirmation_weight: u64, do_not_advance: bool, ) -> Result< ( @@ -5824,7 +5743,6 @@ impl StacksChainState { burn_transfer_stx_ops, burn_delegate_stx_ops, burn_vote_for_aggregate_key_ops, - affirmation_weight, ) .expect("FATAL: failed to advance chain tip"); @@ -6035,7 +5953,6 @@ impl StacksChainState { /// consumption by future miners). pub fn process_next_staging_block( &mut self, - burnchain_dbconn: &DBConn, sort_tx: &mut SortitionHandleTx, dispatcher_opt: Option<&T>, ) -> Result<(Option, Option), Error> { @@ -6223,24 +6140,6 @@ impl StacksChainState { last_microblock_seq ); - test_debug!( - "About to load affirmation map for {}/{}", - &next_staging_block.consensus_hash, - &next_staging_block.anchored_block_hash - ); - let block_am = StacksChainState::inner_find_stacks_tip_affirmation_map( - burnchain_dbconn, - sort_tx.tx(), - &next_staging_block.consensus_hash, - &next_staging_block.anchored_block_hash, - )?; - test_debug!( - "Affirmation map for {}/{} is `{}`", - &next_staging_block.consensus_hash, - &next_staging_block.anchored_block_hash, - &block_am - ); - // attach the block to the chain state and calculate the next chain tip. // Execute the confirmed microblocks' transactions against the chain state, and then // execute the anchored block's transactions against the chain state. @@ -6260,7 +6159,6 @@ impl StacksChainState { &next_microblocks, next_staging_block.commit_burn, next_staging_block.sortition_burn, - block_am.weight(), false, ) { Ok(next_chain_tip_info) => next_chain_tip_info, @@ -6417,13 +6315,12 @@ impl StacksChainState { #[cfg(test)] pub fn process_blocks_at_tip( &mut self, - burnchain_db_conn: &DBConn, sort_db: &mut SortitionDB, max_blocks: usize, ) -> Result, Option)>, Error> { let tx = sort_db.tx_begin_at_tip(); let null_event_dispatcher: Option<&DummyEventDispatcher> = None; - self.process_blocks(burnchain_db_conn, tx, max_blocks, null_event_dispatcher) + self.process_blocks(tx, max_blocks, null_event_dispatcher) } /// Process some staging blocks, up to max_blocks. @@ -6433,7 +6330,6 @@ impl StacksChainState { /// epoch receipt if the block was invalid. pub fn process_blocks( &mut self, - burnchain_db_conn: &DBConn, mut sort_tx: SortitionHandleTx, max_blocks: usize, dispatcher_opt: Option<&T>, @@ -6466,7 +6362,7 @@ impl StacksChainState { for i in 0..max_blocks { // process up to max_blocks pending blocks - match self.process_next_staging_block(burnchain_db_conn, &mut sort_tx, dispatcher_opt) { + match self.process_next_staging_block(&mut sort_tx, dispatcher_opt) { Ok((next_tip_opt, next_microblock_poison_opt)) => match next_tip_opt { Some(next_tip) => { ret.push((Some(next_tip), next_microblock_poison_opt)); diff --git a/stackslib/src/chainstate/stacks/db/headers.rs b/stackslib/src/chainstate/stacks/db/headers.rs index d968dd6343..60fd1af3c5 100644 --- a/stackslib/src/chainstate/stacks/db/headers.rs +++ b/stackslib/src/chainstate/stacks/db/headers.rs @@ -23,8 +23,8 @@ use crate::chainstate::stacks::db::*; use crate::chainstate::stacks::{Error, *}; use crate::core::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; use crate::util_lib::db::{ - query_row, query_row_columns, query_row_panic, query_rows, u64_to_sql, DBConn, - Error as db_error, FromColumn, FromRow, + query_row, query_row_columns, query_row_panic, u64_to_sql, DBConn, Error as db_error, + FromColumn, FromRow, }; impl FromRow for StacksBlockHeader { @@ -105,7 +105,6 @@ impl StacksChainState { parent_id: &StacksBlockId, tip_info: &StacksHeaderInfo, anchored_block_cost: &ExecutionCost, - affirmation_weight: u64, ) -> Result<(), Error> { let StacksBlockHeaderTypes::Epoch2(header) = &tip_info.anchored_header else { return Err(Error::InvalidChildOfNakomotoBlock); @@ -154,7 +153,7 @@ impl StacksChainState { anchored_block_cost, block_size_str, parent_id, - u64_to_sql(affirmation_weight)?, + u64_to_sql(0)?, // TODO: remove this ]; tx.execute("INSERT INTO block_headers \ @@ -368,18 +367,6 @@ impl StacksChainState { Ok(ret) } - /// Get all headers at a given Stacks height - pub fn get_all_headers_at_height_and_weight( - conn: &Connection, - height: u64, - affirmation_weight: u64, - ) -> Result, Error> { - let qry = - "SELECT * FROM block_headers WHERE block_height = ?1 AND affirmation_weight = ?2 ORDER BY burn_header_height DESC"; - let args = params![u64_to_sql(height)?, u64_to_sql(affirmation_weight)?]; - query_rows(conn, qry, args).map_err(|e| e.into()) - } - /// Get the highest known header height pub fn get_max_header_height(conn: &Connection) -> Result { let qry = "SELECT block_height FROM block_headers ORDER BY block_height DESC LIMIT 1"; @@ -387,16 +374,4 @@ impl StacksChainState { .map(|row_opt: Option| row_opt.map(|h| h as u64).unwrap_or(0)) .map_err(|e| e.into()) } - - /// Get the highest known header affirmation weight - pub fn get_max_affirmation_weight_at_height( - conn: &Connection, - height: u64, - ) -> Result { - let qry = - "SELECT affirmation_weight FROM block_headers WHERE block_height = ?1 ORDER BY affirmation_weight DESC LIMIT 1"; - query_row(conn, qry, &[&u64_to_sql(height)?]) - .map(|row_opt: Option| row_opt.map(|h| h as u64).unwrap_or(0)) - .map_err(|e| e.into()) - } } diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index b34164a924..b4e5d18dc4 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -1717,7 +1717,6 @@ impl StacksChainState { &parent_hash, &first_tip_info, &ExecutionCost::ZERO, - 0, )?; tx.commit()?; } @@ -2603,7 +2602,6 @@ impl StacksChainState { burn_transfer_stx_ops: Vec, burn_delegate_stx_ops: Vec, burn_vote_for_aggregate_key_ops: Vec, - affirmation_weight: u64, ) -> Result { if new_tip.parent_block != FIRST_STACKS_BLOCK_HASH { // not the first-ever block, so linkage must occur @@ -2659,7 +2657,6 @@ impl StacksChainState { &parent_hash, &new_tip_info, anchor_block_cost, - affirmation_weight, )?; StacksChainState::insert_miner_payment_schedule(headers_tx.deref_mut(), block_reward)?; StacksChainState::store_burnchain_txids( diff --git a/stackslib/src/chainstate/stacks/tests/chain_histories.rs b/stackslib/src/chainstate/stacks/tests/chain_histories.rs index 6a4f48e786..af00ebcff3 100644 --- a/stackslib/src/chainstate/stacks/tests/chain_histories.rs +++ b/stackslib/src/chainstate/stacks/tests/chain_histories.rs @@ -177,11 +177,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 1, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 1) .unwrap(); let expect_success = check_oracle(&stacks_block, µblocks); @@ -361,11 +357,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 1, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 1) .unwrap(); // processed _this_ block @@ -571,11 +563,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); // processed exactly one block, but got back two tip-infos @@ -903,11 +891,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); // processed _one_ block @@ -1169,19 +1153,11 @@ where ); let mut tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); let mut tip_info_list_2 = node_2 .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); tip_info_list.append(&mut tip_info_list_2); @@ -1268,19 +1244,11 @@ where ); let _ = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); let _ = node_2 .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); } @@ -1510,11 +1478,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); // processed _one_ block @@ -1763,11 +1727,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); // processed all stacks blocks -- one on each burn chain fork @@ -2065,11 +2025,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); // processed _one_ block @@ -2318,11 +2274,7 @@ where ); let tip_info_list = node .chainstate - .process_blocks_at_tip( - connect_burnchain_db(&burn_node.burnchain).conn(), - &mut burn_node.sortdb, - 2, - ) + .process_blocks_at_tip(&mut burn_node.sortdb, 2) .unwrap(); // processed all stacks blocks -- one on each burn chain fork @@ -2630,7 +2582,6 @@ fn miner_trace_replay_randomized(miner_trace: &mut TestMinerTrace) { let tip_info_list = node .chainstate .process_blocks_at_tip( - connect_burnchain_db(&miner_trace.burn_node.burnchain).conn(), &mut miner_trace.burn_node.sortdb, expected_num_blocks, ) @@ -2658,8 +2609,6 @@ fn miner_trace_replay_randomized(miner_trace: &mut TestMinerTrace) { let tip_info_list = node .chainstate .process_blocks_at_tip( - connect_burnchain_db(&miner_trace.burn_node.burnchain) - .conn(), &mut miner_trace.burn_node.sortdb, expected_num_blocks, ) diff --git a/stackslib/src/cli.rs b/stackslib/src/cli.rs index 2d59f2d1de..27920dfb45 100644 --- a/stackslib/src/cli.rs +++ b/stackslib/src/cli.rs @@ -30,7 +30,6 @@ use stacks_common::types::sqlite::NO_PARAMS; use stacks_common::util::hash::Hash160; use stacks_common::util::vrf::VRFProof; -use crate::burnchains::db::BurnchainDB; use crate::burnchains::Burnchain; use crate::chainstate::burn::db::sortdb::{ get_ancestor_sort_id, SortitionDB, SortitionHandleContext, @@ -553,8 +552,6 @@ fn replay_staging_block(db_path: &str, index_block_hash_hex: &str, conf: Option< let block_id = StacksBlockId::from_hex(index_block_hash_hex).unwrap(); let chain_state_path = format!("{db_path}/chainstate/"); let sort_db_path = format!("{db_path}/burnchain/sortition"); - let burn_db_path = format!("{db_path}/burnchain/burnchain.sqlite"); - let burnchain_blocks_db = BurnchainDB::open(&burn_db_path, false).unwrap(); let conf = conf.unwrap_or(&DEFAULT_MAINNET_CONFIG); @@ -613,7 +610,6 @@ fn replay_staging_block(db_path: &str, index_block_hash_hex: &str, conf: Option< sort_tx, chainstate_tx, clarity_instance, - &burnchain_blocks_db, &parent_header_info, &next_staging_block.parent_microblock_hash, next_staging_block.parent_microblock_seq, @@ -631,8 +627,6 @@ fn replay_staging_block(db_path: &str, index_block_hash_hex: &str, conf: Option< fn replay_mock_mined_block(db_path: &str, block: AssembledAnchorBlock, conf: Option<&Config>) { let chain_state_path = format!("{db_path}/chainstate/"); let sort_db_path = format!("{db_path}/burnchain/sortition"); - let burn_db_path = format!("{db_path}/burnchain/burnchain.sqlite"); - let burnchain_blocks_db = BurnchainDB::open(&burn_db_path, false).unwrap(); let conf = conf.unwrap_or(&DEFAULT_MAINNET_CONFIG); @@ -687,7 +681,6 @@ fn replay_mock_mined_block(db_path: &str, block: AssembledAnchorBlock, conf: Opt sort_tx, chainstate_tx, clarity_instance, - &burnchain_blocks_db, &parent_header_info, &block.anchored_block.header.parent_microblock, block.anchored_block.header.parent_microblock_sequence, @@ -707,7 +700,6 @@ fn replay_block( mut sort_tx: IndexDBTx, mut chainstate_tx: ChainstateTx, clarity_instance: &mut ClarityInstance, - burnchain_blocks_db: &BurnchainDB, parent_header_info: &StacksHeaderInfo, parent_microblock_hash: &BlockHeaderHash, parent_microblock_seq: u16, @@ -799,14 +791,6 @@ fn replay_block( assert_eq!(*parent_microblock_hash, last_microblock_hash); assert_eq!(parent_microblock_seq, last_microblock_seq); - let block_am = StacksChainState::find_stacks_tip_affirmation_map( - burnchain_blocks_db, - sort_tx.tx(), - block_consensus_hash, - block_hash, - ) - .unwrap(); - let pox_constants = sort_tx.context.pox_constants.clone(); match StacksChainState::append_block( @@ -824,7 +808,6 @@ fn replay_block( &next_microblocks, block_commit_burn, block_sortition_burn, - block_am.weight(), true, ) { Ok((receipt, _, _)) => { diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index 43d3772454..7578e5ad32 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -1707,12 +1707,6 @@ pub const EPOCH_CONFIG_3_0_0: &str = "3.0"; pub const EPOCH_CONFIG_3_1_0: &str = "3.1"; pub const EPOCH_CONFIG_3_2_0: &str = "3.2"; -#[derive(Clone, Deserialize, Default, Debug)] -pub struct AffirmationOverride { - pub reward_cycle: u64, - pub affirmation: String, -} - #[derive(Clone, Deserialize, Default, Debug)] #[serde(deny_unknown_fields)] pub struct BurnchainConfigFile { diff --git a/stackslib/src/core/mod.rs b/stackslib/src/core/mod.rs index 6c8fbe2e46..974d335c5f 100644 --- a/stackslib/src/core/mod.rs +++ b/stackslib/src/core/mod.rs @@ -123,11 +123,6 @@ pub const BITCOIN_TESTNET_STACKS_30_BURN_HEIGHT: u64 = 30_000_000; pub const BITCOIN_TESTNET_STACKS_31_BURN_HEIGHT: u64 = 30_000_001; pub const BITCOIN_TESTNET_STACKS_32_BURN_HEIGHT: u64 = 30_000_002; -/// This constant sets the approximate testnet bitcoin height at which 2.5 Xenon -/// was reorged back to 2.5 instantiation. This is only used to calculate the -/// expected affirmation maps (so it only must be accurate to the reward cycle). -pub const BITCOIN_TESTNET_STACKS_25_REORGED_HEIGHT: u64 = 2_586_000; - pub const BITCOIN_REGTEST_FIRST_BLOCK_HEIGHT: u64 = 0; pub const BITCOIN_REGTEST_FIRST_BLOCK_TIMESTAMP: u32 = 0; pub const BITCOIN_REGTEST_FIRST_BLOCK_HASH: &str = diff --git a/stackslib/src/core/tests/mod.rs b/stackslib/src/core/tests/mod.rs index 5599d3fb5c..3bb28f66c0 100644 --- a/stackslib/src/core/tests/mod.rs +++ b/stackslib/src/core/tests/mod.rs @@ -144,7 +144,6 @@ pub fn make_block( &new_index_hash, &new_tip_info, &ExecutionCost::ZERO, - block_height, ) .unwrap(); diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 031b7d4339..7d63a3aab7 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -1569,12 +1569,7 @@ check if the associated microblocks can be downloaded let sortition_tx = new_sortition_db.tx_handle_begin(&sortition_tip).unwrap(); let null_event_dispatcher: Option<&DummyEventDispatcher> = None; let receipts = new_chainstate - .process_blocks( - old_burnchaindb.conn(), - sortition_tx, - 1, - null_event_dispatcher, - ) + .process_blocks(sortition_tx, 1, null_event_dispatcher) .unwrap(); if receipts.is_empty() { break; diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index ce14adc303..f4a012604a 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -1827,14 +1827,16 @@ impl PeerNetwork { .block_height_to_reward_cycle(stacks_tip_burn_block_height) .unwrap_or(0); + let inv_rescan_rc = stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles); // Always want to do the last reward cycle AND the current reward cycle (we could be still looking for the current reward cycles anchor block which is mined in the prior reward cycle) let prior_rc = stacks_tip_rc.saturating_sub(1); - let rescan_rc = std::cmp::min( - stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles), - prior_rc, - ); + let rescan_rc = std::cmp::min(inv_rescan_rc, prior_rc); - test_debug!("begin blocks inv scan at {rescan_rc}"); + test_debug!("begin blocks inv scan at {rescan_rc}"; + "stacks_tip_rc" => stacks_tip_rc, + "prior_rc" => prior_rc, + "inv_rescan_rc" => inv_rescan_rc, + ); rescan_rc } @@ -1854,7 +1856,6 @@ impl PeerNetwork { None => { // proceed to block scan let scan_start_rc = self.get_block_scan_start(sortdb); - debug!("{:?}: cannot make any more GetPoxInv requests for {:?}; proceeding to block inventory scan at reward cycle {}", &self.local_peer, nk, scan_start_rc); stats.reset_block_scan(scan_start_rc); return Ok(()); diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 58bcce35bf..e6c2e022f9 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -3761,14 +3761,6 @@ pub mod test { blockstack_ops, ) .unwrap(); - - Burnchain::process_affirmation_maps( - burnchain, - &mut burnchain_db, - &indexer, - block_header.block_height, - ) - .unwrap(); } /// Generate and commit the next burnchain block with the given block operations. From 8d227154bd1696ce8aa0b1ac6c5bdb9ff843516f Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 25 Aug 2025 14:55:06 -0700 Subject: [PATCH 26/35] WIP: remove insert_block_commit_affirmation_map and update_block_commit_affirmation Signed-off-by: Jacinta Ferrant --- stackslib/src/burnchains/db.rs | 502 +----------------- stackslib/src/burnchains/tests/affirmation.rs | 2 +- stackslib/src/burnchains/tests/db.rs | 304 +---------- stackslib/src/chainstate/burn/db/sortdb.rs | 48 -- stackslib/src/chainstate/coordinator/tests.rs | 314 ----------- 5 files changed, 6 insertions(+), 1164 deletions(-) diff --git a/stackslib/src/burnchains/db.rs b/stackslib/src/burnchains/db.rs index bc960963bc..015e9f1f69 100644 --- a/stackslib/src/burnchains/db.rs +++ b/stackslib/src/burnchains/db.rs @@ -339,50 +339,6 @@ impl BurnchainDBTransaction<'_> { Ok(()) } - /// Add an affirmation map into the database. Returns the affirmation map ID. - pub fn insert_block_commit_affirmation_map( - &self, - affirmation_map: &AffirmationMap, - ) -> Result { - let weight = affirmation_map.weight(); - let sql = "INSERT INTO affirmation_maps (affirmation_map,weight) VALUES (?1,?2)"; - let args = params![affirmation_map.encode(), u64_to_sql(weight)?]; - match self.sql_tx.execute(sql, args) { - Ok(_) => { - let am_id = BurnchainDB::get_affirmation_map_id(&self.sql_tx, affirmation_map)? - .expect("BUG: no affirmation ID for affirmation map we just inserted"); - Ok(am_id) - } - Err(e) => Err(DBError::SqliteError(e)), - } - } - - /// Update a block-commit's affirmation state -- namely, record the reward cycle that this - /// block-commit affirms, if any (anchor_block_descendant), and record the affirmation map ID - /// for this block-commit (affirmation_id). - pub fn update_block_commit_affirmation( - &self, - block_commit: &LeaderBlockCommitOp, - anchor_block_descendant: Option, - affirmation_id: u64, - ) -> Result<(), DBError> { - let sql = "UPDATE block_commit_metadata SET affirmation_id = ?1, anchor_block_descendant = ?2 WHERE burn_block_hash = ?3 AND txid = ?4"; - let args = params![ - u64_to_sql(affirmation_id)?, - opt_u64_to_sql(anchor_block_descendant)?, - block_commit.burn_header_hash, - block_commit.txid, - ]; - match self.sql_tx.execute(sql, args) { - Ok(_) => { - test_debug!("Set affirmation map ID of {} - {},{},{} (parent {},{}) to {} (anchor block descendant? {:?})", - &block_commit.burn_header_hash, &block_commit.txid, block_commit.block_height, block_commit.vtxindex, block_commit.parent_block_ptr, block_commit.parent_vtxindex, affirmation_id, &anchor_block_descendant); - Ok(()) - } - Err(e) => Err(DBError::SqliteError(e)), - } - } - /// Mark a block-commit as being the anchor block commit for a particular reward cycle. pub fn set_anchor_block( &self, @@ -427,451 +383,6 @@ impl BurnchainDBTransaction<'_> { .map_err(DBError::SqliteError) } - /// Calculate a burnchain block's block-commits' descendancy information. - /// Only fails on DB errors. - pub fn update_block_descendancy( - &self, - indexer: &B, - hdr: &BurnchainBlockHeader, - burnchain: &Burnchain, - ) -> Result<(), BurnchainError> { - // find all block-commits for this block - let commits: Vec = { - let block_ops_qry = - "SELECT DISTINCT * FROM burnchain_db_block_ops WHERE block_hash = ?"; - let block_ops = query_rows(&self.sql_tx, block_ops_qry, &[&hdr.block_hash])?; - block_ops - .into_iter() - .filter_map(|op| { - if let BlockstackOperationType::LeaderBlockCommit(opdata) = op { - Some(opdata) - } else { - None - } - }) - .collect() - }; - if commits.is_empty() { - test_debug!("No block-commits for block {}", hdr.block_height); - return Ok(()); - } - - // for each commit[i], find its parent commit - let mut parent_commits = vec![]; - for commit in commits.iter() { - let parent_commit_opt = if commit.parent_block_ptr != 0 || commit.parent_vtxindex != 0 { - // parent is not genesis - BurnchainDB::get_commit_at( - &self.sql_tx, - indexer, - commit.parent_block_ptr, - commit.parent_vtxindex, - )? - } else { - // parent is genesis - test_debug!( - "Parent block-commit of {},{},{} is the genesis commit", - &commit.txid, - commit.block_height, - commit.vtxindex - ); - None - }; - - parent_commits.push(parent_commit_opt); - } - assert_eq!(parent_commits.len(), commits.len()); - - // for each parent block-commit and block-commit, calculate the block-commit's new - // affirmation map - for (parent_commit_opt, commit) in parent_commits.iter().zip(commits.iter()) { - if let Some(parent_commit) = parent_commit_opt.as_ref() { - if get_parent_child_reward_cycles(parent_commit, commit, burnchain).is_some() { - // we have enough info to calculate this commit's affirmation - self.make_reward_phase_affirmation_map(burnchain, commit, parent_commit)?; - } else { - // parent is invalid - test_debug!( - "No block-commit parent reward cycle found for {},{},{}", - &commit.txid, - commit.block_height, - commit.vtxindex - ); - self.update_block_commit_affirmation(commit, None, 0)?; - } - } else { - if commit.parent_block_ptr == 0 && commit.parent_vtxindex == 0 { - test_debug!( - "Block-commit parent of {},{},{} is genesis", - &commit.txid, - commit.block_height, - commit.vtxindex - ); - } else { - // this is an invalid commit -- no parent found - test_debug!( - "No block-commit parent {},{} found for {},{},{} (in {})", - commit.parent_block_ptr, - commit.parent_vtxindex, - &commit.txid, - commit.block_height, - commit.vtxindex, - &commit.burn_header_hash - ); - } - self.update_block_commit_affirmation(commit, None, 0)?; - } - } - - Ok(()) - } - - /// Create a prepare-phase affirmation map. This is only done at the very end of a reward - /// cycle, once the anchor block is chosen and a new reward cycle is about to begin. This - /// method updates the prepare-phase block-commit's affirmation map to reflect what its miner - /// believes to be the state of all anchor blocks, _including_ this new reward cycle's anchor - /// block. - /// Returns the ID of the affirmation map in the database on success. - /// This can be used to later look up the affirmation map. - pub fn make_prepare_phase_affirmation_map( - &self, - indexer: &B, - burnchain: &Burnchain, - reward_cycle: u64, - block_commit: &LeaderBlockCommitOp, - anchor_block: Option<&LeaderBlockCommitOp>, - descends_from_anchor_block: bool, - ) -> Result { - test_debug!( - "Make affirmation map for {},{},{} (parent {},{}) in reward cycle {}", - &block_commit.txid, - block_commit.block_height, - block_commit.vtxindex, - block_commit.parent_block_ptr, - block_commit.parent_vtxindex, - reward_cycle - ); - - let parent = match BurnchainDB::get_commit_at( - &self.sql_tx, - indexer, - block_commit.parent_block_ptr, - block_commit.parent_vtxindex, - )? { - Some(p) => p, - None => { - if block_commit.parent_block_ptr == 0 && block_commit.vtxindex == 0 { - debug!( - "Prepare-phase commit {},{},{} builds off of genesis", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex - ); - } else { - debug!( - "Prepare-phase commit {},{},{} has no parent, so must be invalid", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex - ); - } - return Ok(0); - } - }; - - let parent_metadata = - BurnchainDB::get_commit_metadata(&self.sql_tx, &parent.burn_header_hash, &parent.txid)? - .unwrap_or_else(|| { - panic!( - "BUG: no metadata found for parent block-commit {},{},{} in {}", - parent.block_height, - parent.vtxindex, - &parent.txid, - &parent.burn_header_hash - ) - }); - - let (am, affirmed_reward_cycle) = if anchor_block.is_some() && descends_from_anchor_block { - // this block-commit assumes the affirmation map of the anchor block as a prefix of its - // own affirmation map. - let anchor_block = anchor_block.unwrap(); - let anchor_am_id = - BurnchainDB::get_block_commit_affirmation_id(&self.sql_tx, anchor_block)? - .expect("BUG: anchor block has no affirmation map"); - - let mut am = BurnchainDB::get_affirmation_map(&self.sql_tx, anchor_am_id)? - .ok_or(BurnchainError::DBError(DBError::NotFoundError))?; - - test_debug!("Prepare-phase commit {},{},{} descends from anchor block {},{},{} for reward cycle {}", - &block_commit.block_header_hash, block_commit.block_height, block_commit.vtxindex, &anchor_block.block_header_hash, anchor_block.block_height, anchor_block.vtxindex, reward_cycle); - - let num_affirmed = am.len() as u64; - for rc in (num_affirmed + 1)..reward_cycle { - // it's possible that this anchor block is more than one reward cycle back; if so, - // then back-fill all of the affirmations made between then and now. - if BurnchainDB::has_anchor_block(&self.sql_tx, rc)? { - test_debug!( - "Commit {},{},{} skips reward cycle {} with anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::PoxAnchorBlockAbsent); - } else { - // affirmation weight increases even if there's no decision made, because - // the lack of a decision is still an affirmation of all prior decisions - test_debug!( - "Commit {},{},{} skips reward cycle {} without anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::Nothing); - } - } - - am.push(AffirmationMapEntry::PoxAnchorBlockPresent); - (am, Some(reward_cycle)) - } else { - // this block-commit assumes the affirmation map of its parent as a prefix of its own - // affirmation map. - let (parent_reward_cycle, _) = - get_parent_child_reward_cycles(&parent, block_commit, burnchain) - .ok_or(BurnchainError::DBError(DBError::NotFoundError))?; - - // load up the affirmation map for the last anchor block the parent affirmed - let (mut am, parent_rc_opt) = match parent_metadata.anchor_block_descendant { - Some(parent_ab_rc) => { - // parent affirmed some past anchor block - let ab_metadata = BurnchainDB::get_canonical_anchor_block_commit_metadata(&self.sql_tx, indexer, parent_ab_rc)? - .unwrap_or_else(|| panic!("BUG: parent descends from a reward cycle with an anchor block ({}), but no anchor block found", parent_ab_rc)); - - let mut am = - BurnchainDB::get_affirmation_map(&self.sql_tx, ab_metadata.affirmation_id)? - .expect("BUG: no affirmation map for parent commit's anchor block"); - - test_debug!("Prepare-phase commit {},{},{} does nothing for reward cycle {}, but it builds on its parent which affirms anchor block for reward cycle {} ({}) (affirms? {})", - &block_commit.block_header_hash, block_commit.block_height, block_commit.vtxindex, reward_cycle, parent_ab_rc, &am, (am.len() as u64) < parent_ab_rc); - - if (am.len() as u64) < parent_ab_rc { - // child is affirming the parent - am.push(AffirmationMapEntry::PoxAnchorBlockPresent); - } - - (am, Some(parent_ab_rc)) - } - None => { - let mut parent_am = BurnchainDB::get_affirmation_map( - &self.sql_tx, - parent_metadata.affirmation_id, - )? - .expect("BUG: no affirmation map for parent commit"); - - // parent affirms no anchor blocks - test_debug!("Prepare-phase commit {},{},{} does nothing for reward cycle {}, and it builds on a parent {},{} {} which affirms no anchor block (affirms? {})", - &block_commit.block_header_hash, block_commit.block_height, block_commit.vtxindex, reward_cycle, block_commit.parent_block_ptr, block_commit.parent_vtxindex, &parent_am, (parent_am.len() as u64) < parent_reward_cycle); - - if (parent_am.len() as u64) < parent_reward_cycle { - // child is affirming the parent - parent_am.push(AffirmationMapEntry::Nothing); - } - - (parent_am, None) - } - }; - - let num_affirmed = am.len() as u64; - for rc in (num_affirmed + 1)..(reward_cycle + 1) { - if BurnchainDB::has_anchor_block(&self.sql_tx, rc)? { - test_debug!( - "Commit {},{},{} skips reward cycle {} with anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::PoxAnchorBlockAbsent); - } else { - // affirmation weight increases even if there's no decision made, because - // the lack of a decision is still an affirmation of all prior decisions - test_debug!( - "Commit {},{},{} skips reward cycle {} without anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::Nothing); - } - } - - debug!( - "Prepare-phase commit {},{} affirms parent {},{} with {} descended from {:?}", - block_commit.block_height, - block_commit.vtxindex, - parent.block_height, - parent.vtxindex, - &am, - &parent_metadata.anchor_block_descendant - ); - - (am, parent_rc_opt) - }; - - if let Some(am_id) = BurnchainDB::get_affirmation_map_id(&self.sql_tx, &am)? { - // child doesn't represent any new affirmations by the network, since its - // affirmation map already exists. - if cfg!(test) { - let _am_weight = BurnchainDB::get_affirmation_weight(&self.sql_tx, am_id)? - .unwrap_or_else(|| panic!("BUG: no affirmation map {}", &am_id)); - - test_debug!("Affirmation map of prepare-phase block-commit {},{},{} (parent {},{}) is old: {:?} weight {} affirmed {:?}", - &block_commit.txid, block_commit.block_height, block_commit.vtxindex, block_commit.parent_block_ptr, block_commit.parent_vtxindex, &am, _am_weight, &affirmed_reward_cycle); - } - - self.update_block_commit_affirmation(block_commit, affirmed_reward_cycle, am_id)?; - Ok(am_id) - } else { - test_debug!("Affirmation map of prepare-phase block-commit {},{},{} (parent {},{}) is new: {:?} weight {} affirmed {:?}", - &block_commit.txid, block_commit.block_height, block_commit.vtxindex, block_commit.parent_block_ptr, block_commit.parent_vtxindex, &am, am.weight(), &affirmed_reward_cycle); - - let am_id = self.insert_block_commit_affirmation_map(&am)?; - self.update_block_commit_affirmation(block_commit, affirmed_reward_cycle, am_id)?; - Ok(am_id) - } - } - - /// Make an affirmation map for a block commit in a reward phase (or an in-progress prepare - /// phase). This is done once per Bitcoin block, as block-commits are stored. Affirmation - /// maps for prepare-phase commits will be recomputed once the reward cycle finishes. - fn make_reward_phase_affirmation_map( - &self, - burnchain: &Burnchain, - block_commit: &LeaderBlockCommitOp, - parent: &LeaderBlockCommitOp, - ) -> Result { - assert_eq!(block_commit.parent_block_ptr as u64, parent.block_height); - assert_eq!(block_commit.parent_vtxindex as u32, parent.vtxindex); - - let parent_metadata = - BurnchainDB::get_commit_metadata(&self.sql_tx, &parent.burn_header_hash, &parent.txid)? - .unwrap_or_else(|| { - panic!( - "BUG: no metadata found for existing block commit {},{},{} in {}", - parent.block_height, - parent.vtxindex, - &parent.txid, - &parent.burn_header_hash - ) - }); - - test_debug!( - "Reward-phase commit {},{},{} has parent {},{}, anchor block {:?}", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - parent.block_height, - parent.vtxindex, - &parent_metadata.anchor_block_descendant - ); - - let child_reward_cycle = burnchain - .block_height_to_reward_cycle(block_commit.block_height) - .expect("BUG: block commit exists before first block height"); - - let (am, affirmed_anchor_block_reward_cycle) = - if let Some(parent_ab_rc) = parent_metadata.anchor_block_descendant { - let am_id = parent_metadata.affirmation_id; - let mut am = BurnchainDB::get_affirmation_map(&self.sql_tx, am_id)? - .expect("BUG: no affirmation map for parent commit"); - - test_debug!("Affirmation map of parent is {}", &am); - - let start_rc = am.len() as u64; - for rc in (start_rc + 1)..(child_reward_cycle + 1) { - if BurnchainDB::has_anchor_block(&self.sql_tx, rc)? { - test_debug!( - "Commit {},{},{} skips reward cycle {} with anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::PoxAnchorBlockAbsent); - } else { - test_debug!( - "Commit {},{},{} skips reward cycle {} without anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::Nothing); - } - } - - (am, Some(parent_ab_rc)) - } else { - let mut am = AffirmationMap::empty(); - for rc in 1..(child_reward_cycle + 1) { - if BurnchainDB::has_anchor_block(&self.sql_tx, rc)? { - test_debug!( - "Commit {},{},{} skips reward cycle {} with anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::PoxAnchorBlockAbsent); - } else { - test_debug!( - "Commit {},{},{} skips reward cycle {} without anchor block", - &block_commit.block_header_hash, - block_commit.block_height, - block_commit.vtxindex, - rc - ); - am.push(AffirmationMapEntry::Nothing); - } - } - (am, None) - }; - - if let Some(am_id) = BurnchainDB::get_affirmation_map_id(&self.sql_tx, &am)? { - // child doesn't represent any new affirmations by the network, since its - // affirmation map already exists. - if cfg!(test) { - let _am_weight = BurnchainDB::get_affirmation_weight(&self.sql_tx, am_id)? - .unwrap_or_else(|| panic!("BUG: no affirmation map {}", &am_id)); - - test_debug!("Affirmation map of reward-phase block-commit {},{},{} (parent {},{}) is old: {:?} weight {}", - &block_commit.txid, block_commit.block_height, block_commit.vtxindex, block_commit.parent_block_ptr, block_commit.parent_vtxindex, &am, _am_weight); - } - - self.update_block_commit_affirmation( - block_commit, - affirmed_anchor_block_reward_cycle, - am_id, - )?; - Ok(am_id) - } else { - test_debug!("Affirmation map of reward-phase block-commit {},{},{} (parent {},{}) is new: {:?} weight {}", - &block_commit.txid, block_commit.block_height, block_commit.vtxindex, block_commit.parent_block_ptr, block_commit.parent_vtxindex, &am, am.weight()); - - let am_id = self.insert_block_commit_affirmation_map(&am)?; - - self.update_block_commit_affirmation( - block_commit, - affirmed_anchor_block_reward_cycle, - am_id, - )?; - - Ok(am_id) - } - } - fn insert_block_commit_metadata(&self, bcm: BlockCommitMetadata) -> Result<(), BurnchainError> { let commit_metadata_sql = "INSERT OR REPLACE INTO block_commit_metadata (burn_block_hash, txid, block_height, vtxindex, anchor_block, anchor_block_descendant, affirmation_id) @@ -890,10 +401,8 @@ impl BurnchainDBTransaction<'_> { Ok(()) } - pub(crate) fn store_blockstack_ops( + pub(crate) fn store_blockstack_ops( &self, - burnchain: &Burnchain, - indexer: &B, block_header: &BurnchainBlockHeader, block_ops: &[BlockstackOperationType], ) -> Result<(), BurnchainError> { @@ -930,7 +439,6 @@ impl BurnchainDBTransaction<'_> { } } - self.update_block_descendancy(indexer, block_header, burnchain)?; Ok(()) } @@ -1400,10 +908,8 @@ impl BurnchainDB { // do NOT call directly; only call directly in tests. // This is only `pub` because the tests for it live in a different file. - pub fn store_new_burnchain_block_ops_unchecked( + pub fn store_new_burnchain_block_ops_unchecked( &mut self, - burnchain: &Burnchain, - indexer: &B, block_header: &BurnchainBlockHeader, blockstack_ops: &[BlockstackOperationType], ) -> Result<(), BurnchainError> { @@ -1416,7 +922,7 @@ impl BurnchainDB { blockstack_ops.len() ); db_tx.store_burnchain_db_entry(block_header)?; - db_tx.store_blockstack_ops(burnchain, indexer, block_header, blockstack_ops)?; + db_tx.store_blockstack_ops(block_header, blockstack_ops)?; db_tx.commit()?; Ok(()) @@ -1440,7 +946,7 @@ impl BurnchainDB { self.get_blockstack_transactions(burnchain, indexer, block, &header, epoch_id); apply_blockstack_txs_safety_checks(header.block_height, &mut blockstack_ops); - self.store_new_burnchain_block_ops_unchecked(burnchain, indexer, &header, &blockstack_ops)?; + self.store_new_burnchain_block_ops_unchecked(&header, &blockstack_ops)?; Ok(blockstack_ops) } diff --git a/stackslib/src/burnchains/tests/affirmation.rs b/stackslib/src/burnchains/tests/affirmation.rs index 3bb67184ce..393bf2e2e9 100644 --- a/stackslib/src/burnchains/tests/affirmation.rs +++ b/stackslib/src/burnchains/tests/affirmation.rs @@ -392,7 +392,7 @@ pub fn make_reward_cycle_with_vote( }; burnchain_db - .store_new_burnchain_block_ops_unchecked(burnchain, headers, &block_header, &ops) + .store_new_burnchain_block_ops_unchecked(&block_header, &ops) .unwrap(); headers.push(block_header.clone()); diff --git a/stackslib/src/burnchains/tests/db.rs b/stackslib/src/burnchains/tests/db.rs index 424abadd9a..2686a08967 100644 --- a/stackslib/src/burnchains/tests/db.rs +++ b/stackslib/src/burnchains/tests/db.rs @@ -24,7 +24,6 @@ use stacks_common::types::sqlite::NO_PARAMS; use stacks_common::util::hash::*; use super::*; -use crate::burnchains::affirmation::AffirmationMap; use crate::burnchains::bitcoin::address::*; use crate::burnchains::bitcoin::blocks::*; use crate::burnchains::bitcoin::*; @@ -82,7 +81,7 @@ impl BurnchainDB { ); db_tx.store_burnchain_db_entry(&header)?; - db_tx.store_blockstack_ops(burnchain, indexer, &header, &blockstack_ops)?; + db_tx.store_blockstack_ops(&header, &blockstack_ops)?; db_tx.commit()?; @@ -626,8 +625,6 @@ fn test_get_commit_at() { ); burnchain_db .store_new_burnchain_block_ops_unchecked( - &burnchain, - &headers, block_header, &vec![BlockstackOperationType::LeaderBlockCommit(cmt.clone())], ) @@ -669,8 +666,6 @@ fn test_get_commit_at() { burnchain_db .store_new_burnchain_block_ops_unchecked( - &burnchain, - &fork_headers, &fork_block_header, &vec![BlockstackOperationType::LeaderBlockCommit(fork_cmt.clone())], ) @@ -736,8 +731,6 @@ fn test_get_set_check_anchor_block() { ); burnchain_db .store_new_burnchain_block_ops_unchecked( - &burnchain, - &headers, block_header, &vec![BlockstackOperationType::LeaderBlockCommit(cmt.clone())], ) @@ -771,301 +764,6 @@ fn test_get_set_check_anchor_block() { .unwrap()); } -#[test] -fn test_update_block_descendancy() { - let first_bhh = BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(); - let first_timestamp = 0; - let first_height = 1; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = burn_db_test_pox(); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let mut parent = None; - let mut parent_block_header: Option = None; - let mut cmts = vec![]; - let mut cmts_genesis = vec![]; - let mut cmts_invalid = vec![]; - - for i in 0..5 { - let hdr = BurnchainHeaderHash([(i + 1) as u8; 32]); - let block_header = BurnchainBlockHeader { - block_height: first_height + i, - block_hash: hdr, - parent_block_hash: parent_block_header - .as_ref() - .map(|blk| blk.block_hash.clone()) - .unwrap_or(first_block_header.block_hash.clone()), - num_txs: 3, - timestamp: i, - }; - - headers.push(block_header.clone()); - parent_block_header = Some(block_header); - } - - let mut am_id = 0; - - for i in 0..5 { - let block_header = &headers[i + 1]; - - let cmt = make_simple_block_commit( - &burnchain, - parent.as_ref(), - block_header, - BlockHeaderHash([((i + 1) as u8) | 0x80; 32]), - ); - - // make a second commit that builds off of genesis - let mut cmt_genesis = cmt.clone(); - cmt_genesis.parent_block_ptr = 0; - cmt_genesis.parent_vtxindex = 0; - cmt_genesis.block_header_hash = BlockHeaderHash([((i + 1) as u8) | 0xa0; 32]); - cmt_genesis.txid = next_txid(); - - // make an invalid commit - let mut cmt_invalid = cmt.clone(); - cmt_invalid.parent_vtxindex += 1; - cmt_invalid.block_header_hash = BlockHeaderHash([((i + 1) as u8) | 0xc0; 32]); - cmt_invalid.txid = next_txid(); - - burnchain_db - .store_new_burnchain_block_ops_unchecked( - &burnchain, - &headers, - block_header, - &vec![ - BlockstackOperationType::LeaderBlockCommit(cmt.clone()), - BlockstackOperationType::LeaderBlockCommit(cmt_genesis.clone()), - BlockstackOperationType::LeaderBlockCommit(cmt_invalid.clone()), - ], - ) - .unwrap(); - - cmts.push(cmt.clone()); - cmts_genesis.push(cmt_genesis.clone()); - cmts_invalid.push(cmt_invalid.clone()); - - parent = Some(cmt); - - if i == 0 { - am_id = { - let tx = burnchain_db.tx_begin().unwrap(); - tx.set_anchor_block(&cmts[0], 1).unwrap(); - let am_id = tx - .insert_block_commit_affirmation_map(&AffirmationMap::decode("p").unwrap()) - .unwrap(); - tx.update_block_commit_affirmation(&cmts[0], Some(1), am_id) - .unwrap(); - tx.commit().unwrap(); - am_id - }; - assert_ne!(am_id, 0); - } - } - - // each valid commit should have cmts[0]'s affirmation map - for i in 1..5 { - let cmt_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &cmts[i]).unwrap(); - assert_eq!(cmt_am_id.unwrap(), am_id); - - let genesis_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &cmts_genesis[i]) - .unwrap(); - assert_eq!(genesis_am_id.unwrap(), 0); - - let invalid_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &cmts_invalid[i]) - .unwrap(); - assert_eq!(invalid_am_id.unwrap(), 0); - } -} - -#[test] -fn test_update_block_descendancy_with_fork() { - let first_bhh = BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(); - let first_timestamp = 0; - let first_height = 1; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = burn_db_test_pox(); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let mut fork_headers = vec![first_block_header.clone()]; - - let mut parent = None; - let mut parent_block_header: Option = None; - let mut cmts = vec![]; - let mut cmts_genesis = vec![]; - let mut cmts_invalid = vec![]; - - let mut fork_cmts = vec![]; - - for i in 0..5 { - let hdr = BurnchainHeaderHash([(i + 1) as u8; 32]); - let block_header = BurnchainBlockHeader { - block_height: first_height + i, - block_hash: hdr, - parent_block_hash: parent_block_header - .as_ref() - .map(|blk| blk.block_hash.clone()) - .unwrap_or(first_block_header.block_hash.clone()), - num_txs: 3, - timestamp: i, - }; - - headers.push(block_header.clone()); - parent_block_header = Some(block_header); - } - - for i in 0..5 { - let hdr = BurnchainHeaderHash([(i + 128 + 1) as u8; 32]); - let block_header = BurnchainBlockHeader { - block_height: first_height + i, - block_hash: hdr, - parent_block_hash: parent_block_header - .as_ref() - .map(|blk| blk.block_hash.clone()) - .unwrap_or(first_block_header.block_hash.clone()), - num_txs: 3, - timestamp: i, - }; - - fork_headers.push(block_header.clone()); - } - - let mut am_id = 0; - let mut fork_am_id = 0; - - for i in 0..5 { - let block_header = &headers[i + 1]; - let fork_block_header = &fork_headers[i + 1]; - - let cmt = make_simple_block_commit( - &burnchain, - parent.as_ref(), - block_header, - BlockHeaderHash([((i + 1) as u8) | 0x80; 32]), - ); - - // make a second commit that builds off of genesis - let mut cmt_genesis = cmt.clone(); - cmt_genesis.parent_block_ptr = 0; - cmt_genesis.parent_vtxindex = 0; - cmt_genesis.block_header_hash = BlockHeaderHash([((i + 1) as u8) | 0xa0; 32]); - cmt_genesis.txid = next_txid(); - - // make an invalid commit - let mut cmt_invalid = cmt.clone(); - cmt_invalid.parent_vtxindex += 1; - cmt_invalid.block_header_hash = BlockHeaderHash([((i + 1) as u8) | 0xc0; 32]); - cmt_invalid.txid = next_txid(); - - // make a commit on the fork - let mut fork_cmt = cmt.clone(); - fork_cmt.burn_header_hash = fork_block_header.block_hash.clone(); - fork_cmt.vtxindex = 100; - fork_cmt.parent_vtxindex = 100; - - burnchain_db - .store_new_burnchain_block_ops_unchecked( - &burnchain, - &headers, - block_header, - &vec![ - BlockstackOperationType::LeaderBlockCommit(cmt.clone()), - BlockstackOperationType::LeaderBlockCommit(cmt_genesis.clone()), - BlockstackOperationType::LeaderBlockCommit(cmt_invalid.clone()), - ], - ) - .unwrap(); - - burnchain_db - .store_new_burnchain_block_ops_unchecked( - &burnchain, - &fork_headers, - fork_block_header, - &vec![BlockstackOperationType::LeaderBlockCommit(fork_cmt.clone())], - ) - .unwrap(); - - cmts.push(cmt.clone()); - cmts_genesis.push(cmt_genesis.clone()); - cmts_invalid.push(cmt_invalid.clone()); - fork_cmts.push(fork_cmt.clone()); - - parent = Some(cmt); - - if i == 0 { - am_id = { - let tx = burnchain_db.tx_begin().unwrap(); - tx.set_anchor_block(&cmts[0], 1).unwrap(); - let am_id = tx - .insert_block_commit_affirmation_map(&AffirmationMap::decode("p").unwrap()) - .unwrap(); - tx.update_block_commit_affirmation(&cmts[0], Some(1), am_id) - .unwrap(); - tx.commit().unwrap(); - am_id - }; - assert_ne!(am_id, 0); - - fork_am_id = { - let tx = burnchain_db.tx_begin().unwrap(); - tx.set_anchor_block(&fork_cmts[0], 1).unwrap(); - let fork_am_id = tx - .insert_block_commit_affirmation_map(&AffirmationMap::decode("a").unwrap()) - .unwrap(); - tx.update_block_commit_affirmation(&fork_cmts[0], Some(1), fork_am_id) - .unwrap(); - tx.commit().unwrap(); - fork_am_id - }; - assert_ne!(fork_am_id, 0); - } - } - - // each valid commit should have cmts[0]'s affirmation map - for i in 1..5 { - let cmt_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &cmts[i]).unwrap(); - assert_eq!(cmt_am_id.unwrap(), am_id); - - let genesis_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &cmts_genesis[i]) - .unwrap(); - assert_eq!(genesis_am_id.unwrap(), 0); - - let invalid_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &cmts_invalid[i]) - .unwrap(); - assert_eq!(invalid_am_id.unwrap(), 0); - } - - // each valid commit should have fork_cmts[0]'s affirmation map - for i in 1..5 { - let cmt_am_id = - BurnchainDB::get_block_commit_affirmation_id(burnchain_db.conn(), &fork_cmts[i]) - .unwrap(); - assert_eq!(cmt_am_id.unwrap(), fork_am_id); - } -} - #[test] fn test_classify_delegate_stx() { let first_bhh = BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH).unwrap(); diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 72a276c541..43cc6d084b 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -32,7 +32,6 @@ use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::{hex_bytes, to_hex, Sha512Trunc256Sum}; use stacks_common::util::vrf::*; -use crate::burnchains::affirmation::{AffirmationMap, AffirmationMapEntry}; use crate::burnchains::db::BurnchainDB; use crate::burnchains::{ Burnchain, BurnchainBlockHeader, BurnchainStateTransition, BurnchainStateTransitionOps, @@ -912,10 +911,6 @@ impl db_keys { "sortition_db::last_selected_anchor_block_txid" } - pub fn pox_affirmation_map() -> &'static str { - "sortition_db::affirmation_map" - } - pub fn pox_reward_cycle_unlocks(cycle: u64) -> String { format!("sortition_db::reward_set_unlocks::{}", cycle) } @@ -1817,18 +1812,6 @@ impl SortitionHandleTx<'_> { ); Ok(anchor_block_txid) } - - pub fn get_sortition_affirmation_map(&mut self) -> Result { - let chain_tip = self.context.chain_tip.clone(); - let affirmation_map = match self.get_indexed(&chain_tip, db_keys::pox_affirmation_map())? { - Some(am_str) => { - AffirmationMap::decode(&am_str).expect("FATAL: corrupt affirmation map") - } - None => AffirmationMap::empty(), - }; - Ok(affirmation_map) - } - pub fn get_last_selected_anchor_block_hash( &mut self, ) -> Result, db_error> { @@ -2046,17 +2029,6 @@ impl<'a> SortitionHandleConn<'a> { Ok(anchor_block_txid) } - pub fn get_sortition_affirmation_map(&self) -> Result { - let chain_tip = self.context.chain_tip.clone(); - let affirmation_map = match self.get_indexed(&chain_tip, db_keys::pox_affirmation_map())? { - Some(am_str) => { - AffirmationMap::decode(&am_str).expect("FATAL: corrupt affirmation map") - } - None => AffirmationMap::empty(), - }; - Ok(affirmation_map) - } - pub fn get_last_selected_anchor_block_hash(&self) -> Result, db_error> { let anchor_block_hash = SortitionDB::parse_last_anchor_block_hash( self.get_indexed(&self.context.chain_tip, db_keys::pox_last_selected_anchor())?, @@ -6146,14 +6118,9 @@ impl SortitionHandleTx<'_> { pox_id.extend_with_not_present_block(); } - let mut cur_affirmation_map = self.get_sortition_affirmation_map()?; - let mut selected_anchor_block = false; - // if we have selected an anchor block (known or unknown), write that info if let Some((anchor_block, anchor_block_txid)) = reward_info.selected_anchor_block() { - selected_anchor_block = true; - keys.push(db_keys::pox_anchor_to_prepare_end(anchor_block)); values.push(parent_snapshot.sortition_id.to_hex()); @@ -6163,9 +6130,6 @@ impl SortitionHandleTx<'_> { keys.push(db_keys::pox_last_anchor_txid().to_string()); values.push(anchor_block_txid.to_hex()); - keys.push(db_keys::pox_affirmation_map().to_string()); - values.push(cur_affirmation_map.encode()); - keys.push(db_keys::pox_last_selected_anchor().to_string()); values.push(anchor_block.to_hex()); @@ -6182,9 +6146,6 @@ impl SortitionHandleTx<'_> { keys.push(db_keys::pox_last_anchor_txid().to_string()); values.push("".to_string()); - - cur_affirmation_map.push(AffirmationMapEntry::Nothing); - debug!( "No anchor block at reward cycle starting at burn height {}", snapshot.block_height @@ -6245,17 +6206,11 @@ impl SortitionHandleTx<'_> { keys.push(db_keys::pox_reward_cycle_unlocks(cycle_number)); values.push(reward_set.start_cycle_state.serialize()); } - - cur_affirmation_map.push(AffirmationMapEntry::PoxAnchorBlockPresent); } else { // no anchor block; we're burning keys.push(db_keys::pox_reward_set_size().to_string()); values.push(db_keys::reward_set_size_to_string(0)); - if selected_anchor_block { - cur_affirmation_map.push(AffirmationMapEntry::PoxAnchorBlockAbsent); - } - pox_payout_addrs = vec![]; } @@ -6263,9 +6218,6 @@ impl SortitionHandleTx<'_> { keys.push(db_keys::pox_identifier().to_string()); values.push(pox_id.to_string()); - keys.push(db_keys::pox_affirmation_map().to_string()); - values.push(cur_affirmation_map.encode()); - pox_payout_addrs } else { // if this snapshot consumed some reward set entries AND diff --git a/stackslib/src/chainstate/coordinator/tests.rs b/stackslib/src/chainstate/coordinator/tests.rs index 463253d2cf..c05009425a 100644 --- a/stackslib/src/chainstate/coordinator/tests.rs +++ b/stackslib/src/chainstate/coordinator/tests.rs @@ -5822,320 +5822,6 @@ fn test_sortition_with_sunset_and_epoch_switch() { } } -#[test] -// This test should panic until the MARF stability issue -// https://github.com/blockstack/stacks-blockchain/issues/1805 -// is resolved: -#[should_panic] -/// Test a block that is processable in 2 PoX forks: -/// block "11" should be processable in both `111` and `110` -/// (because its parent is block `0`, and nobody stacks in -/// this test, all block commits must burn) -fn test_pox_processable_block_in_different_pox_forks() { - let path = &test_path("pox_processable_block_in_different_pox_forks"); - // setup a second set of states that won't see the broadcasted blocks - let path_blinded = &test_path("pox_processable_block_in_different_pox_forks.blinded"); - let _r = std::fs::remove_dir_all(path); - let _r = std::fs::remove_dir_all(path_blinded); - - let pox_consts = Some(PoxConstants::new( - 5, - 2, - 2, - 25, - 5, - u64::MAX - 1, - u64::MAX, - u32::MAX, - u32::MAX, - u32::MAX, - u32::MAX, - )); - let b = get_burnchain(path, pox_consts.clone()); - let b_blind = get_burnchain(path_blinded, pox_consts.clone()); - - let vrf_keys: Vec<_> = (0..20).map(|_| VRFPrivateKey::new()).collect(); - let committers: Vec<_> = (0..20).map(|_| StacksPrivateKey::random()).collect(); - - setup_states_with_epochs( - &[path, path_blinded], - &vrf_keys, - &committers, - pox_consts.clone(), - None, - StacksEpochId::Epoch2_05, - None, - ); - - let mut coord = make_coordinator(path, Some(b)); - let mut coord_blind = make_coordinator(path_blinded, Some(b_blind)); - - coord.handle_new_burnchain_block().unwrap(); - coord_blind.handle_new_burnchain_block().unwrap(); - - let sort_db = get_sortition_db(path, pox_consts.clone()); - - let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); - assert_eq!(tip.block_height, 1); - assert!(!tip.sortition); - let (_, ops) = sort_db - .get_sortition_result(&tip.sortition_id) - .unwrap() - .unwrap(); - - let sort_db_blind = get_sortition_db(path_blinded, pox_consts.clone()); - - let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db_blind.conn()).unwrap(); - assert_eq!(tip.block_height, 1); - assert!(!tip.sortition); - let (_, ops) = sort_db_blind - .get_sortition_result(&tip.sortition_id) - .unwrap() - .unwrap(); - - // we should have all the VRF registrations accepted - assert_eq!(ops.accepted_ops.len(), vrf_keys.len()); - assert!(ops.consumed_leader_keys.is_empty()); - - // process sequential blocks, and their sortitions... - let mut stacks_blocks: Vec<(SortitionId, StacksBlock)> = vec![]; - - // setup: - // sort:1 6 11 16 21 - // |----- rc 0 --------|------ rc 1 -------|----- rc 2 ------------|-------- rc 3 ----------|----- rc 4 - // ix: X - 0 - 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - // \_____________________________________ 10 _ 11 _ 12 _ 13 _ 14 _ 15 _ 16 _ 17 _ 18 _ 19 - // - // - for (ix, (vrf_key, miner)) in vrf_keys.iter().zip(committers.iter()).enumerate() { - let mut burnchain = get_burnchain_db(path, pox_consts.clone()); - let burnchain_blind = get_burnchain_db(path_blinded, pox_consts.clone()); - let mut chainstate = get_chainstate(path); - let mut chainstate_blind = get_chainstate(path_blinded); - let burnchain_tip = burnchain.get_canonical_chain_tip().unwrap(); - let burnchain_tip_blind = burnchain_blind.get_canonical_chain_tip().unwrap(); - let b = get_burnchain(path, pox_consts.clone()); - let b_blind = get_burnchain(path_blinded, pox_consts.clone()); - - eprintln!("Making block {}", ix); - let (op, block) = if ix == 0 { - make_genesis_block( - &b, - &sort_db, - &mut chainstate, - &BlockHeaderHash([0; 32]), - miner, - 10000, - vrf_key, - ix as u32, - ) - } else { - let parent = if ix == 10 { - stacks_blocks[0].1.header.block_hash() - } else { - stacks_blocks[ix - 1].1.header.block_hash() - }; - if ix < 10 { - make_stacks_block( - &sort_db, - &mut chainstate, - &b, - &parent, - burnchain_tip.block_height, - miner, - 10000, - vrf_key, - ix as u32, - ) - } else { - make_stacks_block( - &sort_db_blind, - &mut chainstate_blind, - &b_blind, - &parent, - burnchain_tip_blind.block_height, - miner, - 10000, - vrf_key, - ix as u32, - ) - } - }; - produce_burn_block( - &b, - &mut burnchain, - &burnchain_tip.block_hash, - vec![op], - [burnchain_blind].iter_mut(), - ); - - loop { - let missing_anchor_opt = coord - .handle_new_burnchain_block() - .unwrap() - .into_missing_block_hash(); - if let Some(missing_anchor) = missing_anchor_opt { - eprintln!( - "Unblinded database reports missing anchor block {:?} (ix={})", - &missing_anchor, ix - ); - for (_, blk) in stacks_blocks.iter() { - if blk.block_hash() == missing_anchor { - let ic = sort_db.index_conn(); - let tip = - SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); - let sn = SortitionDB::get_block_snapshot_for_winning_stacks_block( - &ic, - &tip.sortition_id, - &blk.block_hash(), - ) - .unwrap() - .unwrap(); - - // feed this missing reward cycle data - let rc = b_blind - .block_height_to_reward_cycle(sn.block_height) - .unwrap(); - let start_height = b_blind.reward_cycle_to_block_height(rc); - for height in start_height..sn.block_height { - let asn = - SortitionDB::get_ancestor_snapshot(&ic, height, &tip.sortition_id) - .unwrap() - .unwrap(); - for (_, blk) in stacks_blocks.iter() { - if blk.block_hash() == asn.winning_stacks_block_hash { - eprintln!("Unblinded database accepts missing anchor block ancestor {} of {} (ix={})", &blk.block_hash(), &missing_anchor, ix); - preprocess_block(&mut chainstate, &sort_db, &asn, blk.clone()); - coord.handle_new_stacks_block().unwrap(); - break; - } - } - } - - // *now* process this anchor block - eprintln!( - "Unblinded database processes missing anchor block {} (ix={})", - &missing_anchor, ix - ); - preprocess_block(&mut chainstate, &sort_db, &sn, blk.clone()); - coord.handle_new_stacks_block().unwrap(); - break; - } - } - } else { - coord.handle_new_stacks_block().unwrap(); - break; - } - } - - coord_blind.handle_new_burnchain_block().unwrap(); - coord_blind.handle_new_stacks_block().unwrap(); - - let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); - let blinded_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db_blind.conn()).unwrap(); - - if ix < 10 { - // load the block into staging and process it on the un-blinded sortition DB - let block_hash = block.header.block_hash(); - eprintln!( - "Block hash={}, parent={}, height={}, ix={} (not blind)", - &block_hash, &block.header.parent_block, block.header.total_work.work, ix - ); - - assert_eq!(&tip.winning_stacks_block_hash, &block_hash); - stacks_blocks.push((tip.sortition_id.clone(), block.clone())); - - preprocess_block(&mut chainstate, &sort_db, &tip, block.clone()); - - // handle the stacks block - coord.handle_new_stacks_block().unwrap(); - } - if ix == 0 || ix >= 10 { - // load the block into staging and process it on the blinded sortition DB - let block_hash = block.header.block_hash(); - eprintln!( - "Block hash={}, parent={}, height={}, ix={} (blind)", - &block_hash, &block.header.parent_block, block.header.total_work.work, ix - ); - - assert_eq!(&blinded_tip.winning_stacks_block_hash, &block_hash); - if ix != 0 { - stacks_blocks.push((blinded_tip.sortition_id.clone(), block.clone())); - } - - preprocess_block(&mut chainstate_blind, &sort_db_blind, &blinded_tip, block); - - // handle the stacks block - coord_blind.handle_new_stacks_block().unwrap(); - } - if ix == 18 { - // right at the end of reward cycle 3 -- feed in the blocks from the blinded DB into - // the unblinded DB - for (i, (_, block)) in stacks_blocks.iter().enumerate() { - if i >= 10 && i <= ix { - eprintln!("Mirror blocks from blinded DB to unblinded DB (simulates downloading them) i={}", i); - let ic = sort_db_blind.index_conn(); - let sn = SortitionDB::get_block_snapshot_for_winning_stacks_block( - &ic, - &tip.sortition_id, - &block.block_hash(), - ) - .unwrap() - .unwrap(); - preprocess_block(&mut chainstate, &sort_db, &sn, block.clone()); - let _ = coord.handle_new_stacks_block(); - } - } - } - if ix > 18 { - // starting in reward cycle 4 -- this should NOT panic - eprintln!("Mirror block {} to unblinded DB", ix); - preprocess_block(&mut chainstate, &sort_db, &tip, stacks_blocks[ix].1.clone()); - let _ = coord.handle_new_stacks_block(); - } - } - - // both the blinded and unblined chains should now have the same view - let block_height = eval_at_chain_tip(path, &sort_db, "block-height"); - assert_eq!(block_height, Value::UInt(11)); - - let block_height = eval_at_chain_tip(path_blinded, &sort_db_blind, "block-height"); - assert_eq!(block_height, Value::UInt(11)); - - // because of the affirmations, the canonical PoX ID deliberately omits anchor blocks - { - let ic = sort_db_blind.index_handle_at_tip(); - let pox_id = ic.get_pox_id().unwrap(); - assert_eq!(&pox_id.to_string(), "110011"); - } - { - let ic = sort_db.index_handle_at_tip(); - let pox_id = ic.get_pox_id().unwrap(); - assert_eq!(&pox_id.to_string(), "110011"); - } - - // same canonical Stacks chain tip - let stacks_tip = SortitionDB::get_canonical_stacks_chain_tip_hash(sort_db.conn()).unwrap(); - let stacks_tip_blind = - SortitionDB::get_canonical_stacks_chain_tip_hash(sort_db_blind.conn()).unwrap(); - assert_eq!(stacks_tip, stacks_tip_blind); - - // same final consensus hash, at the start of height 20 - let tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap(); - let blinded_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db_blind.conn()).unwrap(); - - assert!(tip.sortition); - assert!(blinded_tip.sortition); - assert_eq!( - tip.winning_stacks_block_hash, - blinded_tip.winning_stacks_block_hash - ); - assert_eq!(tip.burn_header_hash, blinded_tip.burn_header_hash); - assert_eq!(tip.consensus_hash, blinded_tip.consensus_hash); - assert_eq!(tip.block_height, 21); - assert_eq!(blinded_tip.block_height, 21); -} - #[test] fn test_pox_no_anchor_selected() { let path = &test_path("pox_fork_no_anchor_selected"); From 782e1671bf7722cfeaf13ace0aa013266e1054c4 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 26 Aug 2025 17:59:10 -0400 Subject: [PATCH 27/35] chore: fix epoch2x inv sync to use a given node's last-seen reward cycle as the sync start point, and remove a now-defunct test (PoX bitvectors are always 1's, so there is no need to test for the case when there's a 0) --- stackslib/src/net/inv/epoch2x.rs | 35 +++- stackslib/src/net/tests/inv/epoch2x.rs | 241 ++----------------------- 2 files changed, 37 insertions(+), 239 deletions(-) diff --git a/stackslib/src/net/inv/epoch2x.rs b/stackslib/src/net/inv/epoch2x.rs index f4a012604a..8451a6ccd7 100644 --- a/stackslib/src/net/inv/epoch2x.rs +++ b/stackslib/src/net/inv/epoch2x.rs @@ -1806,8 +1806,12 @@ impl PeerNetwork { } } - /// Determine at which reward cycle to begin scanning inventories - pub(crate) fn get_block_scan_start(&self, sortdb: &SortitionDB) -> u64 { + /// Determine at which reward cycle to begin scanning inventories for this particular neighbor. + pub(crate) fn get_block_scan_start( + &self, + sortdb: &SortitionDB, + peer_block_reward_cycle: u64, + ) -> u64 { // Resume scanning off of wherever we are at the tip. // NOTE: This code path only works in Stacks 2.x, but that's okay because this whole state // machine is only used in Stacks 2.x @@ -1827,14 +1831,15 @@ impl PeerNetwork { .block_height_to_reward_cycle(stacks_tip_burn_block_height) .unwrap_or(0); - let inv_rescan_rc = stacks_tip_rc.saturating_sub(self.connection_opts.inv_reward_cycles); - // Always want to do the last reward cycle AND the current reward cycle (we could be still looking for the current reward cycles anchor block which is mined in the prior reward cycle) - let prior_rc = stacks_tip_rc.saturating_sub(1); - let rescan_rc = std::cmp::min(inv_rescan_rc, prior_rc); + // NOTE: include the last full reward cycle + let inv_rescan_rc = + stacks_tip_rc.saturating_sub(cmp::max(1, self.connection_opts.inv_reward_cycles)); + + let rescan_rc = std::cmp::min(inv_rescan_rc, peer_block_reward_cycle); test_debug!("begin blocks inv scan at {rescan_rc}"; "stacks_tip_rc" => stacks_tip_rc, - "prior_rc" => prior_rc, + "peer_block_rc" => peer_block_reward_cycle, "inv_rescan_rc" => inv_rescan_rc, ); rescan_rc @@ -1855,7 +1860,7 @@ impl PeerNetwork { Some(x) => x, None => { // proceed to block scan - let scan_start_rc = self.get_block_scan_start(sortdb); + let scan_start_rc = self.get_block_scan_start(sortdb, stats.block_reward_cycle); debug!("{:?}: cannot make any more GetPoxInv requests for {:?}; proceeding to block inventory scan at reward cycle {}", &self.local_peer, nk, scan_start_rc); stats.reset_block_scan(scan_start_rc); return Ok(()); @@ -1914,7 +1919,7 @@ impl PeerNetwork { // proceed with block scan. // If we're in IBD, then this is an always-allowed peer and we should // react to divergences by deepening our rescan. - let scan_start_rc = self.get_block_scan_start(sortdb); + let scan_start_rc = self.get_block_scan_start(sortdb, stats.block_reward_cycle); debug!( "{:?}: proceeding to block inventory scan for {:?} (diverged) at reward cycle {} (ibd={})", &self.local_peer, nk, scan_start_rc, ibd @@ -2015,7 +2020,7 @@ impl PeerNetwork { } // proceed to block scan. - let scan_start = self.get_block_scan_start(sortdb); + let scan_start = self.get_block_scan_start(sortdb, stats.block_reward_cycle); debug!( "{:?}: proceeding to block inventory scan for {:?} at reward cycle {}", &self.local_peer, nk, scan_start @@ -2397,11 +2402,21 @@ impl PeerNetwork { let broken_peers = inv_state.get_broken_peers(); let dead_peers = inv_state.get_dead_peers(); + let local_rc = network.burnchain.block_height_to_reward_cycle(network.stacks_tip.burnchain_height).unwrap_or(0); + + // find peer with lowest block_reward_cycle + let lowest_block_reward_cycle = inv_state + .block_stats + .iter() + .map(|(_nk, stats)| stats.block_reward_cycle) + .fold(local_rc, |min_block_reward_cycle, rc| cmp::min(rc, min_block_reward_cycle)); + // hint to downloader as to where to begin scanning next time inv_state.block_sortition_start = ibd_diverged_height .unwrap_or(network.burnchain.reward_cycle_to_block_height( network.get_block_scan_start( sortdb, + lowest_block_reward_cycle ), )) .saturating_sub(sortdb.first_block_height); diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index 1f3d2091c3..0e9bb841dd 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -1253,36 +1253,43 @@ fn test_inv_sync_start_reward_cycle() { let block_scan_start = peer_1 .network - .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap(), 10); assert_eq!(block_scan_start, 7); peer_1.network.connection_opts.inv_reward_cycles = 1; let block_scan_start = peer_1 .network - .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap(), 10); assert_eq!(block_scan_start, 7); peer_1.network.connection_opts.inv_reward_cycles = 2; let block_scan_start = peer_1 .network - .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap(), 10); assert_eq!(block_scan_start, 6); peer_1.network.connection_opts.inv_reward_cycles = 3; let block_scan_start = peer_1 .network - .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap(), 10); assert_eq!(block_scan_start, 5); peer_1.network.connection_opts.inv_reward_cycles = 300; let block_scan_start = peer_1 .network - .get_block_scan_start(peer_1.sortdb.as_ref().unwrap()); + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap(), 10); assert_eq!(block_scan_start, 0); + + peer_1.network.connection_opts.inv_reward_cycles = 0; + + let block_scan_start = peer_1 + .network + .get_block_scan_start(peer_1.sortdb.as_ref().unwrap(), 1); + assert_eq!(block_scan_start, 1); } #[test] @@ -1793,230 +1800,6 @@ fn test_sync_inv_2_peers_unstable() { }) } -#[test] -#[ignore] -fn test_sync_inv_2_peers_different_pox_vectors() { - with_timeout(600, || { - let mut peer_1_config = TestPeerConfig::new(function_name!(), 0, 0); - let mut peer_2_config = TestPeerConfig::new(function_name!(), 0, 0); - - peer_1_config.connection_opts.inv_reward_cycles = 10; - peer_2_config.connection_opts.inv_reward_cycles = 10; - - let reward_cycle_length = peer_1_config.burnchain.pox_constants.reward_cycle_length as u64; - assert_eq!(reward_cycle_length, 5); - - let mut peer_1 = TestPeer::new(peer_1_config); - let mut peer_2 = TestPeer::new(peer_2_config); - - peer_1.add_neighbor(&mut peer_2.to_neighbor(), None, true); - peer_2.add_neighbor(&mut peer_1.to_neighbor(), None, true); - - let num_blocks = GETPOXINV_MAX_BITLEN * 3; - - let first_stacks_block_height = { - let sn = - SortitionDB::get_canonical_burn_chain_tip(peer_1.sortdb.as_ref().unwrap().conn()) - .unwrap(); - sn.block_height + 1 - }; - - // only peer 2 makes progress after the point of stability. - for i in 0..num_blocks { - let (mut burn_ops, stacks_block, microblocks) = peer_2.make_default_tenure(); - - let (_, burn_header_hash, consensus_hash) = - peer_2.next_burnchain_block(burn_ops.clone()); - peer_2.process_stacks_epoch_at_tip(&stacks_block, µblocks); - - TestPeer::set_ops_burn_header_hash(&mut burn_ops, &burn_header_hash); - - peer_1.next_burnchain_block_raw(burn_ops.clone()); - if i < num_blocks - reward_cycle_length * 2 { - peer_1.process_stacks_epoch_at_tip(&stacks_block, µblocks); - } - } - - let peer_1_pox_id = { - let tip_sort_id = - SortitionDB::get_canonical_sortition_tip(peer_1.sortdb.as_ref().unwrap().conn()) - .unwrap(); - let ic = peer_1.sortdb.as_ref().unwrap().index_conn(); - let sortdb_reader = SortitionHandleConn::open_reader(&ic, &tip_sort_id).unwrap(); - sortdb_reader.get_pox_id().unwrap() - }; - - let peer_2_pox_id = { - let tip_sort_id = - SortitionDB::get_canonical_sortition_tip(peer_2.sortdb.as_ref().unwrap().conn()) - .unwrap(); - let ic = peer_2.sortdb.as_ref().unwrap().index_conn(); - let sortdb_reader = SortitionHandleConn::open_reader(&ic, &tip_sort_id).unwrap(); - sortdb_reader.get_pox_id().unwrap() - }; - - // peers must have different PoX bit vectors -- peer 1 didn't see the last reward cycle - assert_eq!( - peer_1_pox_id, - PoxId::from_bools(vec![ - true, true, true, true, true, true, true, true, true, true, false - ]) - ); - assert_eq!( - peer_2_pox_id, - PoxId::from_bools(vec![ - true, true, true, true, true, true, true, true, true, true, true - ]) - ); - - let num_burn_blocks = { - let sn = - SortitionDB::get_canonical_burn_chain_tip(peer_1.sortdb.as_ref().unwrap().conn()) - .unwrap(); - sn.block_height + 1 - }; - - let mut round = 0; - let mut inv_1_count = 0; - let mut inv_2_count = 0; - let mut peer_1_sorts = 0; - let mut peer_2_sorts = 0; - - while inv_1_count < reward_cycle_length * 4 - || inv_2_count < num_blocks - reward_cycle_length * 2 - || peer_1_sorts < reward_cycle_length * 9 + 1 - || peer_2_sorts < reward_cycle_length * 9 + 1 - { - let _ = peer_1.step(); - let _ = peer_2.step(); - - // peer 1 should see that peer 2 has all blocks for reward cycles 5 through 9 - if let Some(ref inv) = peer_1.network.inv_state { - inv_1_count = inv.get_inv_num_blocks(&peer_2.to_neighbor().addr); - peer_1_sorts = inv.get_inv_sortitions(&peer_2.to_neighbor().addr); - }; - - // peer 2 should see that peer 1 has all blocks up to where we stopped feeding them to - // it - if let Some(ref inv) = peer_2.network.inv_state { - inv_2_count = inv.get_inv_num_blocks(&peer_1.to_neighbor().addr); - peer_2_sorts = inv.get_inv_sortitions(&peer_1.to_neighbor().addr); - }; - - if let Some(ref inv) = peer_1.network.inv_state { - info!("Peer 1 stats: {:?}", &inv.block_stats); - assert!(inv.get_broken_peers().is_empty()); - assert!(inv.get_dead_peers().is_empty()); - assert!(inv.get_diverged_peers().is_empty()); - } - - if let Some(ref inv) = peer_2.network.inv_state { - info!("Peer 2 stats: {:?}", &inv.block_stats); - assert!(inv.get_broken_peers().is_empty()); - assert!(inv.get_dead_peers().is_empty()); - assert!(inv.get_diverged_peers().is_empty()); - } - - round += 1; - - test_debug!( - "\n\ninv_1_count = {} 0 { - assert!(peer_2_inv.has_ith_microblock_stream(i + first_stacks_block_height)); - } else { - assert!(!peer_2_inv.has_ith_microblock_stream(i + first_stacks_block_height)); - } - } - - // peer 2 should have learned about all of peer 1's blocks - for i in 0..(num_blocks - 2 * reward_cycle_length) { - assert!(peer_1_inv.has_ith_block(i + first_stacks_block_height)); - if i > 0 && i != num_blocks - 2 * reward_cycle_length - 1 { - // peer 1 doesn't have the final microblock stream, since no anchor block confirmed it - assert!(peer_1_inv.has_ith_microblock_stream(i + first_stacks_block_height)); - } - } - - assert!(!peer_1_inv.has_ith_block(reward_cycle_length * 4)); - assert!(!peer_1_inv.has_ith_microblock_stream(reward_cycle_length * 4)); - - assert!(!peer_2_inv.has_ith_block(num_blocks - 2 * reward_cycle_length)); - assert!(!peer_2_inv.has_ith_microblock_stream(num_blocks - 2 * reward_cycle_length)); - }) -} - /// Helper function to create a test neighbor without binding to a port fn create_test_neighbor(port: u16) -> Neighbor { let private_key = Secp256k1PrivateKey::random(); From f0af8d481c7f002730887b0f66795aa28438f281 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 27 Aug 2025 11:11:28 -0700 Subject: [PATCH 28/35] Cleanup Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/burn/db/sortdb.rs | 154 ++------------------ stackslib/src/chainstate/coordinator/mod.rs | 14 +- stackslib/src/main.rs | 3 +- stackslib/src/net/tests/inv/epoch2x.rs | 1 - 4 files changed, 11 insertions(+), 161 deletions(-) diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index 43cc6d084b..f8b5c5ff45 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -32,7 +32,6 @@ use stacks_common::types::StacksPublicKeyBuffer; use stacks_common::util::hash::{hex_bytes, to_hex, Sha512Trunc256Sum}; use stacks_common::util::vrf::*; -use crate::burnchains::db::BurnchainDB; use crate::burnchains::{ Burnchain, BurnchainBlockHeader, BurnchainStateTransition, BurnchainStateTransitionOps, BurnchainView, Error as BurnchainError, PoxConstants, Txid, @@ -2390,148 +2389,28 @@ impl<'a> SortitionHandleConn<'a> { /// Get the chosen PoX anchor block for a reward cycle, given the last block of its prepare /// phase. /// - /// In epochs 2.05 and earlier, this was the Stacks block that received F*w confirmations. - /// - /// In epoch 2.1 and later, this was the Stacks block whose block-commit received not only F*w - /// confirmations from subsequent block-commits in the prepare phase, but also the most BTC - /// burnt from all such candidates (and if there is still a tie, then the higher block-commit - /// is chosen). In particular, the block-commit is not required to be the winning block-commit - /// or even a valid block-commit, unlike in 2.05. However, this will be a winning block-commit - /// if at least 20% of the BTC was spent honestly. - /// - /// Here, instead of reasoning about which epoch we're in, the caller will instead supply an - /// optional handle to the burnchain database. If it's `Some(..)`, then the epoch 2.1 rules - /// are used -- the PoX anchor block will be determined from block-commits. If it's `None`, - /// then the epoch 2.05 rules are used -- the PoX anchor block will be determined from present - /// Stacks blocks. - pub fn get_chosen_pox_anchor( - &self, - burnchain_db_conn: Option<&DBConn>, - prepare_end_bhh: &BurnchainHeaderHash, - pox_consts: &PoxConstants, - ) -> Result, CoordinatorError> { - // match burnchain_db_conn { - // Some(conn) => self.get_chosen_pox_anchor_v210(conn, prepare_end_bhh, pox_consts), - // None => self.get_chosen_pox_anchor_v205(prepare_end_bhh, pox_consts), - // } - self.get_chosen_pox_anchor_v205(prepare_end_bhh, pox_consts) - } - - /// Use the epoch 2.1 method for choosing a PoX anchor block. - /// The PoX anchor block corresponds to the block-commit that received F*w confirmations in its - /// prepare phase, and had the most BTC of all such candidates, and was the highest of all such - /// candidates. This information is stored in the burnchain DB. - /// - /// Note that it does not matter if the anchor block-commit does not correspond to a valid - /// Stacks block, or even won sortition. The result is the same for the honest network - /// participants: they mark the anchor block as absent in their affirmation maps, and this PoX - /// fork's quality is degraded as such. - pub fn get_chosen_pox_anchor_v210( - &self, - burnchain_db_conn: &DBConn, - prepare_end_bhh: &BurnchainHeaderHash, - pox_consts: &PoxConstants, - ) -> Result, CoordinatorError> { - let rc = match self.get_heights_for_prepare_phase_end_block( - prepare_end_bhh, - pox_consts, - true, - )? { - Some((_, block_height)) => { - let rc = pox_consts - .block_height_to_reward_cycle( - self.context.first_block_height, - block_height.into(), - ) - .ok_or(CoordinatorError::NotPrepareEndBlock)?; - rc - } - None => { - // there can't be an anchor block - return Ok(None); - } - }; - let (anchor_block_op, anchor_block_metadata) = { - let mut res = None; - let metadatas = BurnchainDB::get_anchor_block_commit_metadatas(burnchain_db_conn, rc)?; - - // find the one on this fork - for metadata in metadatas { - let sn = SortitionDB::get_ancestor_snapshot( - self, - metadata.block_height, - &self.context.chain_tip, - )? - .expect("FATAL: accepted block-commit but no sortition at height"); - if sn.burn_header_hash == metadata.burn_block_hash { - // this is the metadata on this burnchain fork - res = match BurnchainDB::get_anchor_block_commit( - burnchain_db_conn, - &sn.burn_header_hash, - rc, - )? { - Some(x) => Some(x), - None => { - continue; - } - }; - if res.is_some() { - break; - } - } - } - if let Some(x) = res { - x - } else { - // no anchor block - test_debug!("No anchor block for reward cycle {}", rc); - return Ok(None); - } - }; - - // sanity check: we must have processed this burnchain block already - let anchor_sort_id = self.get_sortition_id_for_bhh(&anchor_block_metadata.burn_block_hash)? - .ok_or_else(|| { - warn!("Missing anchor block sortition"; "burn_header_hash" => %anchor_block_metadata.burn_block_hash, "sortition_tip" => %&self.context.chain_tip); - BurnchainError::MissingParentBlock - })?; - - let anchor_sn = SortitionDB::get_block_snapshot(self, &anchor_sort_id)? - .ok_or(BurnchainError::MissingParentBlock)?; - - // the sortition does not even need to have picked this anchor block; all that matters is - // that miners confirmed it. If the winning block hash doesn't even correspond to a Stacks - // block, then the honest miners in the network will affirm that it's absent. - let ch = anchor_sn.consensus_hash; - Ok(Some(( - ch, - anchor_block_op.block_header_hash, - anchor_block_op.txid, - ))) - } - - /// This is the method for calculating the PoX anchor block for a reward cycle in epoch 2.05 and earlier. + /// This is the method for calculating the PoX anchor block for a reward cycle. /// Return identifying information for a PoX anchor block for the reward cycle that /// begins the block after `prepare_end_bhh`. /// If a PoX anchor block is chosen, this returns Some, if a PoX anchor block was not /// selected, return `None` /// `prepare_end_bhh`: this is the burn block which is the last block in the prepare phase /// for the corresponding reward cycle - fn get_chosen_pox_anchor_v205( + pub fn get_chosen_pox_anchor( &self, prepare_end_bhh: &BurnchainHeaderHash, pox_consts: &PoxConstants, ) -> Result, CoordinatorError> { - match self.get_chosen_pox_anchor_check_position_v205(prepare_end_bhh, pox_consts, true) { + match self.get_chosen_pox_anchor_check_position(prepare_end_bhh, pox_consts, true) { Ok(Ok((c_hash, bh_hash, txid, _))) => Ok(Some((c_hash, bh_hash, txid))), Ok(Err(_)) => Ok(None), Err(e) => Err(e), } } - /// This is the method for calculating the PoX anchor block for a reward cycle in epoch 2.05 and earlier. + /// This is the method for calculating the PoX anchor block for a reward cycle /// If no PoX anchor block is found, it returns Ok(Err(maximum confirmations of all candidates)) - pub fn get_chosen_pox_anchor_check_position_v205( + pub fn get_chosen_pox_anchor_check_position( &self, prepare_end_bhh: &BurnchainHeaderHash, pox_consts: &PoxConstants, @@ -6628,6 +6507,7 @@ pub mod tests { use stacks_common::util::vrf::*; use super::*; + use crate::burnchains::db::BurnchainDB; use crate::burnchains::tests::affirmation::{make_reward_cycle, make_simple_key_register}; use crate::burnchains::*; use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; @@ -10657,16 +10537,8 @@ pub mod tests { let tip = SortitionDB::get_canonical_burn_chain_tip(db.conn()).unwrap(); { let ic = db.index_handle(&tip.sortition_id); - let anchor_2_05 = ic - .get_chosen_pox_anchor(None, &tip.burn_header_hash, &pox_consts) - .unwrap() - .unwrap(); - let anchor_2_1 = ic - .get_chosen_pox_anchor( - Some(burnchain_db.conn()), - &tip.burn_header_hash, - &pox_consts, - ) + let anchor = ic + .get_chosen_pox_anchor(&tip.burn_header_hash, &pox_consts) .unwrap() .unwrap(); @@ -10676,21 +10548,13 @@ pub mod tests { .unwrap() .consensus_hash; assert_eq!( - anchor_2_05, + anchor, ( expected_anchor_ch.clone(), commits[6][0].as_ref().unwrap().block_header_hash.clone(), commits[6][0].as_ref().unwrap().txid.clone(), ) ); - assert_eq!( - anchor_2_1, - ( - expected_anchor_ch, - commits[6][1].as_ref().unwrap().block_header_hash.clone(), - commits[6][1].as_ref().unwrap().txid.clone(), - ) - ); } } diff --git a/stackslib/src/chainstate/coordinator/mod.rs b/stackslib/src/chainstate/coordinator/mod.rs index e976e5694e..65e48e31d1 100644 --- a/stackslib/src/chainstate/coordinator/mod.rs +++ b/stackslib/src/chainstate/coordinator/mod.rs @@ -703,13 +703,11 @@ pub fn get_next_recipients( burnchain: &Burnchain, provider: &U, ) -> Result, Error> { - let burnchain_db = BurnchainDB::open(&burnchain.get_burnchaindb_path(), false)?; let reward_cycle_info = get_reward_cycle_info( sortition_tip.block_height + 1, &sortition_tip.burn_header_hash, &sortition_tip.sortition_id, burnchain, - &burnchain_db, chain_state, sort_db, provider, @@ -729,7 +727,6 @@ pub fn get_reward_cycle_info( parent_bhh: &BurnchainHeaderHash, sortition_tip: &SortitionId, burnchain: &Burnchain, - burnchain_db: &BurnchainDB, chain_state: &mut StacksChainState, sort_db: &mut SortitionDB, provider: &U, @@ -763,14 +760,7 @@ pub fn get_reward_cycle_info( let reward_cycle_info = { let ic = sort_db.index_handle(sortition_tip); - let burnchain_db_conn_opt = if epoch_at_height.epoch_id >= StacksEpochId::Epoch21 { - // use the new block-commit-based PoX anchor block selection rules - Some(burnchain_db.conn()) - } else { - None - }; - - ic.get_chosen_pox_anchor(burnchain_db_conn_opt, parent_bhh, &burnchain.pox_constants) + ic.get_chosen_pox_anchor(parent_bhh, &burnchain.pox_constants) }?; let reward_cycle_info = if let Some((consensus_hash, stacks_block_hash, txid)) = reward_cycle_info { @@ -1427,7 +1417,6 @@ impl< &burn_header.parent_block_hash, sortition_tip_id, &self.burnchain, - &self.burnchain_blocks_db, &mut self.chain_state_db, &mut self.sortition_db, &self.reward_set_provider, @@ -1914,7 +1903,6 @@ impl SortitionDBMigrator { &ancestor_sn.burn_header_hash, &ancestor_sn.sortition_id, &self.burnchain, - &self.burnchain_db, &mut chainstate, sort_db, &OnChainRewardSetProvider::new(), diff --git a/stackslib/src/main.rs b/stackslib/src/main.rs index 7d63a3aab7..16aec03273 100644 --- a/stackslib/src/main.rs +++ b/stackslib/src/main.rs @@ -768,7 +768,7 @@ check if the associated microblocks can be downloaded let pox_consts = PoxConstants::mainnet_default(); let result = sort_conn - .get_chosen_pox_anchor_check_position_v205( + .get_chosen_pox_anchor_check_position( &eval_tip.burn_header_hash, &pox_consts, false, @@ -1937,7 +1937,6 @@ fn analyze_sortition_mev(argv: Vec) { &burn_block.header.parent_block_hash, &tip_sort_id, &burnchain, - &burnchaindb, &mut chainstate, &mut sortdb, &OnChainRewardSetProvider::new(), diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index 0e9bb841dd..6ad7d249df 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -25,7 +25,6 @@ use crate::burnchains::bitcoin::indexer::BitcoinIndexer; use crate::burnchains::db::BurnchainHeaderReader; use crate::burnchains::tests::BURNCHAIN_TEST_BLOCK_TIME; use crate::burnchains::{Burnchain, BurnchainBlockHeader, BurnchainView, PoxConstants}; -use crate::chainstate::burn::db::sortdb::SortitionHandleConn; use crate::chainstate::coordinator::tests::get_burnchain; use crate::net::chat::ConversationP2P; use crate::net::inv::inv2x::*; From bb4423a1ee0b948f8b6e213a4d3b70fffd5bfdd0 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 3 Sep 2025 13:37:25 -0700 Subject: [PATCH 29/35] Remove affirmation_weight from block_headers in chainstate db Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/stacks/db/headers.rs | 11 +-- stackslib/src/chainstate/stacks/db/mod.rs | 99 ++++++++++++++++++- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/stackslib/src/chainstate/stacks/db/headers.rs b/stackslib/src/chainstate/stacks/db/headers.rs index 60fd1af3c5..1be4fef48a 100644 --- a/stackslib/src/chainstate/stacks/db/headers.rs +++ b/stackslib/src/chainstate/stacks/db/headers.rs @@ -23,8 +23,7 @@ use crate::chainstate::stacks::db::*; use crate::chainstate::stacks::{Error, *}; use crate::core::{FIRST_BURNCHAIN_CONSENSUS_HASH, FIRST_STACKS_BLOCK_HASH}; use crate::util_lib::db::{ - query_row, query_row_columns, query_row_panic, u64_to_sql, DBConn, Error as db_error, - FromColumn, FromRow, + query_row, query_row_columns, query_row_panic, DBConn, Error as db_error, FromColumn, FromRow, }; impl FromRow for StacksBlockHeader { @@ -152,8 +151,7 @@ impl StacksChainState { index_root, anchored_block_cost, block_size_str, - parent_id, - u64_to_sql(0)?, // TODO: remove this + parent_id ]; tx.execute("INSERT INTO block_headers \ @@ -177,9 +175,8 @@ impl StacksChainState { index_root, cost, block_size, - parent_block_id, - affirmation_weight) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22)", args) + parent_block_id) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21)", args) .map_err(|e| Error::DBError(db_error::SqliteError(e)))?; Ok(()) diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index 75aab07ae4..d341261ef7 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -644,7 +644,7 @@ impl<'a> DerefMut for ChainstateTx<'a> { } } -pub const CHAINSTATE_VERSION: &str = "10"; +pub const CHAINSTATE_VERSION: &str = "11"; const CHAINSTATE_INITIAL_SCHEMA: &[&str] = &[ "PRAGMA foreign_keys = ON;", @@ -852,6 +852,93 @@ const CHAINSTATE_SCHEMA_4: &[&str] = &[ "#, ]; +pub static CHAINSTATE_SCHEMA_5: &[&str] = &[ + // Schema change: drop the affirmation_weight column from pre_nakamoto block_headers and any indexes that reference it + // but leave everything else the same + r#" + -- Rename old block_headers table + ALTER TABLE block_headers RENAME TO block_headers_old; + + -- Create new schema without affirmation_weight + CREATE TABLE block_headers( + version INTEGER NOT NULL, + total_burn TEXT NOT NULL, -- converted to/from u64 + total_work TEXT NOT NULL, -- converted to/from u64 + proof TEXT NOT NULL, + parent_block TEXT NOT NULL, -- hash of parent Stacks block + parent_microblock TEXT NOT NULL, + parent_microblock_sequence INTEGER NOT NULL, + tx_merkle_root TEXT NOT NULL, + state_index_root TEXT NOT NULL, + microblock_pubkey_hash TEXT NOT NULL, + block_hash TEXT NOT NULL, -- NOTE: not unique, as two burn chain forks can commit to the same Stacks block + index_block_hash TEXT UNIQUE NOT NULL, -- globally unique index hash + block_height INTEGER NOT NULL, + index_root TEXT NOT NULL, -- root hash of internal MARF for chainstate/fork metadata + consensus_hash TEXT UNIQUE NOT NULL, -- guaranteed to be unique + burn_header_hash TEXT NOT NULL, -- burn header hash corresponding to consensus hash + burn_header_height INT NOT NULL, -- height of the burnchain block header + burn_header_timestamp INT NOT NULL,-- timestamp from burnchain block header + parent_block_id TEXT NOT NULL, -- parent index_block_hash + cost TEXT NOT NULL, + block_size TEXT NOT NULL, -- converted to/from u64 + PRIMARY KEY(consensus_hash, block_hash) + ); + + -- Copy data from old table, ignoring affirmation_weight + INSERT INTO block_headers( + version, + total_burn, + total_work, + proof, + parent_block, + parent_microblock, + parent_microblock_sequence, + tx_merkle_root, + state_index_root, + microblock_pubkey_hash, + block_hash, + index_block_hash, + block_height, + index_root, + consensus_hash, + burn_header_hash, + burn_header_height, + burn_header_timestamp, + parent_block_id, + cost, + block_size + ) + SELECT + version, + total_burn, + total_work, + proof, + parent_block, + parent_microblock, + parent_microblock_sequence, + tx_merkle_root, + state_index_root, + microblock_pubkey_hash, + block_hash, + index_block_hash, + block_height, + index_root, + consensus_hash, + burn_header_hash, + burn_header_height, + burn_header_timestamp, + parent_block_id, + cost, + block_size + FROM block_headers_old; + + -- Drop old block_headers table + DROP TABLE block_headers_old; + "#, + r#"UPDATE db_config SET version = "11";"#, +]; + const CHAINSTATE_INDEXES: &[&str] = &[ "CREATE INDEX IF NOT EXISTS index_block_hash_to_primary_key ON block_headers(index_block_hash,consensus_hash,block_hash);", "CREATE INDEX IF NOT EXISTS block_headers_hash_index ON block_headers(block_hash,block_height);", @@ -873,8 +960,6 @@ const CHAINSTATE_INDEXES: &[&str] = &[ "CREATE INDEX IF NOT EXISTS height_stacks_blocks ON staging_blocks(height);", "CREATE INDEX IF NOT EXISTS txid_tx_index ON transactions(txid);", "CREATE INDEX IF NOT EXISTS index_block_hash_tx_index ON transactions(index_block_hash);", - "CREATE INDEX IF NOT EXISTS index_block_header_by_affirmation_weight ON block_headers(affirmation_weight);", - "CREATE INDEX IF NOT EXISTS index_block_header_by_height_and_affirmation_weight ON block_headers(block_height,affirmation_weight);", "CREATE INDEX IF NOT EXISTS index_headers_by_consensus_hash ON block_headers(consensus_hash);", "CREATE INDEX IF NOT EXISTS processable_block ON staging_blocks(processed, orphaned, attachable);", ]; @@ -1120,6 +1205,14 @@ impl StacksChainState { tx.execute_batch(cmd)?; } } + "10" => { + info!( + "Migrating chainstate schema from version 10 to 11: drop affirmation_weight from block_headers" + ); + for cmd in CHAINSTATE_SCHEMA_5.iter() { + tx.execute_batch(cmd)?; + } + } _ => { error!( "Invalid chain state database: expected version = {}, got {}", From 166234d2b1e34127192293424a7b036b47eec061 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 3 Sep 2025 13:38:39 -0700 Subject: [PATCH 30/35] Remove affirmation_id from block_commit_metadata and drop it and affirmation_maps from burnchain db Signed-off-by: Jacinta Ferrant --- stackslib/src/burnchains/db.rs | 527 ++++++++++++--------------------- 1 file changed, 196 insertions(+), 331 deletions(-) diff --git a/stackslib/src/burnchains/db.rs b/stackslib/src/burnchains/db.rs index 015e9f1f69..579e2deb39 100644 --- a/stackslib/src/burnchains/db.rs +++ b/stackslib/src/burnchains/db.rs @@ -23,7 +23,6 @@ use serde_json; use stacks_common::types::chainstate::BurnchainHeaderHash; use stacks_common::types::sqlite::NO_PARAMS; -use crate::burnchains::affirmation::*; use crate::burnchains::{ Burnchain, BurnchainBlock, BurnchainBlockHeader, Error as BurnchainError, Txid, }; @@ -32,10 +31,26 @@ use crate::chainstate::burn::BlockSnapshot; use crate::chainstate::stacks::index::ClarityMarfTrieId; use crate::core::StacksEpochId; use crate::util_lib::db::{ - opt_u64_to_sql, query_row, query_row_panic, query_rows, sqlite_open, tx_begin_immediate, - u64_to_sql, DBConn, Error as DBError, FromColumn, FromRow, + opt_u64_to_sql, query_row, query_row_panic, query_rows, sqlite_open, table_exists, + tx_begin_immediate, u64_to_sql, DBConn, Error as DBError, FromColumn, FromRow, }; +struct Migration { + version: u32, + statements: &'static [&'static str], +} + +static MIGRATIONS: &[Migration] = &[ + Migration { + version: 2, + statements: SCHEMA_2, + }, + Migration { + version: 3, + statements: SCHEMA_3, + }, +]; + pub struct BurnchainDB { pub(crate) conn: Connection, } @@ -74,7 +89,6 @@ pub struct BlockCommitMetadata { pub txid: Txid, pub block_height: u64, pub vtxindex: u32, - pub affirmation_id: u64, /// if Some(..), then this block-commit is the anchor block for a reward cycle, and the /// reward cycle is represented as the inner u64. pub anchor_block: Option, @@ -82,27 +96,12 @@ pub struct BlockCommitMetadata { pub anchor_block_descendant: Option, } -impl FromColumn for AffirmationMap { - fn from_column(row: &Row, col_name: &str) -> Result { - let txt: String = row.get_unwrap(col_name); - let am = AffirmationMap::decode(&txt).ok_or(DBError::ParseError)?; - Ok(am) - } -} - -impl FromRow for AffirmationMap { - fn from_row(row: &Row) -> Result { - AffirmationMap::from_column(row, "affirmation_map") - } -} - impl FromRow for BlockCommitMetadata { fn from_row(row: &Row) -> Result { let burn_block_hash = BurnchainHeaderHash::from_column(row, "burn_block_hash")?; let txid = Txid::from_column(row, "txid")?; let block_height = u64::from_column(row, "block_height")?; let vtxindex: u32 = row.get_unwrap("vtxindex"); - let affirmation_id = u64::from_column(row, "affirmation_id")?; let anchor_block_i64: Option = row.get_unwrap("anchor_block"); let anchor_block = match anchor_block_i64 { Some(ab) => { @@ -130,7 +129,6 @@ impl FromRow for BlockCommitMetadata { txid, block_height, vtxindex, - affirmation_id, anchor_block, anchor_block_descendant, }) @@ -206,11 +204,8 @@ impl FromRow for BlockstackOperationType { Ok(deserialized) } } - -pub const BURNCHAIN_DB_VERSION: &str = "2"; - -const BURNCHAIN_DB_SCHEMA: &str = r#" -CREATE TABLE burnchain_db_block_headers ( +const BURNCHAIN_DB_SCHEMA_2: &str = r#" +CREATE TABLE IF NOT EXISTS burnchain_db_block_headers ( -- height of the block (non-negative) block_height INTEGER NOT NULL, -- 32-byte hash of the block @@ -225,7 +220,7 @@ CREATE TABLE burnchain_db_block_headers ( PRIMARY KEY(block_hash) ); -CREATE TABLE burnchain_db_block_ops ( +CREATE TABLE IF NOT EXISTS burnchain_db_block_ops ( -- 32-byte hash of the block that contains this parsed operation block_hash TEXT NOT NULL, -- opaque serialized operation (e.g. a JSON string) @@ -242,7 +237,7 @@ CREATE TABLE burnchain_db_block_ops ( FOREIGN KEY(block_hash) REFERENCES burnchain_db_block_headers(block_hash) ); -CREATE TABLE affirmation_maps ( +CREATE TABLE IF NOT EXISTS affirmation_maps ( -- unique ID of this affirmation map affirmation_id INTEGER PRIMARY KEY AUTOINCREMENT, -- the weight of this affirmation map. "weight" is the number of affirmed anchor blocks @@ -253,12 +248,12 @@ CREATE TABLE affirmation_maps ( CREATE INDEX affirmation_maps_index ON affirmation_maps(affirmation_map); -- ensure anchor block uniqueness -CREATE TABLE anchor_blocks ( +CREATE TABLE IF NOT EXISTS anchor_blocks ( -- the nonnegative reward cycle number reward_cycle INTEGER PRIMARY KEY ); -CREATE TABLE block_commit_metadata ( +CREATE TABLE IF NOT EXISTS block_commit_metadata ( -- 32-byte hash of the burnchain block that contains this block-cmmit burn_block_hash TEXT NOT NULL, -- 32-byte hash of the transaction that contains this block-commit @@ -287,13 +282,13 @@ CREATE TABLE block_commit_metadata ( -- override the canonical affirmation map at the operator's discression. -- set values in this table only in an emergency -- such as when a hidden anchor block was mined, and the operator -- wants to avoid a deep Stacks blockchain reorg that would arise if the hidden anchor block was later disclosed. -CREATE TABLE overrides ( +CREATE TABLE IF NOT EXISTS overrides ( reward_cycle INTEGER PRIMARY KEY NOT NULL, affirmation_map TEXT NOT NULL ); -- database version -CREATE TABLE db_config(version TEXT NOT NULL); +CREATE TABLE IF NOT EXISTS db_config(version TEXT NOT NULL); -- empty affirmation map always exists, so foreign key relationships work INSERT INTO affirmation_maps(affirmation_id,weight,affirmation_map) VALUES (0,0,""); @@ -311,6 +306,39 @@ const BURNCHAIN_DB_INDEXES: &[&str] = &[ "CREATE INDEX IF NOT EXISTS index_block_commit_metadata_burn_block_hash_anchor_block ON block_commit_metadata(burn_block_hash,anchor_block);", ]; +// Required to drop old affirmation maps from Burnchain DB schema V2 and migrate to V3 +const BURNCHAIN_DB_MIGRATION_V2_TO_V3: &str = r#" + CREATE TABLE IF NOT EXISTS block_commit_metadata_new ( + burn_block_hash TEXT NOT NULL, + txid TEXT NOT NULL, + block_height INTEGER NOT NULL, + vtxindex INTEGER NOT NULL, + anchor_block INTEGER, + anchor_block_descendant INTEGER, + PRIMARY KEY(burn_block_hash, txid), + FOREIGN KEY(anchor_block) REFERENCES anchor_blocks(reward_cycle) + ); + + INSERT INTO block_commit_metadata_new (burn_block_hash, txid, block_height, vtxindex, anchor_block, anchor_block_descendant) + SELECT burn_block_hash, txid, block_height, vtxindex, anchor_block, anchor_block_descendant + FROM block_commit_metadata; + + DROP TABLE block_commit_metadata; + ALTER TABLE block_commit_metadata_new RENAME TO block_commit_metadata; + + DROP TABLE affirmation_maps; +"#; + +static SCHEMA_2: &[&str] = &[ + BURNCHAIN_DB_SCHEMA_2, + "INSERT INTO db_config (version) VALUES (2);", +]; + +static SCHEMA_3: &[&str] = &[ + BURNCHAIN_DB_MIGRATION_V2_TO_V3, + "INSERT INTO db_config (version) VALUES (3);", +]; + impl BurnchainDBTransaction<'_> { /// Store a burnchain block header into the burnchain database. /// Returns the row ID on success. @@ -385,8 +413,8 @@ impl BurnchainDBTransaction<'_> { fn insert_block_commit_metadata(&self, bcm: BlockCommitMetadata) -> Result<(), BurnchainError> { let commit_metadata_sql = "INSERT OR REPLACE INTO block_commit_metadata - (burn_block_hash, txid, block_height, vtxindex, anchor_block, anchor_block_descendant, affirmation_id) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"; + (burn_block_hash, txid, block_height, vtxindex, anchor_block, anchor_block_descendant) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)"; let mut stmt = self.sql_tx.prepare(commit_metadata_sql)?; let args = params![ bcm.burn_block_hash, @@ -395,7 +423,6 @@ impl BurnchainDBTransaction<'_> { bcm.vtxindex, opt_u64_to_sql(bcm.anchor_block)?, opt_u64_to_sql(bcm.anchor_block_descendant)?, - u64_to_sql(bcm.affirmation_id)?, ]; stmt.execute(args)?; Ok(()) @@ -431,7 +458,6 @@ impl BurnchainDBTransaction<'_> { block_height: opdata.block_height, vtxindex: opdata.vtxindex, // NOTE: these fields are filled in by the subsequent call. - affirmation_id: 0, anchor_block: None, anchor_block_descendant: None, }; @@ -450,57 +476,51 @@ impl BurnchainDBTransaction<'_> { &self.sql_tx } - pub fn get_canonical_chain_tip(&self) -> Result { - BurnchainDB::inner_get_canonical_chain_tip(&self.sql_tx) + pub fn rollback(self) -> Result<(), BurnchainError> { + self.sql_tx.rollback().map_err(BurnchainError::from) } - // TODO: add tests from mutation testing results #4837 - #[cfg_attr(test, mutants::skip)] - /// You'd only do this in network emergencies, where node operators are expected to declare an - /// anchor block missing (or present). Ideally there'd be a smart contract somewhere for this. - pub fn set_override_affirmation_map( - &self, - reward_cycle: u64, - affirmation_map: AffirmationMap, - ) -> Result<(), DBError> { - assert_eq!((affirmation_map.len() as u64) + 1, reward_cycle); - let qry = - "INSERT OR REPLACE INTO overrides (reward_cycle, affirmation_map) VALUES (?1, ?2)"; - let args = params![u64_to_sql(reward_cycle)?, affirmation_map.encode()]; - - let mut stmt = self.sql_tx.prepare(qry)?; - stmt.execute(args)?; - Ok(()) + pub fn execute_batch(&self, statement: &str) -> Result<(), BurnchainError> { + self.sql_tx + .execute_batch(statement) + .map_err(BurnchainError::from) } - pub fn clear_override_affirmation_map(&self, reward_cycle: u64) -> Result<(), DBError> { - let qry = "DELETE FROM overrides WHERE reward_cycle = ?1"; - let args = params![u64_to_sql(reward_cycle)?]; - - let mut stmt = self.sql_tx.prepare(qry)?; - stmt.execute(args)?; - Ok(()) + pub fn get_canonical_chain_tip(&self) -> Result { + BurnchainDB::inner_get_canonical_chain_tip(&self.sql_tx) } } impl BurnchainDB { - fn add_indexes(&mut self) -> Result<(), BurnchainError> { - let exists: i64 = query_row( - self.conn(), - "SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?1", - params![LAST_BURNCHAIN_DB_INDEX], - )? - .unwrap_or(0); - if exists == 0 { - let db_tx = self.tx_begin()?; - for index in BURNCHAIN_DB_INDEXES.iter() { - db_tx.sql_tx.execute_batch(index)?; - } - db_tx.commit()?; + /// The current schema version of the burnchain DB. + pub const SCHEMA_VERSION: u32 = 3; + + /// Returns the schema version of the database + fn get_schema_version(conn: &Connection) -> Result { + // If the db_config table doesn't exist, assume "version 1" as starting point + // (we don't have a schema 1, otherwise would start from 0) + if !table_exists(conn, "db_config")? { + return Ok(1); } - Ok(()) + + // Query all version values and get the max + let mut stmt = conn.prepare("SELECT version FROM db_config")?; + let max_version: u32 = stmt + .query_map([], |row| { + // Always read as String + row.get::<_, String>(0) + })? + .filter_map(Result::ok) + .filter_map(|v| v.parse::().ok()) + .max() + .unwrap_or(1); // default to 1 if table exists but empty + + Ok(max_version) } + /// Connect to a new `BurnchainDB` instance. + /// This will create a new SQLite database at the given path + /// or an in-memory database if the path is ":memory:" pub fn connect( path: &str, burnchain: &Burnchain, @@ -520,7 +540,7 @@ impl BurnchainDB { let ppath = Path::new(path); let pparent_path = ppath .parent() - .unwrap_or_else(|| panic!("BUG: no parent of '{}'", path)); + .unwrap_or_else(|| panic!("BUG: no parent of '{path}'")); fs::create_dir_all(&pparent_path) .map_err(|e| BurnchainError::from(DBError::IOError(e)))?; @@ -544,28 +564,104 @@ impl BurnchainDB { }; let conn = sqlite_open(path, open_flags, true)?; - let mut db = BurnchainDB { conn }; + debug!("Burnchain DB instantiated at {path}."); + let mut burnchain_db = Self { conn }; + burnchain_db.create_or_migrate(burnchain, readwrite, create_flag)?; - if create_flag { - let db_tx = db.tx_begin()?; - db_tx.sql_tx.execute_batch(BURNCHAIN_DB_SCHEMA)?; - db_tx.sql_tx.execute( - "INSERT INTO db_config (version) VALUES (?1)", - params![&BURNCHAIN_DB_VERSION], - )?; - - let first_block_header = BurnchainBlockHeader { - block_height: burnchain.first_block_height, - block_hash: burnchain.first_block_hash.clone(), - timestamp: burnchain.first_block_timestamp.into(), - num_txs: 0, - parent_block_hash: BurnchainHeaderHash::sentinel(), - }; + Ok(burnchain_db) + } + fn add_indexes(&mut self) -> Result<(), BurnchainError> { + let exists: i64 = query_row( + self.conn(), + "SELECT 1 FROM sqlite_master WHERE type = 'index' AND name = ?1", + params![LAST_BURNCHAIN_DB_INDEX], + )? + .unwrap_or(0); + if exists == 0 { + let db_tx = self.tx_begin()?; + for index in BURNCHAIN_DB_INDEXES.iter() { + db_tx.execute_batch(index)?; + } + db_tx.commit()?; + } + Ok(()) + } + + /// Either instantiate a new database, or migrate an existing one + fn create_or_migrate( + &mut self, + burnchain: &Burnchain, + readwrite: bool, + create_flag: bool, + ) -> Result<(), BurnchainError> { + let db_tx = self.tx_begin()?; + + let mut current_db_version = Self::get_schema_version(&db_tx.sql_tx)?; + debug!("Current Burnchain schema version: {}", current_db_version); + + for migration in MIGRATIONS.iter() { + if current_db_version >= migration.version { + // don't need this migration, continue to see if we need later migrations + continue; + } + if current_db_version != migration.version - 1 { + // This implies a gap or out-of-order migration definition, + // or the database is at a version X, and the next migration is X+2 instead of X+1. + db_tx.rollback()?; + return Err(BurnchainError::from(DBError::Other(format!( + "Migration step missing or out of order. Current DB version: {current_db_version}, trying to apply migration for version: {}", + migration.version + )))); + } debug!( - "Instantiate burnchain DB at {}. First block header is {:?}", - path, &first_block_header + "Applying SignerDB migration for schema version {}", + migration.version ); + for statement in migration.statements.iter() { + db_tx.execute_batch(statement)?; + } + + // Verify that the migration script updated the version correctly + let new_version_check = Self::get_schema_version(&db_tx.conn())?; + if new_version_check != migration.version { + db_tx.rollback()?; + return Err(BurnchainError::from(DBError::Other(format!( + "Migration to version {} failed to update DB version. Expected {}, got {new_version_check}.", + migration.version, migration.version + )))); + } + current_db_version = new_version_check; + debug!("Successfully migrated to schema version {current_db_version}"); + } + + match current_db_version.cmp(&Self::SCHEMA_VERSION) { + std::cmp::Ordering::Less => { + db_tx.rollback()?; + return Err(DBError::Other(format!( + "Database migration incomplete. Current version: {current_db_version}, SCHEMA_VERSION: {}", + Self::SCHEMA_VERSION + )).into()); + } + std::cmp::Ordering::Greater => { + db_tx.rollback()?; + return Err(DBError::Other(format!( + "Database schema is newer than SCHEMA_VERSION. SCHEMA_VERSION = {}, Current version = {current_db_version}. Did you forget to update SCHEMA_VERSION?", + Self::SCHEMA_VERSION + )).into()); + } + std::cmp::Ordering::Equal => {} + } + + let first_block_header = BurnchainBlockHeader { + block_height: burnchain.first_block_height, + block_hash: burnchain.first_block_hash.clone(), + timestamp: burnchain.first_block_timestamp.into(), + num_txs: 0, + parent_block_hash: BurnchainHeaderHash::sentinel(), + }; + if create_flag { + debug!("First block header is {first_block_header:?}"); db_tx.store_burnchain_db_entry(&first_block_header)?; let first_snapshot = BlockSnapshot::initial( @@ -578,18 +674,19 @@ impl BurnchainDB { txid: first_snapshot.winning_block_txid.clone(), block_height: first_snapshot.block_height, vtxindex: 0, - affirmation_id: 0, anchor_block: None, anchor_block_descendant: None, }; db_tx.insert_block_commit_metadata(first_snapshot_commit_metadata)?; - db_tx.commit()?; } + db_tx.commit()?; + if readwrite { - db.add_indexes()?; + self.add_indexes()?; } - Ok(db) + + Ok(()) } pub fn open(path: &str, readwrite: bool) -> Result { @@ -769,54 +866,6 @@ impl BurnchainDB { ops } - pub fn get_affirmation_map( - conn: &DBConn, - affirmation_id: u64, - ) -> Result, DBError> { - let sql = "SELECT affirmation_map FROM affirmation_maps WHERE affirmation_id = ?1"; - let args = params![&u64_to_sql(affirmation_id)?]; - query_row(conn, sql, args) - } - - pub fn get_affirmation_weight( - conn: &DBConn, - affirmation_id: u64, - ) -> Result, DBError> { - let sql = "SELECT weight FROM affirmation_maps WHERE affirmation_id = ?1"; - let args = params![&u64_to_sql(affirmation_id)?]; - query_row(conn, sql, args) - } - - pub fn get_affirmation_map_id( - conn: &DBConn, - affirmation_map: &AffirmationMap, - ) -> Result, DBError> { - let sql = "SELECT affirmation_id FROM affirmation_maps WHERE affirmation_map = ?1"; - let args = params![&affirmation_map.encode()]; - query_row(conn, sql, args) - } - - pub fn get_affirmation_map_id_at( - conn: &DBConn, - burn_header_hash: &BurnchainHeaderHash, - txid: &Txid, - ) -> Result, DBError> { - let sql = "SELECT affirmation_id FROM block_commit_metadata WHERE burn_block_hash = ?1 AND txid = ?2"; - let args = params![burn_header_hash, txid]; - query_row(conn, sql, args) - } - - pub fn get_block_commit_affirmation_id( - conn: &DBConn, - block_commit: &LeaderBlockCommitOp, - ) -> Result, DBError> { - BurnchainDB::get_affirmation_map_id_at( - conn, - &block_commit.burn_header_hash, - &block_commit.txid, - ) - } - pub fn is_anchor_block( conn: &DBConn, burn_header_hash: &BurnchainHeaderHash, @@ -985,7 +1034,7 @@ impl BurnchainDB { } Err(e) => { debug!( - "BurnchainDB Error {:?} finding PoX affirmation at {},{} in {:?}", + "BurnchainDB Error {:?} finding PoX at {},{} in {:?}", e, block_ptr, vtxindex, &header_hash ); return Ok(None); @@ -1030,188 +1079,4 @@ impl BurnchainDB { }, ) } - - /// Get the block-commit and block metadata for the anchor block with the heaviest affirmation - /// weight. - pub fn get_heaviest_anchor_block( - conn: &DBConn, - indexer: &B, - ) -> Result, DBError> { - let sql = "SELECT block_commit_metadata.* \ - FROM affirmation_maps JOIN block_commit_metadata ON affirmation_maps.affirmation_id = block_commit_metadata.affirmation_id \ - WHERE block_commit_metadata.anchor_block IS NOT NULL \ - ORDER BY affirmation_maps.weight DESC, block_commit_metadata.anchor_block DESC"; - - let mut stmt = conn.prepare(sql)?; - let mut rows = stmt.query(NO_PARAMS)?; - while let Some(row) = rows.next()? { - let metadata = BlockCommitMetadata::from_row(row)?; - - if let Some(block_header) = indexer.read_burnchain_header(metadata.block_height)? { - if block_header.block_hash != metadata.burn_block_hash { - continue; - } - - // this metadata is part of the indexer's burnchain fork - let commit = - BurnchainDB::get_block_commit(conn, &metadata.burn_block_hash, &metadata.txid)? - .expect("BUG: no block commit for existing metadata"); - return Ok(Some((commit, metadata))); - } - } - - return Ok(None); - } - - /// Find the affirmation map of the anchor block whose affirmation map is the heaviest. - /// In the event of a tie, pick the one from the anchor block of the latest reward cycle. - pub fn get_heaviest_anchor_block_affirmation_map( - conn: &DBConn, - burnchain: &Burnchain, - indexer: &B, - ) -> Result { - match BurnchainDB::get_heaviest_anchor_block(conn, indexer)? { - Some((_, metadata)) => { - let last_reward_cycle = burnchain - .block_height_to_reward_cycle(metadata.block_height) - .unwrap_or(0) - + 1; - - // is there an override set for this reward cycle? - if let Some(am) = - BurnchainDB::get_override_affirmation_map(conn, last_reward_cycle)? - { - warn!( - "Overriding heaviest affirmation map for reward cycle {} to {}", - last_reward_cycle, &am - ); - return Ok(am); - } - - let am = BurnchainDB::get_affirmation_map(conn, metadata.affirmation_id)? - .unwrap_or_else(|| { - panic!( - "BUG: failed to load affirmation map {}", - metadata.affirmation_id - ) - }); - - if cfg!(test) { - let _weight = - BurnchainDB::get_affirmation_weight(conn, metadata.affirmation_id)? - .unwrap_or_else(|| { - panic!( - "BUG: have affirmation map {} but no weight", - &metadata.affirmation_id - ) - }); - - test_debug!( - "Heaviest anchor block affirmation map is {:?} (ID {}, weight {})", - &am, - metadata.affirmation_id, - _weight - ); - } - Ok(am) - } - None => { - test_debug!("No anchor block affirmations maps"); - Ok(AffirmationMap::empty()) - } - } - } - - /// Load an overridden affirmation map. - /// You'd only do this in network emergencies, where node operators are expected to declare an - /// anchor block missing (or present). Ideally there'd be a smart contract somewhere for this. - pub fn get_override_affirmation_map( - conn: &DBConn, - reward_cycle: u64, - ) -> Result, DBError> { - let am_opt: Option = query_row_panic( - conn, - "SELECT affirmation_map FROM overrides WHERE reward_cycle = ?1", - params![u64_to_sql(reward_cycle)?], - || "BUG: more than one override affirmation map for the same reward cycle".to_string(), - )?; - if let Some(am) = &am_opt { - assert_eq!((am.len() + 1) as u64, reward_cycle); - } - Ok(am_opt) - } - - /// Get the canonical affirmation map. This is the heaviest anchor block affirmation map, but - /// accounting for any subsequent reward cycles whose anchor blocks either aren't on the - /// heaviest anchor block affirmation map, or which have no anchor blocks. - pub fn get_canonical_affirmation_map( - conn: &DBConn, - burnchain: &Burnchain, - indexer: &B, - mut unconfirmed_oracle: F, - ) -> Result - where - B: BurnchainHeaderReader, - F: FnMut(LeaderBlockCommitOp, BlockCommitMetadata) -> bool, - { - let canonical_tip = - BurnchainDB::inner_get_canonical_chain_tip(conn).map_err(|e| match e { - BurnchainError::DBError(dbe) => dbe, - _ => DBError::Other(format!("Burnchain error: {:?}", &e)), - })?; - - let last_reward_cycle = burnchain - .block_height_to_reward_cycle(canonical_tip.block_height) - .unwrap_or(0) - + 1; - - // is there an override set for this reward cycle? - if let Some(am) = BurnchainDB::get_override_affirmation_map(conn, last_reward_cycle)? { - warn!( - "Overriding heaviest affirmation map for reward cycle {} to {}", - last_reward_cycle, &am - ); - return Ok(am); - } - - let mut heaviest_am = - BurnchainDB::get_heaviest_anchor_block_affirmation_map(conn, burnchain, indexer)?; - let start_rc = (heaviest_am.len() as u64) + 1; - - test_debug!( - "Add reward cycles {}-{} to heaviest anchor block affirmation map {}", - start_rc, - last_reward_cycle, - &heaviest_am - ); - for rc in start_rc..last_reward_cycle { - if let Some(metadata) = - BurnchainDB::get_canonical_anchor_block_commit_metadata(conn, indexer, rc)? - { - let bhh = metadata.burn_block_hash.clone(); - let txid = metadata.txid.clone(); - let commit = BurnchainDB::get_block_commit(conn, &bhh, &txid)? - .expect("FATAL: have block-commit metadata but not block-commit"); - let present = unconfirmed_oracle(commit, metadata); - if present { - debug!( - "Assume present unaffirmed anchor block {} txid {} at reward cycle {}", - &bhh, &txid, rc - ); - heaviest_am.push(AffirmationMapEntry::PoxAnchorBlockPresent); - } else { - debug!( - "Assume absent unaffirmed anchor block {} txid {} at reward cycle {}", - &bhh, &txid, rc - ); - heaviest_am.push(AffirmationMapEntry::PoxAnchorBlockAbsent); - } - } else { - debug!("Assume no anchor block at reward cycle {}", rc); - heaviest_am.push(AffirmationMapEntry::Nothing); - } - } - - Ok(heaviest_am) - } } From 838d3090856baa21fe34aef3ca98e3e57ec082b7 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 3 Sep 2025 13:42:52 -0700 Subject: [PATCH 31/35] Remove remaining references to affirmation maps throughout code base and update changelog Signed-off-by: Jacinta Ferrant --- CHANGELOG.md | 2 +- .../examples/node-info.example.json | 6 - .../components/schemas/node-info.schema.yaml | 15 - stacks-node/src/tests/epoch_21.rs | 24 - stacks-node/src/tests/epoch_22.rs | 1 - stackslib/src/burnchains/affirmation.rs | 1160 ---------------- stackslib/src/burnchains/mod.rs | 1 - stackslib/src/burnchains/tests/affirmation.rs | 1194 ----------------- stackslib/src/burnchains/tests/mod.rs | 2 - stackslib/src/chainstate/burn/db/sortdb.rs | 180 ++- 10 files changed, 180 insertions(+), 2405 deletions(-) delete mode 100644 stackslib/src/burnchains/affirmation.rs delete mode 100644 stackslib/src/burnchains/tests/affirmation.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f7db5d27c..91d353eae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - Clarity errors pertaining to syntax binding errors have been made more expressive (#6337) - +- Removed affirmation maps logic throughout, upgrading chainstate DB schema to 11 and burnchain DB schema to 3 (#6314) ## [3.2.0.0.1] diff --git a/docs/rpc/components/examples/node-info.example.json b/docs/rpc/components/examples/node-info.example.json index 9cb48475b9..fa8ae8dd74 100644 --- a/docs/rpc/components/examples/node-info.example.json +++ b/docs/rpc/components/examples/node-info.example.json @@ -17,12 +17,6 @@ "is_fully_synced": false, "node_public_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01", "node_public_key_hash": "1328af7c4f96eee8fdbb1e023e48ba11e255d1c8", - "affirmations": { - "heaviest": "", - "stacks_tip": "", - "sortition_tip": "", - "tentative_best": "" - }, "last_pox_anchor": { "anchor_block_hash": "47c20352492e1647127d6f6275fa5abfe90157f6a64e6a175ff45ed0c1fb3f4e", "anchor_block_txid": "5b5d678c658525122fa73a8699b5810325ce9412bf0dc3762fb7518699be3d4d" diff --git a/docs/rpc/components/schemas/node-info.schema.yaml b/docs/rpc/components/schemas/node-info.schema.yaml index 1abd48d3de..b93af1761a 100644 --- a/docs/rpc/components/schemas/node-info.schema.yaml +++ b/docs/rpc/components/schemas/node-info.schema.yaml @@ -88,21 +88,6 @@ properties: node_public_key_hash: type: [string, "null"] description: The HASH160 of the node's public key. - affirmations: - type: [object, "null"] - properties: - heaviest: - type: string - description: Encoded affirmation map string. - stacks_tip: - type: string - description: Encoded affirmation map string. - sortition_tip: - type: string - description: Encoded affirmation map string. - tentative_best: - type: string - description: Encoded affirmation map string. last_pox_anchor: type: [object, "null"] properties: diff --git a/stacks-node/src/tests/epoch_21.rs b/stacks-node/src/tests/epoch_21.rs index 957b73ee5d..5c80fc1908 100644 --- a/stacks-node/src/tests/epoch_21.rs +++ b/stacks-node/src/tests/epoch_21.rs @@ -2289,8 +2289,6 @@ fn test_pox_reorgs_three_flaps() { for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Tip for miner {i}: {tip_info:?}"); - - //assert_eq!(tip_info.affirmations.heaviest, AffirmationMap::decode("nnnnnnnnnnnnnnnnnnnnp").unwrap()); } info!("####################### end of cycle ##############################"); @@ -2405,8 +2403,6 @@ fn test_pox_reorgs_three_flaps() { for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Tip for miner {i}: {tip_info:?}"); - - // miner 0 may have won here, but its affirmation map isn't yet the heaviest. } info!("####################### end of cycle ##############################"); @@ -2432,8 +2428,6 @@ fn test_pox_reorgs_three_flaps() { for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Tip for miner {i}: {tip_info:?}"); - - // miner 0's affirmation map now becomes the heaviest. } info!("####################### end of cycle ##############################"); @@ -2461,7 +2455,6 @@ fn test_pox_reorgs_three_flaps() { let tip_info = get_chain_info(c); info!("Tip for miner {i}: {tip_info:?}"); - // miner 0's affirmation map is now the heaviest, and there's no longer a tie. max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); } info!("####################### end of cycle ##############################"); @@ -2483,7 +2476,6 @@ fn test_pox_reorgs_three_flaps() { eprintln!("Wait for all blocks to propagate; max tip is {max_stacks_tip}"); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on affirmation maps for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); @@ -2885,7 +2877,6 @@ fn test_pox_reorg_one_flap() { eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on stacks affirmation map for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); @@ -2894,7 +2885,6 @@ fn test_pox_reorg_one_flap() { /// PoX reorg tests where two miners take turn mining hidden anchor blocks. /// Both miners mine in the reward phase, and in doing so, confirm their hidden anchor blocks. -/// The heaviest affirmation map grows as n+pppa+ #[test] #[ignore] fn test_pox_reorg_flap_duel() { @@ -3206,14 +3196,10 @@ fn test_pox_reorg_flap_duel() { for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Tip for miner {i}: {tip_info:?}"); - - //assert_eq!(tip_info.affirmations.heaviest, AffirmationMap::decode("nnnnnnnnnnnnnnnnnnnnp").unwrap()); } info!("####################### end of cycle ##############################"); // prevent Stacks at these heights from propagating. - // This means that both nodes affirm the absence of each others' anchor blocks, and the - // heaviest affirmation map will always look like n+pppa+ env::set_var( "STACKS_HIDE_BLOCKS_AT_HEIGHT", "[226,227,228,229,230,236,237,238,239,240,246,247,248,249,250,256,257,258,259,260,266,267,268,269,270,276,277,278,279,280,286,287,288,289,290]" @@ -3295,13 +3281,9 @@ fn test_pox_reorg_flap_duel() { env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); // wait for all blocks to propagate - // NOTE: the stacks affirmation maps will differ from the heaviest affirmation map, because the - // act of flapping back and forth so much will have caused these nodes to forget about some of - // their anchor blocks. This is an artifact of the test. eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on stacks affirmation map for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); @@ -3698,13 +3680,9 @@ fn test_pox_reorg_flap_reward_cycles() { env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); // wait for all blocks to propagate - // NOTE: the stacks affirmation maps will differ from the heaviest affirmation map, because the - // act of flapping back and forth so much will have caused these nodes to forget about some of - // their anchor blocks. This is an artifact of the test. eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on stacks affirmation map for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); @@ -4079,7 +4057,6 @@ fn test_pox_missing_five_anchor_blocks() { info!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}",); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on stacks affirmation map for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); @@ -4503,7 +4480,6 @@ fn test_sortition_divergence_pre_21() { info!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on stacks affirmation map for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); diff --git a/stacks-node/src/tests/epoch_22.rs b/stacks-node/src/tests/epoch_22.rs index 8ac2407d87..d0d4c0070f 100644 --- a/stacks-node/src/tests/epoch_22.rs +++ b/stacks-node/src/tests/epoch_22.rs @@ -1626,7 +1626,6 @@ fn test_pox_reorg_one_flap() { eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - // nodes now agree on stacks affirmation map for (i, c) in confs.iter().enumerate() { let tip_info = get_chain_info(c); info!("Final tip for miner {i}: {tip_info:?}"); diff --git a/stackslib/src/burnchains/affirmation.rs b/stackslib/src/burnchains/affirmation.rs deleted file mode 100644 index 79d986fda1..0000000000 --- a/stackslib/src/burnchains/affirmation.rs +++ /dev/null @@ -1,1160 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2021 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -/// ## The Problem -/// -/// There were two related design flaws in the way the Stacks blockchain deals with PoX anchor blocks: -/// -/// * If it is ever the case in which a PoX anchor block is missing, and yet somehow manages to achieve 80% or more -/// confirmations during the prepare phase, then the subsequent arrival of that anchor block will cause a _deep_ chain -/// reorg. It doesn't matter how many future blocks get mined -- if the anchor block is later revealed, it will -/// invalidate all of the blocks that did not build on it. While mining and confirming an anchor block is very costly, -/// it's not only possible, but profitable: anyone who manages to do this could hold the blockchain for ransom by -/// threatening to disclose the anchor block and invaldiate all blocks after it unless they were paid not to (i.e. in -/// perpetuity). -/// -/// * If it is ever the case that not enough STX get locked for PoX to begin in reward cycle _R_, then a node that -/// processes Stacks blocks first without the anchor block in _R_ and then with the anchor block in _R_ will crash -/// because it will attempt to calculate the same sortition twice. This is because the same block-commits would be -/// processed in both cases -- they'd both be PoB commits. -/// -/// This subsystem fixes both problems by making the _history of anchor blocks itself_ forkable, and by implementing -/// _Nakamoto consensus_ on the anchor block history forks so that there will always be a canonical anchor block -/// history. In doing so, the Stacks blockchain now has _three_ levels of forks: the Bitcoin chain, the history of PoX -/// anchor blocks, and the history of Stacks blocks. The canonical Stacks fork is the longest history of Stacks blocks -/// that passes through the canonical history of anchor blocks which resides on the canonical Bitcoin chain. -/// -/// ## Background: Sortition Histories -/// -/// Recall that each Bitcoin block can contain block-commits that are valid only if certain anchor blocks are known to -/// the node, and invalid if other anchor blocks are known. Specifically, a block-commit can be a valid PoX -/// block-commit _only if_ the current reward cycle has an anchor block, _and_ that anchor block is known to the node. -/// Otherwise, if the block-commit does not descend from the anchor block, or there is no anchor block for this reward -/// cycle, then the block-commit can only be valid if it's a PoB block-commit. -/// -/// What this means is that there is a _set_ of sortition histories on the Bitcoin chainstate that will each yield a -/// unique history of block-commits (which in turn represent a unique set of possible Stacks forks). This set has -/// _O(2**n)_ members, where _n_ is the number of reward cycles that have anchor blocks. This is because each time a -/// new reward cycle is processed with an anchor block, there will be a sortition history that descends from it in which -/// the anchor block is known to the node, and a sortition history in which it is _not_ known. -/// -/// Which sortition history is the "true" sortition history, and how do we determine this? This is what this subsystem -/// addresses. -/// -/// ## Solution: Weight Sortition Histories by Miner Affirmations -/// -/// Can we deduce whether or not an anchor block _should_ exist and be known to the network, using only Bitcoin -/// chainstate? A likely anchor block's block-commit will have at least 80 confirmations in the prepare phase -- at -/// least F*w (i.e. 80) Bitcoin blocks will contain at least one block-commit that has the likely anchor block-commit as -/// an ancestor. -/// -/// Of course, there are competing block-commits in each Bitcoin block; only one will be chosen as the Stacks block. -/// But, recall that in the prepare phase of a reward cycle, all miners must burn BTC. Because miners are sending BTC -/// to the burn address, you can _compare_ the economic worth of all block-commits within a prepare-phase block. -/// Moreover, you can calculate how much BTC went into confirming a likely anchor block's block-commit. In doing so, we -/// can introduce an extra criterion for selecting the anchor block in a reward cycle: -/// -/// **The PoX anchor block for reward cycle _R_ is a Stacks block that has not yet been chosen to be an anchor block, -/// and is the highest block outside _R_'s prepare phase that has at least F*w confirmations and is confirmed by the -/// most BTC burnt.** -/// -/// This is slightly different than the definition in SIP-007. We're only looking at block-commits now. If there are -/// two or more reward-phase block-commits that got F*w confirmations, then we select the block-commit that got the most -/// BTC. If this block-commit doesn't actually correspond to a Stacks block, then there is no anchor block for the -/// reward cycle. Also, if this block-commit has been an anchor block before in some prior reward cycle, then there is -/// no anchor block for this reward cycle. If Stacks miners are honest, and no Stacks miner has more than 80% of the -/// mining power, then neither of these two cases arise -- Stacks miners will build Stacks blocks on top of blocks they -/// know about, and their corresponding block-commits in the prepare-phase will confirm the block-commit for an anchor -/// block the miners believe exists. -/// -/// The key insight into understanding the solution to #1805 is to see that the act of choosing an anchor block is -/// _also_ the acts of doing the following two things: -/// -/// * Picking a likely anchor block-commit is the act of _affirming_ that the anchor block is known to the network. A -/// bootstrapping node does not know which Stacks blocks actually exist, since it needs to go and actually download -/// them. But, it can examine only the Bitcoin chainstate and deduce the likely anchor block for each reward cycle. If -/// a reward cycle has a likely anchor block-commit, then we say that the set of miners who mined that prepare-phase -/// have _affirmed_ to this node and all future bootstrapping nodes that they believed that this anchor block exists. I -/// say "affirmed" because it's a weaker guarantee than "confirmed" -- the anchor block can still get lost after the -/// miners make their affirmations. -/// -/// * Picking a likely anchor block-commit is the act of affirming all of the previous affirmations that this anchor -/// block represents. An anchor block is a descendant of a history of prior anchor blocks, so miners affirming that it -/// exists by sending block-commits that confirm its block-commit is also the act of miners affirming that all of the -/// ancestor anchor blocks it confirms also exist. For example, if there are 4 reward cycles, and cycles 1, 2, and 3 -/// have anchor blocks, then the act of miners choosing an anchor block in reward cycle 4's prepare phase that descends -/// from the anchor block in reward cycle 3 is _also_ the act of affirming that the anchor block for reward cycle 3 -/// exists. If the anchor block for reward cycle 3 descends from the anchor block of reward cycle 1, but _not_ from the -/// anchor block in reward cycle 2, then the miners have also affirmed that the anchor block for reward cycle 1 exists. -/// Moreover, the anchor block in reward cycle 1 has been affirmed _twice_ -- both by the miners in reward cycle 3's -/// prepare phase, and the miners in reward cycle 4's prepare phase. The anchor block in reward cycle 2 has _not_ been -/// affirmed. -/// -/// The act of building anchor blocks on top of anchor blocks gives us a way to _weight_ the corresponding sortition -/// histories. An anchor block gets "heavier" as the number of descendant anchor blocks increases, and as the number of -/// reward cycles without anchor blocks increases. This is because in both cases, miners are _not_ working on an anchor -/// block history that would _invalidate_ this anchor block -- i.e. they are continuously affirming that this anchor -/// block exists. -/// -/// We can define the weight of a sortition history as the weight of its heaviest anchor block. If you want to produce -/// a sortition history that is heavier, but invalidates the last _N_ anchor blocks, you'll have to mine at least _N + -/// 1_ reward cycles. This gets us a form of Nakamoto consensus for the status of anchor blocks -- the more affirmed an -/// anchor block is, the harder it is to get it unaffirmed. By doing this, we address the first problem with PoX anchor -/// blocks: in order to hold the chain hostage, you have to _continuously_ mine reward cycles that confirm your missing -/// anchor block. -/// -/// ## Implementation: Affirmation Maps -/// -/// We track this information through a data structure called an **affirmation map**. An affirmation map has the -/// following methods: -/// -/// * `at(i)`: Determine the network's affirmation status of the anchor block for the _ith_ reward cycle, starting at -/// reward cycle 1 (reward cycle 0 has no anchor block, ever). The domain of `i` is defined as the set of reward cycles -/// known to the node, excluding 0, and evaluates to one of the following: -/// -/// * `p`: There is an anchor block, and it's present -/// * `a`: There is an anchor block, and it's absent -/// * `n`: There is no anchor block -/// -/// * `weight()`: This returns the maximum number of anchor blocks that descend from an anchor block this affirmation -/// map represents -/// -/// Each block-commit represents an affirmation by the miner about the state of the anchor blocks that the -/// block-commit's Stacks block confirms. When processing block-commits, the node will calculate the affirmation map -/// for each block-commit inductively as follows: -/// -/// * If the block-commit is in the prepare phase for reward cycle _R_: -/// -/// * If there is an anchor block for _R_: -/// -/// * If this commit descends from the anchor block, then its affirmation map is the same as the anchor -/// block's, plus having `at(R)` set to `p` -/// -/// * Otherwise, its affirmation map the same as the anchor block's, plus having `at(R)`set to `a` -/// -/// * Otherwise: -/// -/// * If the parent descended from some anchor block at reward cycle _R - k_ then this commit's affirmation -/// map is the same as its parent, plus having `at(R - k)` set to `p`, plus having all `at(R - k < x < R)` -/// set to `n` if reward cycle _x_ doesn't have an anchor block, and `a` if it does. -/// -/// * Otherwise, this commit's affirmation map is defined as `at(x)` set to `n` if reward cycle _x_ doesn't -/// have an anchor block, and `a` if it does. -/// -/// * Otherwise: -/// -/// * If the parent descended from some anchor block in reward cycle _R - k_, then this commit's affirmation map -/// is the same as its parent, plus having `at(R - k < x < R)` set to `n` if reward cycle _x_ doesn't have an -/// anchor block, and `a` if it does. -/// -/// * Otherwise, this commit's affirmation map is defined as `at(x)` set to `n` if reward cycle _x_ doesn't have -/// an anchor block, and `a` if it does. -/// -/// Consider the example above, where we have anchor block histories 1,3,4 and 1,2. -/// -/// * A block-commit in the prepare-phase for reward cycle 4 that confirms the anchor block for reward cycle 4 would -/// have affirmation map `papp`, because it affirms that the anchor blocks for reward cycles 1, 3, and 4 exist. -/// -/// * A block-commit in the prepare-phase for reward cycle 4 that does NOT confirm the anchor block for reward cycle 4, but -/// descends from a block that descends from the anchor block in reward cycle 3, would have the affirmation map `papa`, -/// because it does NOT affirm that the anchor block for reward cycle 4 exists, but it DOES affirm that the anchor block -/// history terminating at the anchor block for reward cycle 3 exists. -/// -/// * A block-commit in the prepare-phase for reward cycle 4 that descends from a block that descends from the anchor block -/// for reward cycle 2 would have affirmation map `ppaa`, because it builds on the anchor block for reward cycle 2, but it -/// doesn't build on the anchor blocks for 3 and 4. -/// -/// * Suppose reward cycle 5 rolls around, and no anchor block is chosen at all. Then, a block in the reward -/// phase for reward cycle 5 that builds off the anchor block in reward cycle 4 would have affirmation map `pappn`. -/// Similarly, a block in reward cycle 5's reward phase that builds off of the anchor block in reward cycle 2 would have -/// affirmation map `ppaan`. -/// -/// (Here's a small lemma: if any affirmation map has `at(R) = n` for a given reward cycle `R`, then _all_ affirmation -/// maps will have `at(R) == n`). -/// -/// Now that we have a way to measure affirmations on anchor blocks, we can use them to deduce a canonical sortition -/// history as simply the history that represents the affirmation map with the highest `weight()` value. If there's a -/// tie, then we pick the affirmation map with the highest `i` such that `at(i) = p` (i.e. a later anchor block -/// affirmation is a stronger affirmation than an earlier one). This is always a tie-breaker, because each -/// prepare-phase either affirms or does not affirm exactly one anchor block. -/// -/// ### Using Affirmation Maps -/// -/// Each time we finish processing a reward cycle, the burnchain processor identifies the anchor block's commit and -/// updates the affirmation maps for the prepare-phase block-commits in the burnchain DB (now that an anchor block -/// decision has been made). As the DB receives subsequent reward-phase block-commits, their affirmation maps are -/// calculated using the above definition. -/// -/// Each time the chains coordinator processes a burnchain block, it sees if its view of the heaviest affirmation map -/// has changed. If so, it executes a PoX reorg like before -- it invalidates the sortitions back to the latest -/// sortition that is represented on the now-heaviest affirmation map. Unlike before, it will _re-validate_ any -/// sortitions that it has processed in the past if a _prefix_ of the now-heaviest affirmation map has been the heaviest -/// affirmation map in the past. This can arise if there are two competing sets of miners that are fighting over two -/// different sortition histories. In this case, it also forgets the orphaned statuses of all invalidated and -/// re-validated Stacks blocks, so they can be downloaded and applied again to the Stacks chain state (note that a -/// Stacks block will be applied at most once in any case -- it's just that it can be an orphan on one sortition -/// history, but a valid and accepted block in another). -/// -/// Because we take care to re-validate sortitions that have already been processed, we avoid the second design flaw in -/// the PoX anchor block handling -- a sortition will always be processed at most once. This is further guaranteed by -/// making sure that the consensus hash for each sortition is calculated in part from the PoX bit vector that is -/// _induced_ by the heaviest affirmation map. That is, the node's PoX ID is no longer calculated from the presence or -/// absence of anchor blocks, but instead calculated from the heaviest affirmation map as follows: -/// -/// * If `at(i)` is `p` or `n`, then bit `i` is 1 -/// * Otherwise, bit `i` is 0 -/// -/// In addition, when a late anchor block arrives and is processed by the chains coordinator, the heaviest affirmation -/// map is consulted to determine whether or not it _should_ be processed. If it's _not_ affirmed, then it is ignored. -/// -/// ## Failure Recovery -/// -/// In the event that a hidden anchor block arises, this subsystem includes a way to _override_ the heaviest affirmation -/// map for a given reward cycle. If an anchor block is missing, miners can _declare_ it missing by updating a row in -/// the burnchain DB that marks the anchor block as forever missing. This prevents a "short" (but still devastating) -/// reorg whereby an anchor block is missing for _almost_ the duration of the reward cycle -- in such a case, the -/// absence of this declaration would cause the reward cycle's blocks to all be invalidated. Adding this declaration, -/// and then mining an anchor block that does _not_ affirm the missing anchor block would solve this for future -/// bootstrapping nodes. -/// -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::fmt; -use std::fmt::Write; - -use serde::de::Error as de_Error; -use serde::{Deserialize, Serialize}; - -use crate::burnchains::db::{BurnchainDB, BurnchainDBTransaction, BurnchainHeaderReader}; -use crate::burnchains::{Burnchain, Error, PoxConstants}; -use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; -use crate::chainstate::burn::operations::{BlockstackOperationType, LeaderBlockCommitOp}; -use crate::util_lib::db::Error as DBError; - -/// Affirmation map entries. By building on a PoX-mined block, -/// a PoB-mined block (in a PoX reward cycle), -/// or no block in reward cycle _i_, a sortition's miner -/// affirms something about the status of the ancestral anchor blocks. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub enum AffirmationMapEntry { - PoxAnchorBlockPresent, - PoxAnchorBlockAbsent, - Nothing, -} - -impl AffirmationMapEntry { - pub fn from_chr(c: char) -> Option { - match c { - 'p' => Some(AffirmationMapEntry::PoxAnchorBlockPresent), - 'a' => Some(AffirmationMapEntry::PoxAnchorBlockAbsent), - 'n' => Some(AffirmationMapEntry::Nothing), - _ => None, - } - } -} - -/// An affirmation map is simply a list of affirmation map entries. This struct merely wraps the -/// list behind accessor and mutator methods. -#[derive(Clone, PartialEq)] -pub struct AffirmationMap { - pub affirmations: Vec, -} - -impl fmt::Display for AffirmationMapEntry { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - AffirmationMapEntry::PoxAnchorBlockPresent => write!(f, "p"), - AffirmationMapEntry::PoxAnchorBlockAbsent => write!(f, "a"), - AffirmationMapEntry::Nothing => write!(f, "n"), - } - } -} - -impl fmt::Debug for AffirmationMapEntry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&format!("{}", &self)) - } -} - -impl fmt::Display for AffirmationMap { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - for entry in self.affirmations.iter() { - write!(f, "{}", &entry)?; - } - Ok(()) - } -} - -impl fmt::Debug for AffirmationMap { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self) - } -} - -impl Serialize for AffirmationMap { - fn serialize(&self, s: S) -> Result { - let am_str = self.encode(); - s.serialize_str(am_str.as_str()) - } -} - -impl<'de> Deserialize<'de> for AffirmationMap { - fn deserialize>(d: D) -> Result { - let am_str = String::deserialize(d)?; - let am = AffirmationMap::decode(&am_str).ok_or(de_Error::custom( - "Failed to decode affirmation map".to_string(), - ))?; - Ok(am) - } -} - -/// The pointer to the PoX anchor block in the burnchain -pub struct PoxAnchorPtr { - /// height of the block - pub block_height: u64, - /// index in the block - pub vtxindex: u32, - /// how any tokens burnt to create it - pub burnt: u64, - /// number of confirmations it received - pub confs: u64, -} - -impl AffirmationMap { - pub fn new(entries: Vec) -> AffirmationMap { - AffirmationMap { - affirmations: entries, - } - } - - pub fn empty() -> AffirmationMap { - AffirmationMap { - affirmations: vec![], - } - } - - pub fn at(&self, reward_cycle: u64) -> Option<&AffirmationMapEntry> { - self.affirmations.get(reward_cycle as usize) - } - - pub fn push(&mut self, entry: AffirmationMapEntry) { - self.affirmations.push(entry) - } - - #[cfg_attr(test, mutants::skip)] - pub fn pop(&mut self) -> Option { - self.affirmations.pop() - } - - pub fn len(&self) -> usize { - self.affirmations.len() - } - - pub fn is_empty(&self) -> bool { - self.affirmations.is_empty() - } - - pub fn as_slice(&self) -> &[AffirmationMapEntry] { - &self.affirmations - } - - // used to store to database - pub fn encode(&self) -> String { - let mut ret = String::with_capacity(self.affirmations.len()); - write!(&mut ret, "{}", self).expect("BUG: failed to serialize affirmations -- likely OOM"); - ret - } - - // used for database from-row - pub fn decode(s: &str) -> Option { - if !s.is_ascii() { - return None; - } - - let mut affirmations = Vec::with_capacity(s.len()); - for chr in s.chars() { - if let Some(next) = AffirmationMapEntry::from_chr(chr) { - affirmations.push(next); - } else { - return None; - } - } - Some(AffirmationMap { affirmations }) - } - - /// Has `other` diverged from `self`? - /// If `other` contains a reward cycle affirmation that is not present in `self`, then yes. - /// (Note that this means that if `other` is a prefix of `self`, then no divergence). - /// Return the index into `other` where the affirmation differs from `self`. - /// Return `None` if no difference exists. - pub fn find_divergence(&self, other: &AffirmationMap) -> Option { - for (i, (self_aff, other_aff)) in self - .affirmations - .iter() - .zip(other.affirmations.iter()) - .enumerate() - { - if self_aff != other_aff { - return Some(i as u64); - } - } - - if other.len() > self.len() { - return Some(self.len() as u64); - } - - None - } - - /// At what reward cycle should a node start searching for block inventories, given the heaviest - /// affirmation map?. This is the lowest reward cycle in which both self and heaviest affirm - /// "absent" that comes _after_ the highest reward cycle in which both self and heaviest affirm - /// "present". - /// - /// For `paa` and `pap`, it's 1 - /// For `paap` and `paap`, it's 3 - /// For `papa` and `apap`, it's 0 - /// For `paapapap` and `paappapa`, it's 4 - /// For `aaaaa` and `aaaaa`, it's 0. - /// For `ppppp` and `ppppp`, it's 4. - pub fn find_inv_search(&self, heaviest: &AffirmationMap) -> u64 { - let mut highest_p = None; - for (i, (self_aff, heaviest_aff)) in self - .affirmations - .iter() - .zip(heaviest.affirmations.iter()) - .enumerate() - { - if self_aff == heaviest_aff && *self_aff == AffirmationMapEntry::PoxAnchorBlockPresent { - highest_p = Some(i); - } - } - let Some(highest_p) = highest_p else { - // no agreement on any anchor block - return 0; - }; - let remainder_self = self.affirmations.iter().skip(highest_p); - let remainder_heaviest = heaviest.affirmations.iter().skip(highest_p); - for (i, (self_aff, heaviest_aff)) in remainder_self.zip(remainder_heaviest).enumerate() { - let inv_index = u64::try_from(i.saturating_add(highest_p)) - .expect("FATAL: usize is larger than u64. Unsupported system type"); - if self_aff == heaviest_aff && *self_aff == AffirmationMapEntry::PoxAnchorBlockAbsent { - return inv_index; - } - if self_aff != heaviest_aff { - return inv_index; - } - } - u64::try_from(highest_p).expect("FATAL: usize is larger than u64. Unsupported system type") - } - - /// Is `other` a prefix of `self`? - /// Returns true if so; false if not - pub fn has_prefix(&self, prefix: &AffirmationMap) -> bool { - let Some(self_prefix) = self.affirmations.get(..prefix.len()) else { - return false; - }; - - if self_prefix.len() != prefix.len() { - return false; - } - - self_prefix == &prefix.affirmations - } - - /// What is the weight of this affirmation map? - /// i.e. how many times did the network either affirm an anchor block, or make no election? - pub fn weight(&self) -> u64 { - let mut weight = 0; - for affirmation in self.affirmations.iter() { - match affirmation { - AffirmationMapEntry::PoxAnchorBlockAbsent => {} - _ => { - weight += 1; - } - } - } - weight - } -} - -/// Get a parent/child reward cycle. Only return Some(..) if the reward cycle is known for both -- -/// i.e. their block heights are plausible -- they are at or after the first burnchain block -/// height. -pub fn get_parent_child_reward_cycles( - parent: &LeaderBlockCommitOp, - block_commit: &LeaderBlockCommitOp, - burnchain: &Burnchain, -) -> Option<(u64, u64)> { - let child_reward_cycle = match burnchain.block_height_to_reward_cycle(block_commit.block_height) - { - Some(crc) => crc, - None => return None, - }; - - let parent_reward_cycle = match burnchain.block_height_to_reward_cycle(parent.block_height) { - Some(prc) => prc, - None => { - if parent.block_height == 0 && parent.vtxindex == 0 { - // this is a first block commit - 0 - } else { - return None; - } - } - }; - - test_debug!( - "{},{} is rc={},rc={}", - parent.block_height, - block_commit.block_height, - parent_reward_cycle, - child_reward_cycle - ); - Some((parent_reward_cycle, child_reward_cycle)) -} - -/// Read a range of blockstack operations for a prepare phase of a given reward cycle. -/// Only includes block-commits. -/// The returned vec is a vec of vecs of block-commits in block order. The ith item is a vec of -/// block-commits in block order for the ith prepare-phase block (item 0 is the first prepare-phase -/// block's block-commits). -pub fn read_prepare_phase_commits( - burnchain_tx: &BurnchainDBTransaction, - indexer: &B, - pox_consts: &PoxConstants, - first_block_height: u64, - reward_cycle: u64, -) -> Result>, Error> { - // start and end heights of the prepare phase for this reward cycle - let start_height = pox_consts - .reward_cycle_to_block_height(first_block_height, reward_cycle + 1) - - (pox_consts.prepare_length as u64); - let end_height = start_height + (pox_consts.prepare_length as u64); - let headers = indexer.read_burnchain_headers(start_height, end_height)?; - let _num_headers = headers.len(); - - let mut ret = vec![]; - for header in headers.into_iter() { - let blk = BurnchainDB::get_burnchain_block(burnchain_tx.conn(), &header.block_hash) - .unwrap_or_else(|_| { - panic!( - "BUG: failed to load prepare-phase block {} ({})", - &header.block_hash, header.block_height - ) - }); - - let mut block_ops = vec![]; - for op in blk.ops.into_iter() { - assert!(pox_consts.is_in_prepare_phase(first_block_height, op.block_height())); - match op { - BlockstackOperationType::LeaderBlockCommit(opdata) => { - // basic validity filtering - if opdata.block_height < first_block_height { - test_debug!("Skip too-early block commit"); - continue; - } - // the block commit's parent must be a burnchain block that is evaluated by the node - // blocks that are <= first_block_height do not meet this requirement. - if (opdata.parent_block_ptr as u64) <= first_block_height { - test_debug!("Skip orphaned block-commit"); - continue; - } - if opdata.block_height <= opdata.parent_block_ptr as u64 { - test_debug!("Skip block-commit whose 'parent' comes at or after it"); - continue; - } - if opdata.burn_fee == 0 { - test_debug!("Skip block-commit without burn"); - continue; - } - block_ops.push(opdata); - } - _ => { - continue; - } - } - } - block_ops.sort_by(|op1, op2| { - assert_eq!( - op1.block_height, op2.block_height, - "BUG: block loaded ops from a different block height" - ); - op1.vtxindex.cmp(&op2.vtxindex) - }); - ret.push(block_ops); - } - - test_debug!( - "Read {} headers, {} prepare-phase commits from reward cycle {} ({}-{})", - _num_headers, - ret.len(), - reward_cycle, - start_height, - end_height - ); - Ok(ret) -} - -/// Find all referenced parent block-commits already in the burnchain DB, so we can extract their VRF seeds. -/// If this method errors out, it's because it couldn't read the burnchain headers DB (or it's -/// corrupted). Either way, the caller may treat this as a fatal condition. -pub fn read_parent_block_commits( - burnchain_tx: &BurnchainDBTransaction, - indexer: &B, - prepare_phase_ops: &[Vec], -) -> Result, Error> { - let mut parents = HashMap::new(); - for ops in prepare_phase_ops.iter() { - for opdata in ops.iter() { - let hdr = - if let Some(hdr) = indexer.read_burnchain_header(opdata.parent_block_ptr as u64)? { - hdr - } else { - // this is pretty bad if this happens - error!( - "Discontiguous header database: no such block {}, but have block {}", - opdata.parent_block_ptr, opdata.block_height - ); - return Err(Error::MissingParentBlock); - }; - - test_debug!("Get header at {}: {:?}", opdata.parent_block_ptr, &hdr); - assert_eq!(hdr.block_height, opdata.parent_block_ptr as u64); - - let mut found = false; - let blk = BurnchainDB::get_burnchain_block(burnchain_tx.conn(), &hdr.block_hash) - .unwrap_or_else(|_| { - panic!( - "BUG: failed to load existing block {} ({})", - &hdr.block_hash, &hdr.block_height - ) - }); - - for parent_op in blk.ops.into_iter() { - if let BlockstackOperationType::LeaderBlockCommit(parent_opdata) = parent_op { - if parent_opdata.vtxindex == opdata.parent_vtxindex as u32 { - test_debug!( - "Parent of {},{},{} is {},{},{}", - &opdata.txid, - opdata.block_height, - opdata.vtxindex, - &parent_opdata.txid, - parent_opdata.block_height, - parent_opdata.vtxindex - ); - parents.insert(parent_opdata.txid.clone(), parent_opdata); - found = true; - } - } - } - if !found { - test_debug!( - "Orphan block commit {},{},{}", - &opdata.txid, - opdata.block_height, - opdata.vtxindex - ); - } - } - } - let mut parent_list: Vec<_> = parents.into_values().collect(); - parent_list.sort_by(|a, b| { - if a.block_height != b.block_height { - a.block_height.cmp(&b.block_height) - } else { - a.vtxindex.cmp(&b.vtxindex) - } - }); - - test_debug!("Read {} parent block-commits", parent_list.len()); - Ok(parent_list) -} - -/// Given a list of prepare-phase block-commits, and a list of parent commits, filter out and remove -/// the prepare-phase commits that _don't_ have a parent. -pub fn filter_orphan_block_commits( - parents: &[LeaderBlockCommitOp], - prepare_phase_ops: Vec>, -) -> Vec> { - let mut parent_set = HashSet::new(); - for parent in parents.iter() { - parent_set.insert((parent.block_height, parent.vtxindex)); - } - for prepare_phase_block in prepare_phase_ops.iter() { - for opdata in prepare_phase_block.iter() { - parent_set.insert((opdata.block_height, opdata.vtxindex)); - } - } - - prepare_phase_ops - .into_iter() - .map(|prepare_phase_block| { - prepare_phase_block - .into_iter() - .filter(|opdata| { - if parent_set.contains(&( - opdata.parent_block_ptr as u64, - opdata.parent_vtxindex as u32, - )) { - true - } else { - test_debug!( - "Ignore invalid block-commit {},{} ({}): no parent {},{}", - opdata.block_height, - opdata.vtxindex, - &opdata.txid, - opdata.parent_block_ptr, - opdata.parent_vtxindex - ); - false - } - }) - .collect() - }) - .collect() -} - -/// Given a list of prepare-phase block-commits, filter out the ones that don't have correct burn -/// modulii. This means that late block-commits don't count as confirmations. -pub fn filter_missed_block_commits( - prepare_phase_ops: Vec>, -) -> Vec> { - prepare_phase_ops - .into_iter() - .map(|commits| { - commits - .into_iter() - .filter(|cmt| { - let intended_modulus = - (cmt.burn_block_mined_at() + 1) % BURN_BLOCK_MINED_AT_MODULUS; - let actual_modulus = cmt.block_height % BURN_BLOCK_MINED_AT_MODULUS; - if actual_modulus == intended_modulus { - true - } else { - test_debug!( - "Ignore invalid block-commit {},{} ({}): {} != {}", - cmt.block_height, - cmt.vtxindex, - &cmt.txid, - actual_modulus, - intended_modulus - ); - false - } - }) - .collect() - }) - .collect() -} - -/// Given a list of block-commits in the prepare-phase, find the block-commit pointer outside the -/// prepare-phase which must be the anchor block, if it exists at all. This is always -/// the block-commit that has the most cumulative BTC committed behind it (and the highest -/// such in the event of a tie), as well as at least `anchor_threshold` confirmations. -/// Returns the pointer into the burnchain where the anchor block-commit can be found, if it -/// exists at all. -/// Returns None otherwise -fn inner_find_heaviest_block_commit_ptr( - prepare_phase_ops: &[Vec], - anchor_threshold: u32, -) -> Option<(PoxAnchorPtr, BTreeMap<(u64, u32), (u64, u32)>)> { - // sanity check -- must be in order by block height and vtxindex - for prepare_block_ops in prepare_phase_ops.iter() { - let mut expected_block_height = None; - let mut last_vtxindex = None; - for opdata in prepare_block_ops.iter() { - if let Some(expected_block_height) = expected_block_height.as_ref() { - assert_eq!(expected_block_height, &opdata.block_height); - } else { - expected_block_height = Some(opdata.block_height); - } - - if let Some(last_vtxindex) = last_vtxindex.as_mut() { - assert!( - *last_vtxindex < opdata.vtxindex, - "{} !< {} at block {} (op {:?})", - *last_vtxindex, - opdata.vtxindex, - opdata.block_height, - &opdata - ); - *last_vtxindex = opdata.vtxindex; - } else { - last_vtxindex = Some(opdata.vtxindex); - } - test_debug!( - "Prepare-phase block-commit {},{}: {}", - opdata.block_height, - opdata.vtxindex, - &opdata.txid - ); - } - } - - // map (block_height, vtxindex) to (parent_block_height, parent_vtxindex) - let mut parents = BTreeMap::new(); - - // map (block_height, vtxindex) to (non-prepare-ancestor-height, non-prepare-ancestor-vtxindex) - let mut ancestors = BTreeMap::new(); - - // map (non-prepare-ancestor-height, non-prepare-ancestor-vtxindex) to (set-of-block-heights, total_burnt) - // that contain descendants - let mut ancestor_confirmations: BTreeMap<(u64, u32), (HashSet, u64)> = BTreeMap::new(); - - // calculate each block-commit's parents - for prepare_block_ops in prepare_phase_ops.iter() { - for opdata in prepare_block_ops.iter() { - parents.insert( - (opdata.block_height, opdata.vtxindex), - ( - opdata.parent_block_ptr as u64, - opdata.parent_vtxindex as u32, - ), - ); - } - } - - // calculate the ancestor map -- find the highest non-prepare-phase ancestor for each prepare-phase block-commit. - for prepare_block_ops in prepare_phase_ops.iter().rev() { - for opdata in prepare_block_ops.iter() { - let mut cursor = (opdata.block_height, opdata.vtxindex); - while let Some((parent_block, parent_vtxindex)) = parents.get(&cursor) { - cursor = (*parent_block, *parent_vtxindex); - if let Some((block_height, vtxindex)) = ancestors.get(&cursor) { - // already processed - cursor = (*block_height, *vtxindex); - break; - } - } - ancestors.insert((opdata.block_height, opdata.vtxindex), (cursor.0, cursor.1)); - } - } - - // calculate the ancestor confirmations -- figure out how many distinct blocks contain - // block-commits that descend from each pre-prepare-phase ancestor - for prepare_block_ops in prepare_phase_ops.iter() { - for opdata in prepare_block_ops.iter() { - if let Some((ancestor_height, ancestor_vtxindex)) = - ancestors.get(&(opdata.block_height, opdata.vtxindex)) - { - if let Some((ref mut confirmed_block_set, ref mut ancestor_burnt)) = - ancestor_confirmations.get_mut(&(*ancestor_height, *ancestor_vtxindex)) - { - confirmed_block_set.insert(opdata.block_height); - *ancestor_burnt += opdata.burn_fee; - } else { - let mut block_set = HashSet::new(); - block_set.insert(opdata.block_height); - ancestor_confirmations.insert( - (*ancestor_height, *ancestor_vtxindex), - (block_set, opdata.burn_fee), - ); - } - } - } - } - - test_debug!("parents = {:?}", &parents); - test_debug!("ancestors = {:?}", &ancestors); - test_debug!("ancestor_confirmations = {:?}", &ancestor_confirmations); - - if ancestor_confirmations.is_empty() { - // empty prepare phase - test_debug!("Prepare-phase has no block-commits"); - return None; - } - - // find the ancestors with at least $anchor_threshold confirmations, and pick the one that has the - // most total BTC. Break ties by ancestor order -- highest ancestor commit wins. - let mut ancestor_block = 0; - let mut ancestor_vtxindex = 0; - let mut most_burnt = 0; - let mut most_confs = 0; - - // consider ancestor candidates in _highest_-first order - for ((height, vtxindex), (block_set, burnt)) in ancestor_confirmations.iter().rev() { - let confs = block_set.len() as u64; - if confs < u64::from(anchor_threshold) { - continue; - } - - // only consider an earlier ancestor if it burned more than the candidate - if *burnt > most_burnt { - most_burnt = *burnt; - most_confs = confs; - ancestor_block = *height; - ancestor_vtxindex = *vtxindex; - } - } - - if most_burnt == 0 { - // no anchor block possible -- no block-commit has enough confirmations - test_debug!("No block-commit has enough support to be an anchor block"); - return None; - } - - Some(( - PoxAnchorPtr { - block_height: ancestor_block, - vtxindex: ancestor_vtxindex, - burnt: most_burnt, - confs: most_confs, - }, - ancestors, - )) -} - -/// Given a list of block-commits in the prepare-phase, find the block-commit outside the -/// prepare-phase which must be the anchor block, if it exists at all. This is always -/// the block-commit that has the most cumulative BTC committed behind it (and the highest -/// such in the event of a tie), as well as at least `anchor_threshold` confirmations. If the anchor block -/// commit is found, return the descendancy matrix for it as well. -/// Returns Some(the winning block commit, descendancy matrix, total confirmations, total burnt) if -/// there's an anchor block commit. -/// Returns None otherwise -pub fn find_heaviest_block_commit( - burnchain_tx: &BurnchainDBTransaction, - indexer: &B, - prepare_phase_ops: &[Vec], - anchor_threshold: u32, -) -> Result>, u64, u64)>, DBError> { - let (pox_anchor_ptr, ancestors) = - match inner_find_heaviest_block_commit_ptr(prepare_phase_ops, anchor_threshold) { - Some(ptr) => ptr, - None => { - return Ok(None); - } - }; - - let ancestor_block = pox_anchor_ptr.block_height; - let ancestor_vtxindex = pox_anchor_ptr.vtxindex; - let most_burnt = pox_anchor_ptr.burnt; - let most_confs = pox_anchor_ptr.confs; - - // find the ancestor that this tip confirms - let heaviest_ancestor_header = indexer - .read_burnchain_headers(ancestor_block, ancestor_block + 1)? - .first() - .unwrap_or_else(|| panic!("BUG: no block headers for height {}", ancestor_block)) - .to_owned(); - - let heaviest_ancestor_block = - BurnchainDB::get_burnchain_block(burnchain_tx.conn(), &heaviest_ancestor_header.block_hash) - .unwrap_or_else(|_| { - panic!( - "BUG: no ancestor block {:?} ({})", - &heaviest_ancestor_header.block_hash, heaviest_ancestor_header.block_height - ) - }); - - // find the PoX anchor block-commit, if it exists at all - // (note that it may not -- a rich attacker can force F*w confirmations with lots of BTC on a - // commit that was never mined). - for block_op in heaviest_ancestor_block.ops.into_iter() { - if let BlockstackOperationType::LeaderBlockCommit(opdata) = block_op { - if opdata.block_height == ancestor_block && opdata.vtxindex == ancestor_vtxindex { - // found - debug!( - "PoX anchor block-commit {},{},{} has {} burnt, {} confs", - &opdata.txid, opdata.block_height, opdata.vtxindex, most_burnt, most_confs; - "stacks_block_hash" => opdata.block_header_hash - ); - - // sanity check -- there should be exactly as many confirmations on the suspected - // anchor block as there are distinct descendancies. - let mut conf_count = 0; - - // sanity check -- there should be exactly as many BTC burnt for the suspected - // anchor block as the most_burnt. - let mut burn_count = 0; - - let mut descendancy = Vec::with_capacity(prepare_phase_ops.len()); - for prepare_block_ops in prepare_phase_ops.iter() { - let mut block_descendancy = Vec::with_capacity(prepare_phase_ops.len()); - let mut found_conf = false; - for opdata in prepare_block_ops.iter() { - if let Some((op_ancestor_height, op_ancestor_vtxindex, ..)) = - ancestors.get(&(opdata.block_height, opdata.vtxindex)) - { - if *op_ancestor_height == ancestor_block - && *op_ancestor_vtxindex == ancestor_vtxindex - { - debug!("Block-commit {},{} descends from likely PoX anchor block {},{}", opdata.block_height, opdata.vtxindex, op_ancestor_height, op_ancestor_vtxindex; - "stacks_block_hash" => opdata.block_header_hash - ); - block_descendancy.push(true); - if !found_conf { - conf_count += 1; - found_conf = true; - } - burn_count += opdata.burn_fee; - } else { - debug!("Block-commit {},{} does NOT descend from likely PoX anchor block {},{}", opdata.block_height, opdata.vtxindex, ancestor_block, ancestor_vtxindex; - "stacks_block_hash" => opdata.block_header_hash - ); - block_descendancy.push(false); - } - } else { - debug!("Block-commit {},{} does NOT descend from likely PoX anchor block {},{}", opdata.block_height, opdata.vtxindex, ancestor_block, ancestor_vtxindex; - "stacks_block_hash" => opdata.block_header_hash - ); - block_descendancy.push(false); - } - } - descendancy.push(block_descendancy); - } - - assert_eq!(conf_count, most_confs); - assert_eq!(burn_count, most_burnt); - - return Ok(Some((opdata, descendancy, most_confs, most_burnt))); - } - } - } - - warn!("Evil miners confirmed a non-existant PoX anchor block!"); - Ok(None) -} - -/// Find the valid prepare-phase ops for a given reward cycle -fn inner_find_valid_prepare_phase_commits( - burnchain_tx: &BurnchainDBTransaction, - reward_cycle: u64, - indexer: &B, - burnchain: &Burnchain, -) -> Result>, Error> { - let pox_consts = &burnchain.pox_constants; - let first_block_height = burnchain.first_block_height; - - let prepare_ops = read_prepare_phase_commits( - burnchain_tx, - indexer, - pox_consts, - first_block_height, - reward_cycle, - )?; - test_debug!("{} prepare-phase commits", prepare_ops.len()); - - let parent_commits = read_parent_block_commits(burnchain_tx, indexer, &prepare_ops)?; - test_debug!("{} parent block-commits", parent_commits.len()); - - let prepare_ops_no_orphans = filter_orphan_block_commits(&parent_commits, prepare_ops); - test_debug!( - "{} prepare-phase block-commits that have parents", - prepare_ops_no_orphans.len() - ); - - let prepare_ops_valid = filter_missed_block_commits(prepare_ops_no_orphans); - test_debug!( - "{} prepare-phase block-commits that have parents and are on-time", - prepare_ops_valid.len() - ); - - Ok(prepare_ops_valid) -} - -/// Find the pointer to the PoX anchor block selected in a reward cycle, if it exists. This is the heaviest F*w-confirmed -/// block-commit before the prepare-phase of this reward cycle, provided that it is not already an -/// anchor block for some other reward cycle. Note that the anchor block found will be the anchor -/// block for the *next* reward cycle. -/// Returns a pointer to the block-commit transaction in the burnchain, if the prepare phase -/// selected an anchor block. -/// Returns None if not. -pub fn find_pox_anchor_block_ptr( - burnchain_tx: &BurnchainDBTransaction, - reward_cycle: u64, - indexer: &B, - burnchain: &Burnchain, -) -> Result, Error> { - let prepare_ops_valid = - inner_find_valid_prepare_phase_commits(burnchain_tx, reward_cycle, indexer, burnchain)?; - Ok(inner_find_heaviest_block_commit_ptr( - &prepare_ops_valid, - burnchain.pox_constants.anchor_threshold, - ) - .map(|(ptr, _)| ptr)) -} - -/// Find the PoX anchor block selected in a reward cycle, if it exists. This is the heaviest F*w-confirmed -/// block-commit before the prepare-phase of this reward cycle, provided that it is not already an -/// anchor block for some other reward cycle. Note that the anchor block found will be the anchor -/// block for the *next* reward cycle. -/// Returns: -/// (a) the list of block-commits, grouped by block and ordered by vtxindex, in this prepare phase -/// (b) the PoX anchor block-commit, if it exists, and -/// (c) the descendancy data for the prepare phase. Descendency[i][j] is true if the jth -/// block-commit in the ith block in the prepare phase descends from the anchor block, or False -/// if not. -/// Returns only database-related errors. -pub fn find_pox_anchor_block( - burnchain_tx: &BurnchainDBTransaction, - reward_cycle: u64, - indexer: &B, - burnchain: &Burnchain, -) -> Result< - ( - // (a) prepare-phase block-commits - Vec>, - // (b) PoX anchor block commit (if found) - // (c) descendancy matrix - Option<(LeaderBlockCommitOp, Vec>)>, - ), - Error, -> { - let prepare_ops_valid = - inner_find_valid_prepare_phase_commits(burnchain_tx, reward_cycle, indexer, burnchain)?; - let anchor_block_and_descendancy_opt = find_heaviest_block_commit( - burnchain_tx, - indexer, - &prepare_ops_valid, - burnchain.pox_constants.anchor_threshold, - )?; - if let Some((ref anchor_block_commit, ..)) = anchor_block_and_descendancy_opt.as_ref() { - // cannot have been an anchor block in some other reward cycle - let md = BurnchainDB::get_commit_metadata( - burnchain_tx.conn(), - &anchor_block_commit.burn_header_hash, - &anchor_block_commit.txid, - )? - .expect("BUG: anchor block commit has no metadata"); - - if let Some(rc) = md.anchor_block { - warn!( - "Block-commit {} is already an anchor block for reward cycle {}", - &anchor_block_commit.txid, rc - ); - return Ok((prepare_ops_valid, None)); - } - } - - if anchor_block_and_descendancy_opt.is_some() { - test_debug!( - "Selected an anchor block in prepare phase of reward cycle {}", - reward_cycle - ); - } else { - test_debug!( - "Did NOT select an anchor block in prepare phase of reward cycle {}", - reward_cycle - ); - } - - Ok(( - prepare_ops_valid, - anchor_block_and_descendancy_opt - .map(|(anchor_block_commit, descendancy, ..)| (anchor_block_commit, descendancy)), - )) -} diff --git a/stackslib/src/burnchains/mod.rs b/stackslib/src/burnchains/mod.rs index fd81ac653c..dd1acd1186 100644 --- a/stackslib/src/burnchains/mod.rs +++ b/stackslib/src/burnchains/mod.rs @@ -43,7 +43,6 @@ use crate::net::neighbors::MAX_NEIGHBOR_BLOCK_DELAY; use crate::util_lib::db::Error as db_error; /// This module contains drivers and types for all burn chains we support. -pub mod affirmation; pub mod bitcoin; pub mod burnchain; pub mod db; diff --git a/stackslib/src/burnchains/tests/affirmation.rs b/stackslib/src/burnchains/tests/affirmation.rs deleted file mode 100644 index 393bf2e2e9..0000000000 --- a/stackslib/src/burnchains/tests/affirmation.rs +++ /dev/null @@ -1,1194 +0,0 @@ -// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation -// Copyright (C) 2020-2021 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -use stacks_common::types::chainstate::BurnchainHeaderHash; -use stacks_common::util::hash::hex_bytes; -use stacks_common::util::vrf::*; - -use crate::burnchains::affirmation::*; -use crate::burnchains::db::*; -use crate::burnchains::tests::db::*; -use crate::burnchains::{BurnchainBlockHeader, *}; -use crate::chainstate::burn::operations::leader_block_commit::*; -use crate::chainstate::burn::operations::*; -use crate::chainstate::coordinator::tests::*; - -fn make_test_pox( - cycle_len: u32, - prepare_len: u32, - anchor_thresh: u32, - rejection_frac: u64, -) -> PoxConstants { - PoxConstants::new( - cycle_len, - prepare_len, - anchor_thresh, - rejection_frac, - 0, - u64::MAX - 1, - u64::MAX, - u32::MAX, - u32::MAX, - u32::MAX, - u32::MAX, - ) -} - -#[test] -fn affirmation_map_encode_decode() { - assert_eq!(AffirmationMap::decode(""), Some(AffirmationMap::empty())); - assert_eq!( - AffirmationMap::decode("anp"), - Some(AffirmationMap::new(vec![ - AffirmationMapEntry::PoxAnchorBlockAbsent, - AffirmationMapEntry::Nothing, - AffirmationMapEntry::PoxAnchorBlockPresent - ])) - ); - assert_eq!(AffirmationMap::decode("x"), None); - assert_eq!(AffirmationMap::decode("\u{0101}"), None); - - assert_eq!(AffirmationMap::empty().encode(), "".to_string()); - assert_eq!( - AffirmationMap::new(vec![ - AffirmationMapEntry::PoxAnchorBlockAbsent, - AffirmationMapEntry::Nothing, - AffirmationMapEntry::PoxAnchorBlockPresent - ]) - .encode(), - "anp".to_string() - ); -} - -#[test] -fn affirmation_map_find_divergence() { - assert_eq!( - AffirmationMap::decode("aaa") - .unwrap() - .find_divergence(&AffirmationMap::decode("aaa").unwrap()), - None - ); - assert_eq!( - AffirmationMap::decode("aaa") - .unwrap() - .find_divergence(&AffirmationMap::decode("aaaa").unwrap()), - Some(3) - ); - assert_eq!( - AffirmationMap::decode("aaa") - .unwrap() - .find_divergence(&AffirmationMap::decode("aa").unwrap()), - None - ); - assert_eq!( - AffirmationMap::decode("apa") - .unwrap() - .find_divergence(&AffirmationMap::decode("aaa").unwrap()), - Some(1) - ); - assert_eq!( - AffirmationMap::decode("apa") - .unwrap() - .find_divergence(&AffirmationMap::decode("aaaa").unwrap()), - Some(1) - ); - assert_eq!( - AffirmationMap::decode("naa") - .unwrap() - .find_divergence(&AffirmationMap::decode("aa").unwrap()), - Some(0) - ); - assert_eq!( - AffirmationMap::decode("napn") - .unwrap() - .find_divergence(&AffirmationMap::decode("").unwrap()), - None - ); - assert_eq!( - AffirmationMap::decode("pn") - .unwrap() - .find_divergence(&AffirmationMap::decode("n").unwrap()), - Some(0) - ); -} - -#[test] -fn affirmation_map_find_inv_search() { - assert_eq!( - AffirmationMap::decode("aaa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aaa").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("aaa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aaaa").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("aaa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aa").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("apa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aaa").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("apa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aaaa").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("naa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aa").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("napn") - .unwrap() - .find_inv_search(&AffirmationMap::decode("").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("pn") - .unwrap() - .find_inv_search(&AffirmationMap::decode("n").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("paap") - .unwrap() - .find_inv_search(&AffirmationMap::decode("pap").unwrap()), - 1 - ); - assert_eq!( - AffirmationMap::decode("paap") - .unwrap() - .find_inv_search(&AffirmationMap::decode("paap").unwrap()), - 3 - ); - assert_eq!( - AffirmationMap::decode("papa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("apap").unwrap()), - 0 - ); - assert_eq!( - AffirmationMap::decode("paapapap") - .unwrap() - .find_inv_search(&AffirmationMap::decode("paappapa").unwrap()), - 4 - ); - assert_eq!( - AffirmationMap::decode("aaaaa") - .unwrap() - .find_inv_search(&AffirmationMap::decode("aaaaa").unwrap()), - 0 - ); -} - -pub fn make_simple_key_register( - burn_header_hash: &BurnchainHeaderHash, - block_height: u64, - vtxindex: u32, -) -> LeaderKeyRegisterOp { - LeaderKeyRegisterOp { - consensus_hash: ConsensusHash::from_bytes( - &hex_bytes("2222222222222222222222222222222222222222").unwrap(), - ) - .unwrap(), - public_key: VRFPublicKey::from_bytes( - &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a").unwrap(), - ) - .unwrap(), - memo: vec![1, 2, 3, 4, 5], - - txid: next_txid(), - vtxindex, - block_height, - burn_header_hash: burn_header_hash.clone(), - } -} - -/// Create a mock reward cycle with a particular anchor block vote outcome -- it either confirms or -/// does not confirm an anchor block. The method returns the data for all new mocked blocks -/// created -- it returns the list of new block headers, and for each new block, it returns the -/// list of block-commits created (if any). In addition, the `headers` argument will be grown to -/// include the new block-headers (so that a succession of calls to this method will grow the given -/// headers argument). The list of headers returned (first tuple item) is in 1-to-1 correspondence -/// with the list of lists of block-commits returned (second tuple item). If the ith item in -/// parent_commits is None, then all the block-commits in the ith list of lists of block-commits -/// will be None. -/// -/// The caller can control how many block-commits get produced per block with the `parent_commits` -/// argument. If parent_commits[i] is Some(..), then a sequence of block-commits will be produced -/// that descend from it. -/// -/// If `confirm_anchor_block` is true, then the prepare-phase of the reward cycle will confirm an -/// anchor block -- there will be sufficiently many confirmations placed on a block-commit in the -/// reward phase. Otherwise, enough preapre-phase blocks will be missing block-commits that no -/// anchor block is selected. -/// -/// All block-commits produced reference the given miner key (given in the `key` argument). All -/// block-commits created, as well as all block headers, will be stored to the given burnchain -/// database (in addition to being returned). -pub fn make_reward_cycle_with_vote( - burnchain_db: &mut BurnchainDB, - burnchain: &Burnchain, - key: &LeaderKeyRegisterOp, - headers: &mut Vec, - mut parent_commits: Vec>, - confirm_anchor_block: bool, -) -> ( - Vec, - Vec>>, -) { - let mut new_headers = vec![]; - let mut new_commits = vec![]; - - let first_block_header = burnchain_db.get_first_header().unwrap(); - let mut current_header = burnchain_db.get_canonical_chain_tip().unwrap(); - let mut height = current_header.block_height + 1; - let mut parent_block_header: Option = - Some(headers.last().unwrap().to_owned()); - - for i in 0..burnchain.pox_constants.reward_cycle_length { - let block_header = BurnchainBlockHeader { - block_height: height, - block_hash: next_burn_header_hash(), - parent_block_hash: parent_block_header - .as_ref() - .map(|blk| blk.block_hash.clone()) - .unwrap_or(first_block_header.block_hash.clone()), - num_txs: parent_commits.len() as u64, - timestamp: i as u64, - }; - - let ops = if current_header == first_block_header { - // first-ever block -- add only the leader key - let mut key_insert = key.clone(); - key_insert.burn_header_hash = block_header.block_hash.clone(); - - test_debug!( - "Insert key-register in {}: {},{},{} in block {}", - &key_insert.burn_header_hash, - &key_insert.txid, - key_insert.block_height, - key_insert.vtxindex, - block_header.block_height - ); - - new_commits.push(vec![None; parent_commits.len()]); - vec![BlockstackOperationType::LeaderKeyRegister( - key_insert.clone(), - )] - } else { - let mut commits = vec![]; - for i in 0..parent_commits.len() { - let mut block_commit = make_simple_block_commit( - burnchain, - parent_commits[i].as_ref(), - &block_header, - next_block_hash(), - ); - block_commit.key_block_ptr = key.block_height as u32; - block_commit.key_vtxindex = key.vtxindex as u16; - block_commit.vtxindex += i as u32; - block_commit.burn_parent_modulus = if height > 0 { - ((height - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8 - } else { - BURN_BLOCK_MINED_AT_MODULUS as u8 - 1 - }; - - assert_eq!(block_commit.burn_header_hash, block_header.block_hash); - assert_eq!(block_commit.block_height, block_header.block_height); - - let append = if !burnchain.is_in_prepare_phase(block_commit.block_height) { - // non-prepare-phase commits always confirm their parent - true - } else if confirm_anchor_block { - // all block-commits confirm anchor block - true - } else { - // fewer than anchor_threshold commits confirm anchor block - let next_rc_start = burnchain.reward_cycle_to_block_height( - burnchain - .block_height_to_reward_cycle(block_commit.block_height) - .unwrap() - + 1, - ); - if block_commit.block_height - + (burnchain.pox_constants.anchor_threshold as u64) - + 1 - < next_rc_start - { - // in first half of prepare phase, so confirm - true - } else { - // in second half of prepare phase, so don't confirm - false - } - }; - - if append { - test_debug!( - "Insert block-commit in {}: {},{},{}, builds on {},{}", - &block_commit.burn_header_hash, - &block_commit.txid, - block_commit.block_height, - block_commit.vtxindex, - block_commit.parent_block_ptr, - block_commit.parent_vtxindex - ); - - if let Some(parent_commit) = parent_commits[i].as_ref() { - assert!(parent_commit.block_height != block_commit.block_height); - assert!( - parent_commit.block_height == u64::from(block_commit.parent_block_ptr) - ); - assert!(parent_commit.vtxindex == u32::from(block_commit.parent_vtxindex)); - } - - parent_commits[i] = Some(block_commit.clone()); - commits.push(Some(block_commit.clone())); - } else { - test_debug!( - "Do NOT insert block-commit in {}: {},{},{}", - &block_commit.burn_header_hash, - &block_commit.txid, - block_commit.block_height, - block_commit.vtxindex - ); - - commits.push(None); - } - } - new_commits.push(commits.clone()); - commits - .into_iter() - .flatten() - .map(BlockstackOperationType::LeaderBlockCommit) - .collect() - }; - - burnchain_db - .store_new_burnchain_block_ops_unchecked(&block_header, &ops) - .unwrap(); - - headers.push(block_header.clone()); - new_headers.push(block_header.clone()); - parent_block_header = Some(block_header); - - current_header = burnchain_db.get_canonical_chain_tip().unwrap(); - height = current_header.block_height + 1; - } - - (new_headers, new_commits) -} - -/// Conveninece wrapper that produces a reward cycle with one sequence of block-commits. Returns -/// the sequence of block headers in this reward cycle, and the list of block-commits created. If -/// parent_commit is None, then the list of block-commits will contain all None's. -fn make_simple_reward_cycle( - burnchain_db: &mut BurnchainDB, - burnchain: &Burnchain, - key: &LeaderKeyRegisterOp, - headers: &mut Vec, - parent_commit: Option, -) -> (Vec, Vec>) { - let (new_headers, commits) = - make_reward_cycle(burnchain_db, burnchain, key, headers, vec![parent_commit]); - ( - new_headers, - commits - .into_iter() - .map(|mut cmts| cmts.pop().unwrap()) - .collect(), - ) -} - -/// Convenience wrapper that produces a reward cycle with zero or more sequences of block-commits, -/// such that an anchor block-commit is chosen. -/// Returns the list of new block headers and each blocks' commits. -pub fn make_reward_cycle( - burnchain_db: &mut BurnchainDB, - burnchain: &Burnchain, - key: &LeaderKeyRegisterOp, - headers: &mut Vec, - parent_commits: Vec>, -) -> ( - Vec, - Vec>>, -) { - make_reward_cycle_with_vote(burnchain_db, burnchain, key, headers, parent_commits, true) -} - -/// Convenience wrapper that produces a reward cycle with zero or more sequences of block-commits, -/// such that no anchor block-commit is chosen. -/// Returns the list of new block headers and each blocks' commits. -pub fn make_reward_cycle_without_anchor( - burnchain_db: &mut BurnchainDB, - burnchain: &Burnchain, - key: &LeaderKeyRegisterOp, - headers: &mut Vec, - parent_commits: Vec>, -) -> ( - Vec, - Vec>>, -) { - make_reward_cycle_with_vote(burnchain_db, burnchain, key, headers, parent_commits, false) -} - -#[test] -fn test_read_prepare_phase_commits() { - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(10, 5, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - assert_eq!(&first_block_header.block_hash, &first_bhh); - assert_eq!(first_block_header.block_height, first_height); - assert_eq!(first_block_header.timestamp, first_timestamp as u64); - - eprintln!( - "First block parent is {}", - &first_block_header.parent_block_hash - ); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - let (next_headers, commits) = make_simple_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - None, - ); - - assert_eq!( - commits.len() as u32, - burnchain.pox_constants.reward_cycle_length - ); - assert!(commits[0].is_none()); - for i in 1..burnchain.pox_constants.reward_cycle_length { - assert!(commits[i as usize].is_some()); - } - - let all_ops = read_prepare_phase_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &burnchain.pox_constants, - first_block_header.block_height, - 0, - ) - .unwrap(); - assert_eq!(all_ops.len() as u32, burnchain.pox_constants.prepare_length); - for i in 0..burnchain.pox_constants.prepare_length { - assert_eq!(all_ops[i as usize].len(), 1); - - let opdata = &all_ops[i as usize][0]; - assert_eq!( - opdata, - commits[(i + burnchain.pox_constants.reward_cycle_length - - burnchain.pox_constants.prepare_length) as usize] - .as_ref() - .unwrap() - ); - } -} - -#[test] -fn test_parent_block_commits() { - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(10, 5, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits) = make_simple_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - None, - ); - - let all_ops = read_prepare_phase_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &burnchain.pox_constants, - first_block_header.block_height, - 0, - ) - .unwrap(); - let parent_commits = - read_parent_block_commits(&burnchain_db.tx_begin().unwrap(), &headers, &all_ops).unwrap(); - - // this is a simple reward cycle -- each block-commit has a unique parent - assert_eq!(parent_commits.len(), all_ops.len()); - - for op_list in all_ops.iter() { - for opdata in op_list.iter() { - let mut found_parent = false; - for parent_commit in parent_commits.iter() { - if parent_commit.block_height == (opdata.parent_block_ptr as u64) - && parent_commit.vtxindex == (opdata.parent_vtxindex as u32) - { - found_parent = true; - break; - } - } - assert!(found_parent, "did not find parent for {:?}", opdata); - } - } - - let mut all_ops_with_orphan = all_ops.clone(); - all_ops_with_orphan[1][0].parent_vtxindex += 1; - - let parent_commits = read_parent_block_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_with_orphan, - ) - .unwrap(); - - // this is a simple reward cycle -- each block-commit has a unique parent, except for the - // orphan - assert_eq!(parent_commits.len(), all_ops_with_orphan.len() - 1); - - let mut all_ops_with_same_parent = all_ops; - for ops in all_ops_with_same_parent.iter_mut() { - for opdata in ops.iter_mut() { - opdata.parent_block_ptr = 3; - opdata.parent_vtxindex = 0; - } - } - - let parent_commits = read_parent_block_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_with_same_parent, - ) - .unwrap(); - - assert_eq!(parent_commits.len(), 1); - assert_eq!(parent_commits[0].block_height, 3); - assert_eq!(parent_commits[0].vtxindex, 0); -} - -#[test] -fn test_filter_orphan_block_commits() { - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(5, 3, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits) = make_simple_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - None, - ); - - let all_ops = read_prepare_phase_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &burnchain.pox_constants, - first_block_header.block_height, - 0, - ) - .unwrap(); - let parent_commits = - read_parent_block_commits(&burnchain_db.tx_begin().unwrap(), &headers, &all_ops).unwrap(); - - let mut all_ops_with_orphan = all_ops.clone(); - all_ops_with_orphan[1][0].parent_vtxindex += 1; - - assert_eq!(all_ops_with_orphan[0].len(), 1); - assert_eq!(all_ops_with_orphan[1].len(), 1); - assert_eq!(all_ops_with_orphan[2].len(), 1); - - let parent_commits = read_parent_block_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_with_orphan, - ) - .unwrap(); - let filtered_ops = filter_orphan_block_commits(&parent_commits, all_ops_with_orphan); - - assert_eq!(filtered_ops.len(), all_ops.len()); - assert_eq!(filtered_ops[0].len(), 1); - assert!(filtered_ops[1].is_empty()); - assert_eq!(filtered_ops[2].len(), 1); -} - -#[test] -fn test_filter_missed_block_commits() { - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(5, 3, 3, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits) = make_simple_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - None, - ); - - let all_ops = read_prepare_phase_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &burnchain.pox_constants, - first_block_header.block_height, - 0, - ) - .unwrap(); - let parent_commits = - read_parent_block_commits(&burnchain_db.tx_begin().unwrap(), &headers, &all_ops).unwrap(); - - let mut all_ops_with_missed = all_ops.clone(); - all_ops_with_missed[1][0].burn_parent_modulus -= 1; - - assert_eq!(all_ops_with_missed[0].len(), 1); - assert_eq!(all_ops_with_missed[1].len(), 1); - assert_eq!(all_ops_with_missed[2].len(), 1); - - let parent_commits = read_parent_block_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_with_missed, - ) - .unwrap(); - let filtered_ops = filter_missed_block_commits(all_ops_with_missed); - - assert_eq!(filtered_ops.len(), all_ops.len()); - assert_eq!(filtered_ops[0].len(), 1); - assert!(filtered_ops[1].is_empty()); - assert_eq!(filtered_ops[2].len(), 1); -} - -#[test] -fn test_find_heaviest_block_commit() { - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(5, 3, 2, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - // first reward cycle is all (linear) commits, so it must elect an anchor block - let (next_headers, commits) = make_simple_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - None, - ); - - let all_ops = read_prepare_phase_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &burnchain.pox_constants, - first_block_header.block_height, - 0, - ) - .unwrap(); - let parent_commits = - read_parent_block_commits(&burnchain_db.tx_begin().unwrap(), &headers, &all_ops).unwrap(); - let filtered_ops = filter_orphan_block_commits(&parent_commits, all_ops); - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &filtered_ops, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - // since this is just a linear chain of block-commits, the heaviest parent is the parent of the - // first block-commit in the prepare phase - assert_eq!(commits[1].as_ref().unwrap(), &heaviest_parent_block_commit); - assert_eq!(descendancy, vec![vec![true], vec![true], vec![true]]); - assert_eq!(total_confs, 3); - assert_eq!(total_burns, 3 * 10000); - - // make a forked history, but with a best-tip - // 1,0 <-- 2,0 <-- 3,0 <-- 4,0 - // \ - // `---------------------------- 5,0 - let mut all_ops_forked_majority = filtered_ops.clone(); - all_ops_forked_majority[2][0].parent_block_ptr = 1; - all_ops_forked_majority[2][0].parent_vtxindex = 0; - - // still commit 1 - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_forked_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - assert_eq!(commits[1].as_ref().unwrap(), &heaviest_parent_block_commit); - assert_eq!(descendancy, vec![vec![true], vec![true], vec![false]]); - assert_eq!(total_confs, 2); - assert_eq!(total_burns, 2 * 10000); - - // make a forked history, with another best-tip winner, but with a deeper fork split - // 1,0 <-- 2,0 <-- 3,0 - // \ - // `------- 4,0 <-- 5,0 - let mut all_ops_forked_majority = filtered_ops.clone(); - all_ops_forked_majority[1][0].parent_block_ptr = 2; - all_ops_forked_majority[1][0].parent_vtxindex = 0; - - all_ops_forked_majority[2][0].parent_block_ptr = 2; - all_ops_forked_majority[2][0].parent_vtxindex = 0; - - // still commit 1 - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_forked_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - assert_eq!(commits[1].as_ref().unwrap(), &heaviest_parent_block_commit); - assert_eq!(descendancy, vec![vec![true], vec![true], vec![true]]); - assert_eq!(total_confs, 3); - assert_eq!(total_burns, 3 * 10000); - - // make a forked history where there is no best tip, but enough confirmations - // 1,0 <-- 2,0 <-- 3,0 - // |\ - // | `------- 4,0 - // \ - // `------------- 5,0 - let mut all_ops_no_majority = filtered_ops.clone(); - all_ops_no_majority[0][0].parent_block_ptr = 2; - all_ops_no_majority[0][0].parent_vtxindex = 0; - all_ops_no_majority[0][0].burn_fee = 0; - - all_ops_no_majority[1][0].parent_block_ptr = 2; - all_ops_no_majority[1][0].parent_vtxindex = 0; - all_ops_no_majority[1][0].burn_fee = 1; - - all_ops_no_majority[2][0].parent_block_ptr = 2; - all_ops_no_majority[2][0].parent_vtxindex = 0; - all_ops_no_majority[2][0].burn_fee = 2; - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_no_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - assert_eq!(commits[1].as_ref().unwrap(), &heaviest_parent_block_commit); - assert_eq!(descendancy, vec![vec![true], vec![true], vec![true]]); - assert_eq!(total_confs, 3); - assert_eq!(total_burns, 1 + 2); - - // make a forked history where there is no best tip, but enough (majority) confirmations - // 1,0 <-- 2,0 <-- 3,0 - // | \ - // | `-------- 4,0 - // | - // `----------------------- 5,0 - let mut all_ops_no_majority = filtered_ops.clone(); - all_ops_no_majority[0][0].parent_block_ptr = 2; - all_ops_no_majority[0][0].parent_vtxindex = 0; - all_ops_no_majority[0][0].burn_fee = 0; - - all_ops_no_majority[1][0].parent_block_ptr = 2; - all_ops_no_majority[1][0].parent_vtxindex = 0; - all_ops_no_majority[1][0].burn_fee = 1; - - all_ops_no_majority[2][0].parent_block_ptr = 1; - all_ops_no_majority[2][0].parent_vtxindex = 0; - all_ops_no_majority[2][0].burn_fee = 20; - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_no_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - assert_eq!(commits[1].as_ref().unwrap(), &heaviest_parent_block_commit); - assert_eq!(descendancy, vec![vec![true], vec![true], vec![false]]); - assert_eq!(total_confs, 2); - assert_eq!(total_burns, 1); - - // make a history where there is no anchor block, period - // 1,0 <-- 2,0 X-- 3,0 - // - // X------- 4,0 - // - // X------------ 5,0 - let mut all_ops_no_majority = filtered_ops; - all_ops_no_majority[0][0].parent_block_ptr = 2; - all_ops_no_majority[0][0].parent_vtxindex = 10; - all_ops_no_majority[0][0].burn_fee = 0; - - all_ops_no_majority[1][0].parent_block_ptr = 2; - all_ops_no_majority[1][0].parent_vtxindex = 10; - all_ops_no_majority[1][0].burn_fee = 1; - - all_ops_no_majority[2][0].parent_block_ptr = 1; - all_ops_no_majority[2][0].parent_vtxindex = 10; - all_ops_no_majority[2][0].burn_fee = 20; - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_no_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_none()); -} - -#[test] -fn test_find_heaviest_parent_commit_many_commits() { - // Test finding parent block commits when there's multiple block-commit forks to choose from. - // This tests the tie-breaking logic. - let first_bhh = BurnchainHeaderHash([0; 32]); - let first_timestamp = 0; - let first_height = 0; - - let mut burnchain = Burnchain::regtest(":memory:"); - burnchain.pox_constants = make_test_pox(5, 3, 2, 3); - burnchain.first_block_height = first_height; - burnchain.first_block_hash = first_bhh.clone(); - burnchain.first_block_timestamp = first_timestamp; - - let mut burnchain_db = BurnchainDB::connect(":memory:", &burnchain, true).unwrap(); - - let first_block_header = burnchain_db.get_canonical_chain_tip().unwrap(); - - let mut headers = vec![first_block_header.clone()]; - let key_register = make_simple_key_register(&first_block_header.block_hash, 0, 1); - - let (next_headers, commits) = make_reward_cycle( - &mut burnchain_db, - &burnchain, - &key_register, - &mut headers, - vec![None, None], - ); - - let all_ops = read_prepare_phase_commits( - &burnchain_db.tx_begin().unwrap(), - &headers, - &burnchain.pox_constants, - first_block_header.block_height, - 0, - ) - .unwrap(); - let parent_commits = - read_parent_block_commits(&burnchain_db.tx_begin().unwrap(), &headers, &all_ops).unwrap(); - let filtered_ops = filter_orphan_block_commits(&parent_commits, all_ops); - - // make a history with two miners' commits. - // sortition winners in prepare phase were 3,0; 4,1; 5,0 - // 1,0 <-- 2,0 <--- 3,0 <--- 4,0 ,--- 5,0 - // \ \ / - // `---- 3,1 `--- 4,1 <--- 5,1 - let mut all_ops_no_majority = filtered_ops.clone(); - - // 3,0 - all_ops_no_majority[0][0].parent_block_ptr = 2; - all_ops_no_majority[0][0].parent_vtxindex = 0; - all_ops_no_majority[0][0].vtxindex = 0; - all_ops_no_majority[0][0].burn_fee = 1; - - // 3,1 - all_ops_no_majority[0][1].parent_block_ptr = 2; - all_ops_no_majority[0][1].parent_vtxindex = 0; - all_ops_no_majority[0][1].vtxindex = 1; - all_ops_no_majority[0][1].burn_fee = 1; - - // 4,0 - all_ops_no_majority[1][0].parent_block_ptr = 3; - all_ops_no_majority[1][0].parent_vtxindex = 0; - all_ops_no_majority[1][0].vtxindex = 0; - all_ops_no_majority[1][0].burn_fee = 2; - - // 4,1 - all_ops_no_majority[1][1].parent_block_ptr = 3; - all_ops_no_majority[1][1].parent_vtxindex = 0; - all_ops_no_majority[1][1].vtxindex = 1; - all_ops_no_majority[1][1].burn_fee = 2; - - // 5,0 - all_ops_no_majority[2][0].parent_block_ptr = 4; - all_ops_no_majority[2][0].parent_vtxindex = 1; - all_ops_no_majority[2][0].vtxindex = 0; - all_ops_no_majority[2][0].burn_fee = 3; - - // 5,1 - all_ops_no_majority[2][1].parent_block_ptr = 4; - all_ops_no_majority[2][1].parent_vtxindex = 1; - all_ops_no_majority[2][1].vtxindex = 1; - all_ops_no_majority[2][1].burn_fee = 3; - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_no_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - assert_eq!( - commits[1][0].as_ref().unwrap(), - &heaviest_parent_block_commit - ); - assert_eq!( - descendancy, - vec![vec![true, true], vec![true, true], vec![true, true]] - ); - assert_eq!(total_confs, 3); - assert_eq!(total_burns, 1 + 1 + 2 + 2 + 3 + 3); - - // make a history with two miners' commits - // both histories have the same number of confirmations. - // one history represents more BTC than the other. - // 1,0 <-- 2,0 <--- 3,0 <--- 4,0 <--- 5,0 (winner) - // \ - // `---- 2,1 <--- 3,1 <--- 4,1 <--- 5,1 - let mut all_ops_no_majority = filtered_ops.clone(); - - // 3,0 - all_ops_no_majority[0][0].parent_block_ptr = 2; - all_ops_no_majority[0][0].parent_vtxindex = 0; - all_ops_no_majority[0][0].vtxindex = 0; - all_ops_no_majority[0][0].burn_fee = 1; - - // 3,1 - all_ops_no_majority[0][1].parent_block_ptr = 2; - all_ops_no_majority[0][1].parent_vtxindex = 1; - all_ops_no_majority[0][1].vtxindex = 1; - all_ops_no_majority[0][1].burn_fee = 1; - - // 4,0 - all_ops_no_majority[1][0].parent_block_ptr = 3; - all_ops_no_majority[1][0].parent_vtxindex = 0; - all_ops_no_majority[1][0].vtxindex = 0; - all_ops_no_majority[1][0].burn_fee = 2; - - // 4,1 - all_ops_no_majority[1][1].parent_block_ptr = 3; - all_ops_no_majority[1][1].parent_vtxindex = 1; - all_ops_no_majority[1][1].vtxindex = 1; - all_ops_no_majority[1][1].burn_fee = 2; - - // 5,0 -- slightly heavier than 5,1 - all_ops_no_majority[2][0].parent_block_ptr = 4; - all_ops_no_majority[2][0].parent_vtxindex = 0; - all_ops_no_majority[2][0].vtxindex = 0; - all_ops_no_majority[2][0].burn_fee = 4; - - // 5,1 - all_ops_no_majority[2][1].parent_block_ptr = 4; - all_ops_no_majority[2][1].parent_vtxindex = 1; - all_ops_no_majority[2][1].vtxindex = 1; - all_ops_no_majority[2][1].burn_fee = 3; - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_no_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - // either 2,0 or 2,1 is the anchor block, but we break ties in part by weight. - // 5,0 is heavier than 5,1, so 2,0 wins - assert_eq!( - commits[1][0].as_ref().unwrap(), - &heaviest_parent_block_commit - ); - // prepare-phase commits x,0 all descend from the anchor block. - // prepare-phase commits x,1 do not. - assert_eq!( - descendancy, - vec![vec![true, false], vec![true, false], vec![true, false]] - ); - assert_eq!(total_confs, 3); - assert_eq!(total_burns, 1 + 2 + 4); - - // make a history with two miners' commits - // both histories have the same amount of confirmations and BTC burnt. - // select the anchor block with the latest confirmation to break ties. - // 1,0 <-- 2,0 <--- 3,0 <--- 4,0 <--- 5,0 - // \ - // `---- 2,1 <--- 3,1 <--- 4,1 <--- 5,1 (winner) - let mut all_ops_no_majority = filtered_ops; - - // 3,0 - all_ops_no_majority[0][0].parent_block_ptr = 2; - all_ops_no_majority[0][0].parent_vtxindex = 0; - all_ops_no_majority[0][0].vtxindex = 0; - all_ops_no_majority[0][0].burn_fee = 1; - - // 3,1 - all_ops_no_majority[0][1].parent_block_ptr = 2; - all_ops_no_majority[0][1].parent_vtxindex = 1; - all_ops_no_majority[0][1].vtxindex = 1; - all_ops_no_majority[0][1].burn_fee = 1; - - // 4,0 - all_ops_no_majority[1][0].parent_block_ptr = 3; - all_ops_no_majority[1][0].parent_vtxindex = 0; - all_ops_no_majority[1][0].vtxindex = 0; - all_ops_no_majority[1][0].burn_fee = 2; - - // 4,1 - all_ops_no_majority[1][1].parent_block_ptr = 3; - all_ops_no_majority[1][1].parent_vtxindex = 1; - all_ops_no_majority[1][1].vtxindex = 1; - all_ops_no_majority[1][1].burn_fee = 2; - - // 5,0 - all_ops_no_majority[2][0].parent_block_ptr = 4; - all_ops_no_majority[2][0].parent_vtxindex = 0; - all_ops_no_majority[2][0].vtxindex = 0; - all_ops_no_majority[2][0].burn_fee = 3; - - // 5,1 -- same BTC overall as the history ending at 5,0, but occurs later in the blockchain - all_ops_no_majority[2][1].parent_block_ptr = 4; - all_ops_no_majority[2][1].parent_vtxindex = 1; - all_ops_no_majority[2][1].vtxindex = 1; - all_ops_no_majority[2][1].burn_fee = 3; - - let heaviest_parent_commit_opt = find_heaviest_block_commit( - &burnchain_db.tx_begin().unwrap(), - &headers, - &all_ops_no_majority, - burnchain.pox_constants.anchor_threshold, - ) - .unwrap(); - assert!(heaviest_parent_commit_opt.is_some()); - let (heaviest_parent_block_commit, descendancy, total_confs, total_burns) = - heaviest_parent_commit_opt.unwrap(); - - // number of confirmations and BTC amount are the same in the two fork histories, so break ties - // by choosing the anchor block confirmed by the latest commit. - assert_eq!( - commits[1][1].as_ref().unwrap(), - &heaviest_parent_block_commit - ); - // prepare-phase commits x,0 do not descend from an anchor block - // prepare-phase commits x,1 do - assert_eq!( - descendancy, - vec![vec![false, true], vec![false, true], vec![false, true]] - ); - assert_eq!(total_confs, 3); - assert_eq!(total_burns, 1 + 2 + 3); -} diff --git a/stackslib/src/burnchains/tests/mod.rs b/stackslib/src/burnchains/tests/mod.rs index d71c796cb2..c24c69ba18 100644 --- a/stackslib/src/burnchains/tests/mod.rs +++ b/stackslib/src/burnchains/tests/mod.rs @@ -13,8 +13,6 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . - -pub mod affirmation; pub mod burnchain; pub mod db; pub mod thread_join; diff --git a/stackslib/src/chainstate/burn/db/sortdb.rs b/stackslib/src/chainstate/burn/db/sortdb.rs index f8b5c5ff45..5a763acdf8 100644 --- a/stackslib/src/chainstate/burn/db/sortdb.rs +++ b/stackslib/src/chainstate/burn/db/sortdb.rs @@ -6508,17 +6508,195 @@ pub mod tests { use super::*; use crate::burnchains::db::BurnchainDB; - use crate::burnchains::tests::affirmation::{make_reward_cycle, make_simple_key_register}; + use crate::burnchains::tests::db::make_simple_block_commit; use crate::burnchains::*; use crate::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; use crate::chainstate::burn::operations::{ BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp, }; use crate::chainstate::burn::ConsensusHash; + use crate::chainstate::coordinator::tests::{ + next_block_hash, next_burn_header_hash, next_txid, + }; use crate::chainstate::stacks::StacksPublicKey; use crate::core::{StacksEpochExtension, *}; use crate::util_lib::db::Error as db_error; + pub fn make_simple_key_register( + burn_header_hash: &BurnchainHeaderHash, + block_height: u64, + vtxindex: u32, + ) -> LeaderKeyRegisterOp { + LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes( + &hex_bytes("2222222222222222222222222222222222222222").unwrap(), + ) + .unwrap(), + public_key: VRFPublicKey::from_bytes( + &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") + .unwrap(), + ) + .unwrap(), + memo: vec![1, 2, 3, 4, 5], + + txid: next_txid(), + vtxindex, + block_height, + burn_header_hash: burn_header_hash.clone(), + } + } + + /// Create a mock reward cycle with an anchor block -- The method returns the data for all new mocked blocks + /// created -- it returns the list of new block headers, and for each new block, it returns the + /// list of block-commits created (if any). In addition, the `headers` argument will be grown to + /// include the new block-headers (so that a succession of calls to this method will grow the given + /// headers argument). The list of headers returned (first tuple item) is in 1-to-1 correspondence + /// with the list of lists of block-commits returned (second tuple item). If the ith item in + /// parent_commits is None, then all the block-commits in the ith list of lists of block-commits + /// will be None. + /// + /// The caller can control how many block-commits get produced per block with the `parent_commits` + /// argument. If parent_commits[i] is Some(..), then a sequence of block-commits will be produced + /// that descend from it. + /// + /// All block-commits produced reference the given miner key (given in the `key` argument). All + /// block-commits created, as well as all block headers, will be stored to the given burnchain + /// database (in addition to being returned). + pub fn make_reward_cycle( + burnchain_db: &mut BurnchainDB, + burnchain: &Burnchain, + key: &LeaderKeyRegisterOp, + headers: &mut Vec, + mut parent_commits: Vec>, + ) -> ( + Vec, + Vec>>, + ) { + let mut new_headers = vec![]; + let mut new_commits = vec![]; + + let first_block_header = burnchain_db.get_first_header().unwrap(); + let mut current_header = burnchain_db.get_canonical_chain_tip().unwrap(); + let mut height = current_header.block_height + 1; + let mut parent_block_header: Option = + Some(headers.last().unwrap().to_owned()); + + for i in 0..burnchain.pox_constants.reward_cycle_length { + let block_header = BurnchainBlockHeader { + block_height: height, + block_hash: next_burn_header_hash(), + parent_block_hash: parent_block_header + .as_ref() + .map(|blk| blk.block_hash.clone()) + .unwrap_or(first_block_header.block_hash.clone()), + num_txs: parent_commits.len() as u64, + timestamp: i as u64, + }; + + let ops = if current_header == first_block_header { + // first-ever block -- add only the leader key + let mut key_insert = key.clone(); + key_insert.burn_header_hash = block_header.block_hash.clone(); + + test_debug!( + "Insert key-register in {}: {},{},{} in block {}", + &key_insert.burn_header_hash, + &key_insert.txid, + key_insert.block_height, + key_insert.vtxindex, + block_header.block_height + ); + + new_commits.push(vec![None; parent_commits.len()]); + vec![BlockstackOperationType::LeaderKeyRegister( + key_insert.clone(), + )] + } else { + let mut commits = vec![]; + for i in 0..parent_commits.len() { + let mut block_commit = make_simple_block_commit( + burnchain, + parent_commits[i].as_ref(), + &block_header, + next_block_hash(), + ); + block_commit.key_block_ptr = key.block_height as u32; + block_commit.key_vtxindex = key.vtxindex as u16; + block_commit.vtxindex += i as u32; + block_commit.burn_parent_modulus = if height > 0 { + ((height - 1) % BURN_BLOCK_MINED_AT_MODULUS) as u8 + } else { + BURN_BLOCK_MINED_AT_MODULUS as u8 - 1 + }; + + assert_eq!(block_commit.burn_header_hash, block_header.block_hash); + assert_eq!(block_commit.block_height, block_header.block_height); + + test_debug!( + "Insert block-commit in {}: {},{},{}, builds on {},{}", + &block_commit.burn_header_hash, + &block_commit.txid, + block_commit.block_height, + block_commit.vtxindex, + block_commit.parent_block_ptr, + block_commit.parent_vtxindex + ); + + if let Some(parent_commit) = parent_commits[i].as_ref() { + assert!(parent_commit.block_height != block_commit.block_height); + assert!( + parent_commit.block_height == u64::from(block_commit.parent_block_ptr) + ); + assert!(parent_commit.vtxindex == u32::from(block_commit.parent_vtxindex)); + } + + parent_commits[i] = Some(block_commit.clone()); + commits.push(Some(block_commit.clone())); + } + new_commits.push(commits.clone()); + commits + .into_iter() + .flatten() + .map(BlockstackOperationType::LeaderBlockCommit) + .collect() + }; + + burnchain_db + .store_new_burnchain_block_ops_unchecked(&block_header, &ops) + .unwrap(); + + headers.push(block_header.clone()); + new_headers.push(block_header.clone()); + parent_block_header = Some(block_header); + + current_header = burnchain_db.get_canonical_chain_tip().unwrap(); + height = current_header.block_height + 1; + } + + (new_headers, new_commits) + } + + /// Conveninece wrapper that produces a reward cycle with one sequence of block-commits. Returns + /// the sequence of block headers in this reward cycle, and the list of block-commits created. If + /// parent_commit is None, then the list of block-commits will contain all None's. + fn make_simple_reward_cycle( + burnchain_db: &mut BurnchainDB, + burnchain: &Burnchain, + key: &LeaderKeyRegisterOp, + headers: &mut Vec, + parent_commit: Option, + ) -> (Vec, Vec>) { + let (new_headers, commits) = + make_reward_cycle(burnchain_db, burnchain, key, headers, vec![parent_commit]); + ( + new_headers, + commits + .into_iter() + .map(|mut cmts| cmts.pop().unwrap()) + .collect(), + ) + } + impl SortitionHandleTx<'_> { /// Update the canonical Stacks tip (testing only) pub fn test_update_canonical_stacks_tip( From 82d87e9bd1712a3db12629be422014ac6c7d92ac Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 3 Sep 2025 14:43:57 -0700 Subject: [PATCH 32/35] Fix supports_epoch Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/stacks/db/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index d341261ef7..44aa0d7dfe 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -283,16 +283,16 @@ impl DBConfig { }); match epoch_id { StacksEpochId::Epoch10 => true, - StacksEpochId::Epoch20 => version_u32 >= 1 && version_u32 <= 10, - StacksEpochId::Epoch2_05 => version_u32 >= 2 && version_u32 <= 10, - StacksEpochId::Epoch21 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch22 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch23 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch24 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch25 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch30 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch31 => version_u32 >= 3 && version_u32 <= 10, - StacksEpochId::Epoch32 => version_u32 >= 3 && version_u32 <= 10, + StacksEpochId::Epoch20 => (1..=11).contains(&version_u32), + StacksEpochId::Epoch2_05 => (2..=11).contains(&version_u32), + StacksEpochId::Epoch21 + | StacksEpochId::Epoch22 + | StacksEpochId::Epoch23 + | StacksEpochId::Epoch24 + | StacksEpochId::Epoch25 + | StacksEpochId::Epoch30 + | StacksEpochId::Epoch31 + | StacksEpochId::Epoch32 => (3..=11).contains(&version_u32), } } } From a9aa74e26b91051e0a449ba844331f49ac769e04 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 4 Sep 2025 08:38:44 -0700 Subject: [PATCH 33/35] Remove pox flapping/missing anchor block tests that no longer are relevant Signed-off-by: Jacinta Ferrant --- .github/workflows/bitcoin-tests.yml | 6 - stacks-node/src/tests/epoch_21.rs | 2090 --------------------------- stacks-node/src/tests/epoch_22.rs | 408 ------ 3 files changed, 2504 deletions(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 41e1f95bc4..d85ef0291b 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -80,11 +80,6 @@ jobs: - test-name: tests::epoch_205::test_dynamic_db_method_costs - test-name: tests::epoch_205::test_exact_block_costs - test-name: tests::epoch_205::transition_empty_blocks - - test-name: tests::epoch_21::test_pox_missing_five_anchor_blocks - - test-name: tests::epoch_21::test_pox_reorg_flap_duel - - test-name: tests::epoch_21::test_pox_reorg_flap_reward_cycles - - test-name: tests::epoch_21::test_pox_reorg_one_flap - - test-name: tests::epoch_21::test_pox_reorgs_three_flaps - test-name: tests::epoch_21::test_sortition_divergence_pre_21 - test-name: tests::epoch_21::test_v1_unlock_height_with_current_stackers - test-name: tests::epoch_21::test_v1_unlock_height_with_delay_and_current_stackers @@ -99,7 +94,6 @@ jobs: - test-name: tests::epoch_21::transition_removes_pox_sunset - test-name: tests::epoch_22::disable_pox - test-name: tests::epoch_22::pox_2_unlock_all - - test-name: tests::epoch_22::test_pox_reorg_one_flap - test-name: tests::epoch_23::trait_invocation_behavior - test-name: tests::epoch_24::fix_to_pox_contract - test-name: tests::epoch_24::verify_auto_unlock_behavior diff --git a/stacks-node/src/tests/epoch_21.rs b/stacks-node/src/tests/epoch_21.rs index 5c80fc1908..a2ef04fb03 100644 --- a/stacks-node/src/tests/epoch_21.rs +++ b/stacks-node/src/tests/epoch_21.rs @@ -1973,2096 +1973,6 @@ pub fn wait_pox_stragglers(confs: &[Config], max_stacks_tip: u64, block_time_ms: } } -/// PoX reorg with three flaps. -/// Miner 0 mines and hides the anchor block for cycles 22. -/// Miner 1 mines and hides the anchor block for cycles 23 and 24, causing a PoX reorg in miner 0. -/// Miner 0 mines and hides the anchor block for cycles 25, 26, and 27, causing a PoX reorg in miner 1. -/// At the very end, miners stop hiding their blocks, and the test verifies that both miners -/// converge on having anchor blocks for cycles 22, 25, 26, and 27, but not 23 and 24. -#[test] -#[ignore] -fn test_pox_reorgs_three_flaps() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_miners = 2; - - let reward_cycle_len = 10; - let prepare_phase_len = 3; - let v1_unlock_height = 152; - - let (mut conf_template, _) = neon_integration_test_conf(); - let block_time_ms = 10_000; - conf_template.node.mine_microblocks = true; - conf_template.miner.microblock_attempt_time_ms = 2_000; - conf_template.node.wait_time_for_microblocks = 0; - conf_template.node.microblock_frequency = 0; - conf_template.miner.first_attempt_time_ms = 2_000; - conf_template.miner.subsequent_attempt_time_ms = 5_000; - conf_template.burnchain.max_rbf = 1000000; - conf_template.node.wait_time_for_blocks = 1_000; - conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - - // make epoch 2.1 start in the middle of boot-up - let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); - epochs[StacksEpochId::Epoch20].end_height = 101; - epochs[StacksEpochId::Epoch2_05].start_height = 101; - epochs[StacksEpochId::Epoch2_05].end_height = 151; - epochs[StacksEpochId::Epoch21].start_height = 151; - conf_template.burnchain.epochs = Some(epochs); - - let privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let stack_privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let balances: Vec<_> = privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 30_000_000, - } - }) - .collect(); - - let stack_balances: Vec<_> = stack_privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 2_000_000_000_000_000, - } - }) - .collect(); - - let mut confs = vec![]; - let mut burnchain_configs = vec![]; - let mut blocks_processed = vec![]; - let mut channels = vec![]; - let mut miner_status = vec![]; - - for i in 0..num_miners { - let seed = StacksPrivateKey::random().to_bytes(); - let (mut conf, _) = neon_integration_test_conf_with_seed(seed); - - conf.initial_balances.clear(); - conf.initial_balances.append(&mut balances.clone()); - conf.initial_balances.append(&mut stack_balances.clone()); - - conf.node.mine_microblocks = conf_template.node.mine_microblocks; - conf.miner.microblock_attempt_time_ms = conf_template.miner.microblock_attempt_time_ms; - conf.node.wait_time_for_microblocks = conf_template.node.wait_time_for_microblocks; - conf.node.microblock_frequency = conf_template.node.microblock_frequency; - conf.miner.first_attempt_time_ms = conf_template.miner.first_attempt_time_ms; - conf.miner.subsequent_attempt_time_ms = conf_template.miner.subsequent_attempt_time_ms; - conf.node.wait_time_for_blocks = conf_template.node.wait_time_for_blocks; - conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; - conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); - conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - - // multiple nodes so they must download from each other - conf.miner.wait_for_block_download = true; - - // nodes will selectively hide blocks from one another - conf.node.fault_injection_hide_blocks = true; - - let rpc_port = 41043 + 10 * i; - let p2p_port = 41043 + 10 * i + 1; - conf.node.rpc_bind = format!("127.0.0.1:{rpc_port}"); - conf.node.data_url = format!("http://127.0.0.1:{rpc_port}"); - conf.node.p2p_bind = format!("127.0.0.1:{p2p_port}"); - - // conf.connection_options.inv_reward_cycles = 10; - - confs.push(conf); - } - - let node_privkey_1 = Secp256k1PrivateKey::from_seed(&confs[0].node.local_peer_seed); - let chain_id = confs[0].burnchain.chain_id; - let peer_version = confs[0].burnchain.peer_version; - let p2p_bind = confs[0].node.p2p_bind.clone(); - for conf in confs.iter_mut().skip(1) { - conf.node.set_bootstrap_nodes( - format!( - "{}@{p2p_bind}", - &StacksPublicKey::from_private(&node_privkey_1).to_hex() - ), - chain_id, - peer_version, - ); - } - - // use short reward cycles - for conf in &confs { - let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new( - reward_cycle_len, - prepare_phase_len, - 4 * prepare_phase_len / 5, - 5, - 15, - (1600 * reward_cycle_len - 1).into(), - (1700 * reward_cycle_len).into(), - v1_unlock_height, - u32::MAX, - u32::MAX, - u32::MAX, - ); - burnchain_config.pox_constants = pox_constants.clone(); - - burnchain_configs.push(burnchain_config); - } - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&confs[0]); - btcd_controller - .start_bitcoind() - .map_err(|_e| ()) - .expect("Failed starting bitcoind"); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - confs[0].clone(), - None, - Some(burnchain_configs[0].clone()), - None, - ); - - btc_regtest_controller.bootstrap_chain(1); - - // make sure all miners have BTC - for conf in confs.iter().skip(1) { - let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); - btc_regtest_controller - .set_mining_pubkey(conf.burnchain.local_mining_public_key.clone().unwrap()); - btc_regtest_controller.bootstrap_chain(1); - btc_regtest_controller.set_mining_pubkey(old_mining_pubkey); - } - - btc_regtest_controller.bootstrap_chain((199 - num_miners) as u64); - - eprintln!("Chain bootstrapped..."); - - for (i, burnchain_config) in burnchain_configs.into_iter().enumerate() { - let mut run_loop = neon::RunLoop::new(confs[i].clone()); - let blocks_processed_arc = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - let this_miner_status = run_loop.get_miner_status(); - - blocks_processed.push(blocks_processed_arc); - channels.push(channel); - miner_status.push(this_miner_status); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - } - - let http_origin = format!("http://{}", &confs[0].node.rpc_bind); - - // give the run loops some time to start up! - for bp in &blocks_processed { - wait_for_runloop(bp); - } - - // activate miners - eprintln!("\n\nBoot miner 0\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(&confs[0]); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner 0: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner 0...\n\n"); - } - next_block_and_iterate( - &mut btc_regtest_controller, - &blocks_processed[0], - block_time_ms, - ); - } - - for (i, conf) in confs.iter().enumerate().skip(1) { - eprintln!("\n\nBoot miner {i}\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(conf); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner {i}: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner {i}...\n\n"); - } - next_block_and_iterate(&mut btc_regtest_controller, &blocks_processed[i], 5_000); - } - } - - eprintln!("\n\nBegin transactions\n\n"); - - let pox_pubkey = Secp256k1PublicKey::from_hex( - "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", - ) - .unwrap(); - let pox_pubkey_hash = bytes_to_hex(&Hash160::from_node_public_key(&pox_pubkey).to_bytes()); - - let sort_height = channels[0].get_sortitions_processed(); - - // make everyone stack - let stacking_txs: Vec<_> = stack_privks - .iter() - .map(|pk| { - make_contract_call( - pk, - 0, - 1360, - conf_template.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "pox-2", - "stack-stx", - &[ - Value::UInt(2_000_000_000_000_000 - 30_000_000), - execute( - &format!("{{ hashbytes: 0x{pox_pubkey_hash}, version: 0x00 }}"), - ClarityVersion::Clarity1, - ) - .unwrap() - .unwrap(), - Value::UInt((sort_height + 1) as u128), - Value::UInt(12), - ], - ) - }) - .collect(); - - // keeps the mempool full, and makes it so miners will spend a nontrivial amount of time - // building blocks - let all_txs: Vec<_> = privks - .iter() - .enumerate() - .map(|(i, pk)| { - make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false) - }) - .collect(); - - // everyone locks up - for (cnt, tx) in stacking_txs.iter().enumerate() { - eprintln!("\n\nSubmit stacking tx {cnt}\n\n"); - submit_tx(&http_origin, tx); - } - - // run a reward cycle - let mut at_220 = false; - while !at_220 { - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - if tip_info.burn_block_height == 220 { - at_220 = true; - } - } - } - - // blast out the rest - let mut cnt = 0; - for tx_chain in all_txs { - for tx in tx_chain { - eprintln!("\n\nSubmit tx {cnt}\n\n"); - submit_tx(&http_origin, &tx); - cnt += 1; - } - } - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - assert!(tip_info.burn_block_height <= 220); - } - - eprintln!("\n\nBegin mining\n\n"); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // prevent Stacks at these heights from propagating - env::set_var( - "STACKS_HIDE_BLOCKS_AT_HEIGHT", - "[226,227,228,229,230,236,237,238,239,240,246,247,248,249,250,256,257,258,259,260,266,267,268,269,270,276,277,278,279,280,286,287,288,289,290]" - ); - - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 1 mines a prepare phase and confirms a hidden anchor block. - // miner 0 is disabled for this prepare phase - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[0].clone()); - } - } - signal_mining_ready(miner_status[0].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - // miner 1's history overtakes miner 0's. - // Miner 1 didn't see cycle 22's anchor block, but it just mined an anchor block for cycle - // 23 and affirmed cycle 22's anchor block's absence. - } - info!("####################### end of cycle ##############################"); - - // miner 1 mines a prepare phase and confirms a hidden anchor block. - // miner 0 is disabled for this prepare phase - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[0].clone()); - } - } - signal_mining_ready(miner_status[0].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - // miner 1's history continues to overtake miner 0's. - // Miner 1 didn't see cycle 22's anchor block, but it just mined an anchor block for cycle - // 23 and cycle 24 which both affirm cycle 22's anchor block's absence. - } - info!("####################### end of cycle ##############################"); - - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - let mut max_stacks_tip = 0; - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - info!("####################### end of cycle ##############################"); - - // advance to start of next reward cycle - eprintln!("\n\nBuild final block\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - // resume block propagation - env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); - - // wait for all blocks to propagate - eprintln!("Wait for all blocks to propagate; max tip is {max_stacks_tip}"); - wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Final tip for miner {i}: {tip_info:?}"); - } -} - -/// PoX reorg with just one flap. -/// Miner 0 mines and hides the anchor block for cycle 22. -/// Miner 1 mines and hides the anchor block for cycle 23, causing a PoX reorg in miner 0. -/// At the very end, miners stop hiding their blocks, and the test verifies that both miners -/// converge on having anchor blocks for cycles 22 and 24, but not 23. -#[test] -#[ignore] -fn test_pox_reorg_one_flap() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_miners = 2; - - let reward_cycle_len = 10; - let prepare_phase_len = 3; - let v1_unlock_height = 152; - - let (mut conf_template, _) = neon_integration_test_conf(); - let block_time_ms = 10_000; - conf_template.node.mine_microblocks = true; - conf_template.miner.microblock_attempt_time_ms = 2_000; - conf_template.node.wait_time_for_microblocks = 0; - conf_template.node.microblock_frequency = 0; - conf_template.miner.first_attempt_time_ms = 2_000; - conf_template.miner.subsequent_attempt_time_ms = 5_000; - conf_template.burnchain.max_rbf = 1000000; - conf_template.node.wait_time_for_blocks = 1_000; - conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - - // make epoch 2.1 start in the middle of boot-up - let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); - epochs[StacksEpochId::Epoch20].end_height = 101; - epochs[StacksEpochId::Epoch2_05].start_height = 101; - epochs[StacksEpochId::Epoch2_05].end_height = 151; - epochs[StacksEpochId::Epoch21].start_height = 151; - conf_template.burnchain.epochs = Some(epochs); - - let privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let stack_privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let balances: Vec<_> = privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 30_000_000, - } - }) - .collect(); - - let stack_balances: Vec<_> = stack_privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 2_000_000_000_000_000, - } - }) - .collect(); - - let mut confs = vec![]; - let mut burnchain_configs = vec![]; - let mut blocks_processed = vec![]; - let mut channels = vec![]; - let mut miner_status = vec![]; - - for i in 0..num_miners { - let seed = StacksPrivateKey::random().to_bytes(); - let (mut conf, _) = neon_integration_test_conf_with_seed(seed); - - conf.initial_balances.clear(); - conf.initial_balances.append(&mut balances.clone()); - conf.initial_balances.append(&mut stack_balances.clone()); - - conf.node.mine_microblocks = conf_template.node.mine_microblocks; - conf.miner.microblock_attempt_time_ms = conf_template.miner.microblock_attempt_time_ms; - conf.node.wait_time_for_microblocks = conf_template.node.wait_time_for_microblocks; - conf.node.microblock_frequency = conf_template.node.microblock_frequency; - conf.miner.first_attempt_time_ms = conf_template.miner.first_attempt_time_ms; - conf.miner.subsequent_attempt_time_ms = conf_template.miner.subsequent_attempt_time_ms; - conf.node.wait_time_for_blocks = conf_template.node.wait_time_for_blocks; - conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; - conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); - conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - - // multiple nodes so they must download from each other - conf.miner.wait_for_block_download = true; - - // nodes will selectively hide blocks from one another - conf.node.fault_injection_hide_blocks = true; - - let rpc_port = 41063 + 10 * i; - let p2p_port = 41063 + 10 * i + 1; - conf.node.rpc_bind = format!("127.0.0.1:{rpc_port}"); - conf.node.data_url = format!("http://127.0.0.1:{rpc_port}"); - conf.node.p2p_bind = format!("127.0.0.1:{p2p_port}"); - - confs.push(conf); - } - - let node_privkey_1 = Secp256k1PrivateKey::from_seed(&confs[0].node.local_peer_seed); - let chain_id = confs[0].burnchain.chain_id; - let peer_version = confs[0].burnchain.peer_version; - let p2p_bind = confs[0].node.p2p_bind.clone(); - for conf in confs.iter_mut().skip(1) { - conf.node.set_bootstrap_nodes( - format!( - "{}@{p2p_bind}", - &StacksPublicKey::from_private(&node_privkey_1).to_hex() - ), - chain_id, - peer_version, - ); - } - - // use short reward cycles - for conf in &confs { - let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new( - reward_cycle_len, - prepare_phase_len, - 4 * prepare_phase_len / 5, - 5, - 15, - (1600 * reward_cycle_len - 1).into(), - (1700 * reward_cycle_len).into(), - v1_unlock_height, - u32::MAX, - u32::MAX, - u32::MAX, - ); - burnchain_config.pox_constants = pox_constants.clone(); - - burnchain_configs.push(burnchain_config); - } - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&confs[0]); - btcd_controller - .start_bitcoind() - .map_err(|_e| ()) - .expect("Failed starting bitcoind"); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - confs[0].clone(), - None, - Some(burnchain_configs[0].clone()), - None, - ); - - btc_regtest_controller.bootstrap_chain(1); - - // make sure all miners have BTC - for conf in confs.iter().skip(1) { - let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); - btc_regtest_controller - .set_mining_pubkey(conf.burnchain.local_mining_public_key.clone().unwrap()); - btc_regtest_controller.bootstrap_chain(1); - btc_regtest_controller.set_mining_pubkey(old_mining_pubkey); - } - - btc_regtest_controller.bootstrap_chain((199 - num_miners) as u64); - - eprintln!("Chain bootstrapped..."); - - for (i, burnchain_config) in burnchain_configs.into_iter().enumerate() { - let mut run_loop = neon::RunLoop::new(confs[i].clone()); - let blocks_processed_arc = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - let this_miner_status = run_loop.get_miner_status(); - - blocks_processed.push(blocks_processed_arc); - channels.push(channel); - miner_status.push(this_miner_status); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - } - - let http_origin = format!("http://{}", &confs[0].node.rpc_bind); - - // give the run loops some time to start up! - for bp in &blocks_processed { - wait_for_runloop(bp); - } - - // activate miners - eprintln!("\n\nBoot miner 0\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(&confs[0]); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner 0: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner 0...\n\n"); - } - next_block_and_iterate( - &mut btc_regtest_controller, - &blocks_processed[0], - block_time_ms, - ); - } - - for (i, conf) in confs.iter().enumerate().skip(1) { - eprintln!("\n\nBoot miner {i}\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(conf); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner {i}: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner {i}...\n\n"); - } - next_block_and_iterate(&mut btc_regtest_controller, &blocks_processed[i], 5_000); - } - } - - eprintln!("\n\nBegin transactions\n\n"); - - let pox_pubkey = Secp256k1PublicKey::from_hex( - "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", - ) - .unwrap(); - let pox_pubkey_hash = bytes_to_hex(&Hash160::from_node_public_key(&pox_pubkey).to_bytes()); - - let sort_height = channels[0].get_sortitions_processed(); - - // make everyone stack - let stacking_txs: Vec<_> = stack_privks - .iter() - .map(|pk| { - make_contract_call( - pk, - 0, - 1360, - conf_template.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "pox-2", - "stack-stx", - &[ - Value::UInt(2_000_000_000_000_000 - 30_000_000), - execute( - &format!("{{ hashbytes: 0x{pox_pubkey_hash}, version: 0x00 }}"), - ClarityVersion::Clarity1, - ) - .unwrap() - .unwrap(), - Value::UInt((sort_height + 1) as u128), - Value::UInt(12), - ], - ) - }) - .collect(); - - // keeps the mempool full, and makes it so miners will spend a nontrivial amount of time - // building blocks - let all_txs: Vec<_> = privks - .iter() - .enumerate() - .map(|(i, pk)| { - make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false) - }) - .collect(); - - // everyone locks up - for (cnt, tx) in stacking_txs.iter().enumerate() { - eprintln!("\n\nSubmit stacking tx {cnt}\n\n"); - submit_tx(&http_origin, tx); - } - - // run a reward cycle - let mut at_220 = false; - while !at_220 { - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - if tip_info.burn_block_height == 220 { - at_220 = true; - } - } - } - - // blast out the rest - let mut cnt = 0; - for tx_chain in all_txs { - for tx in tx_chain { - eprintln!("\n\nSubmit tx {cnt}\n\n"); - submit_tx(&http_origin, &tx); - cnt += 1; - } - } - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - assert!(tip_info.burn_block_height <= 220); - } - - eprintln!("\n\nBegin mining\n\n"); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // prevent Stacks at these heights from propagating - env::set_var( - "STACKS_HIDE_BLOCKS_AT_HEIGHT", - "[226,227,228,229,230,236,237,238,239,240,246,247,248,249,250,256,257,258,259,260,266,267,268,269,270,276,277,278,279,280,286,287,288,289,290]" - ); - - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 1 mines a prepare phase and confirms a hidden anchor block. - // miner 0 is disabled for this prepare phase - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[0].clone()); - } - } - signal_mining_ready(miner_status[0].clone()); - - info!("####################### end of cycle ##############################"); - let mut max_stacks_tip = 0; - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - // miner 1's history overtakes miner 0's. - // Miner 1 didn't see cycle 22's anchor block, but it just mined an anchor block for cycle - // 23 and affirmed cycle 22's anchor block's absence. - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - info!("####################### end of cycle ##############################"); - - // advance to start of next reward cycle - eprintln!("\n\nBuild final block\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - // resume block propagation - env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); - - // wait for all blocks to propagate - eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); - wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Final tip for miner {i}: {tip_info:?}"); - } -} - -/// PoX reorg tests where two miners take turn mining hidden anchor blocks. -/// Both miners mine in the reward phase, and in doing so, confirm their hidden anchor blocks. -#[test] -#[ignore] -fn test_pox_reorg_flap_duel() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_miners = 2; - - let reward_cycle_len = 10; - let prepare_phase_len = 3; - let v1_unlock_height = 152; - - let (mut conf_template, _) = neon_integration_test_conf(); - let block_time_ms = 10_000; - conf_template.node.mine_microblocks = true; - conf_template.miner.microblock_attempt_time_ms = 2_000; - conf_template.node.wait_time_for_microblocks = 0; - conf_template.node.microblock_frequency = 0; - conf_template.miner.first_attempt_time_ms = 2_000; - conf_template.miner.subsequent_attempt_time_ms = 5_000; - conf_template.burnchain.max_rbf = 1000000; - conf_template.node.wait_time_for_blocks = 1_000; - conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - - // make epoch 2.1 start in the middle of boot-up - let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); - epochs[StacksEpochId::Epoch20].end_height = 101; - epochs[StacksEpochId::Epoch2_05].start_height = 101; - epochs[StacksEpochId::Epoch2_05].end_height = 151; - epochs[StacksEpochId::Epoch21].start_height = 151; - conf_template.burnchain.epochs = Some(epochs); - - let privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let stack_privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let balances: Vec<_> = privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 30_000_000, - } - }) - .collect(); - - let stack_balances: Vec<_> = stack_privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 2_000_000_000_000_000, - } - }) - .collect(); - - let mut confs = vec![]; - let mut burnchain_configs = vec![]; - let mut blocks_processed = vec![]; - let mut channels = vec![]; - let mut miner_status = vec![]; - - for i in 0..num_miners { - let seed = StacksPrivateKey::random().to_bytes(); - let (mut conf, _) = neon_integration_test_conf_with_seed(seed); - - conf.initial_balances.clear(); - conf.initial_balances.append(&mut balances.clone()); - conf.initial_balances.append(&mut stack_balances.clone()); - - conf.node.mine_microblocks = conf_template.node.mine_microblocks; - conf.miner.microblock_attempt_time_ms = conf_template.miner.microblock_attempt_time_ms; - conf.node.wait_time_for_microblocks = conf_template.node.wait_time_for_microblocks; - conf.node.microblock_frequency = conf_template.node.microblock_frequency; - conf.miner.first_attempt_time_ms = conf_template.miner.first_attempt_time_ms; - conf.miner.subsequent_attempt_time_ms = conf_template.miner.subsequent_attempt_time_ms; - conf.node.wait_time_for_blocks = conf_template.node.wait_time_for_blocks; - conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; - conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); - conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - - // multiple nodes so they must download from each other - conf.miner.wait_for_block_download = true; - - // nodes will selectively hide blocks from one another - conf.node.fault_injection_hide_blocks = true; - - // conf.connection_options.inv_reward_cycles = 10; - - let rpc_port = 41083 + 10 * i; - let p2p_port = 41083 + 10 * i + 1; - conf.node.rpc_bind = format!("127.0.0.1:{rpc_port}"); - conf.node.data_url = format!("http://127.0.0.1:{rpc_port}"); - conf.node.p2p_bind = format!("127.0.0.1:{p2p_port}"); - - confs.push(conf); - } - - let node_privkey_1 = Secp256k1PrivateKey::from_seed(&confs[0].node.local_peer_seed); - let chain_id = confs[0].burnchain.chain_id; - let peer_version = confs[0].burnchain.peer_version; - let p2p_bind = confs[0].node.p2p_bind.clone(); - - for conf in confs.iter_mut().skip(1) { - conf.node.set_bootstrap_nodes( - format!( - "{}@{p2p_bind}", - &StacksPublicKey::from_private(&node_privkey_1).to_hex() - ), - chain_id, - peer_version, - ); - } - - // use short reward cycles - for conf in &confs { - let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new( - reward_cycle_len, - prepare_phase_len, - 4 * prepare_phase_len / 5, - 5, - 15, - (1600 * reward_cycle_len - 1).into(), - (1700 * reward_cycle_len).into(), - v1_unlock_height, - u32::MAX, - u32::MAX, - u32::MAX, - ); - burnchain_config.pox_constants = pox_constants.clone(); - - burnchain_configs.push(burnchain_config); - } - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&confs[0]); - btcd_controller - .start_bitcoind() - .map_err(|_e| ()) - .expect("Failed starting bitcoind"); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - confs[0].clone(), - None, - Some(burnchain_configs[0].clone()), - None, - ); - - btc_regtest_controller.bootstrap_chain(1); - - // make sure all miners have BTC - for conf in confs.iter().skip(1) { - let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); - btc_regtest_controller - .set_mining_pubkey(conf.burnchain.local_mining_public_key.clone().unwrap()); - btc_regtest_controller.bootstrap_chain(1); - btc_regtest_controller.set_mining_pubkey(old_mining_pubkey); - } - - btc_regtest_controller.bootstrap_chain((199 - num_miners) as u64); - - eprintln!("Chain bootstrapped..."); - - for (i, burnchain_config) in burnchain_configs.into_iter().enumerate() { - let mut run_loop = neon::RunLoop::new(confs[i].clone()); - let blocks_processed_arc = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - let this_miner_status = run_loop.get_miner_status(); - - blocks_processed.push(blocks_processed_arc); - channels.push(channel); - miner_status.push(this_miner_status); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - } - - let http_origin = format!("http://{}", &confs[0].node.rpc_bind); - - // give the run loops some time to start up! - for bp in &blocks_processed { - wait_for_runloop(bp); - } - - // activate miners - eprintln!("\n\nBoot miner 0\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(&confs[0]); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner 0: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner 0...\n\n"); - } - next_block_and_iterate( - &mut btc_regtest_controller, - &blocks_processed[0], - block_time_ms, - ); - } - - for (i, conf) in confs.iter().enumerate().skip(1) { - eprintln!("\n\nBoot miner {i}\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(conf); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner {i}: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner {i}...\n\n"); - } - next_block_and_iterate(&mut btc_regtest_controller, &blocks_processed[i], 5_000); - } - } - - eprintln!("\n\nBegin transactions\n\n"); - - let pox_pubkey = Secp256k1PublicKey::from_hex( - "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", - ) - .unwrap(); - let pox_pubkey_hash = bytes_to_hex(&Hash160::from_node_public_key(&pox_pubkey).to_bytes()); - - let sort_height = channels[0].get_sortitions_processed(); - - // make everyone stack - let stacking_txs: Vec<_> = stack_privks - .iter() - .map(|pk| { - make_contract_call( - pk, - 0, - 1360, - conf_template.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "pox-2", - "stack-stx", - &[ - Value::UInt(2_000_000_000_000_000 - 30_000_000), - execute( - &format!("{{ hashbytes: 0x{pox_pubkey_hash}, version: 0x00 }}"), - ClarityVersion::Clarity1, - ) - .unwrap() - .unwrap(), - Value::UInt((sort_height + 1) as u128), - Value::UInt(12), - ], - ) - }) - .collect(); - - // keeps the mempool full, and makes it so miners will spend a nontrivial amount of time - // building blocks - let all_txs: Vec<_> = privks - .iter() - .enumerate() - .map(|(i, pk)| { - make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false) - }) - .collect(); - - // everyone locks up - for (cnt, tx) in stacking_txs.iter().enumerate() { - eprintln!("\n\nSubmit stacking tx {cnt}\n\n"); - submit_tx(&http_origin, tx); - } - - // run a reward cycle - let mut at_220 = false; - while !at_220 { - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - if tip_info.burn_block_height == 220 { - at_220 = true; - } - } - } - - // blast out the rest - let mut cnt = 0; - for tx_chain in all_txs { - for tx in tx_chain { - eprintln!("\n\nSubmit tx {cnt}\n\n"); - submit_tx(&http_origin, &tx); - cnt += 1; - } - } - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - assert!(tip_info.burn_block_height <= 220); - } - - eprintln!("\n\nBegin mining\n\n"); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // prevent Stacks at these heights from propagating. - env::set_var( - "STACKS_HIDE_BLOCKS_AT_HEIGHT", - "[226,227,228,229,230,236,237,238,239,240,246,247,248,249,250,256,257,258,259,260,266,267,268,269,270,276,277,278,279,280,286,287,288,289,290]" - ); - - let mut max_stacks_tip = 0; - - // miners 0 and 1 take turns mining anchor blocks. - // this should cause them both to flip/flop their sortition histories multiple times - for _c in 0..3 { - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 1 mines a prepare phase and confirms a hidden anchor block. - // miner 0 is disabled for this prepare phase - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[0].clone()); - } - } - signal_mining_ready(miner_status[0].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - // miner 1's history overtakes miner 0's. - // Miner 1 didn't see cycle 22's anchor block, but it just mined an anchor block for cycle - // 23 and affirmed cycle 22's anchor block's absence. - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - info!("####################### end of cycle ##############################"); - } - - // advance to start of next reward cycle - eprintln!("\n\nBuild final block\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - // resume block propagation - env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); - - // wait for all blocks to propagate - eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); - wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Final tip for miner {i}: {tip_info:?}"); - } -} - -/// PoX reorg tests where two miners take turn mining hidden reward cycles. -/// Miners take turn mining entire reward cycles, and deny each other to build on them. -#[test] -#[ignore] -fn test_pox_reorg_flap_reward_cycles() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_miners = 2; - - let reward_cycle_len = 10; - let prepare_phase_len = 3; - let v1_unlock_height = 152; - - let (mut conf_template, _) = neon_integration_test_conf(); - let block_time_ms = 10_000; - conf_template.node.mine_microblocks = true; - conf_template.miner.microblock_attempt_time_ms = 2_000; - conf_template.node.wait_time_for_microblocks = 0; - conf_template.node.microblock_frequency = 0; - conf_template.miner.first_attempt_time_ms = 2_000; - conf_template.miner.subsequent_attempt_time_ms = 5_000; - conf_template.burnchain.max_rbf = 1000000; - conf_template.node.wait_time_for_blocks = 1_000; - conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - - // make epoch 2.1 start in the middle of boot-up - let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); - epochs[StacksEpochId::Epoch20].end_height = 101; - epochs[StacksEpochId::Epoch2_05].start_height = 101; - epochs[StacksEpochId::Epoch2_05].end_height = 151; - epochs[StacksEpochId::Epoch21].start_height = 151; - conf_template.burnchain.epochs = Some(epochs); - - let privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let stack_privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let balances: Vec<_> = privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 30_000_000, - } - }) - .collect(); - - let stack_balances: Vec<_> = stack_privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 2_000_000_000_000_000, - } - }) - .collect(); - - let mut confs = vec![]; - let mut burnchain_configs = vec![]; - let mut blocks_processed = vec![]; - let mut channels = vec![]; - let mut miner_status = vec![]; - - for i in 0..num_miners { - let seed = StacksPrivateKey::random().to_bytes(); - let (mut conf, _) = neon_integration_test_conf_with_seed(seed); - - conf.initial_balances.clear(); - conf.initial_balances.append(&mut balances.clone()); - conf.initial_balances.append(&mut stack_balances.clone()); - - conf.node.mine_microblocks = conf_template.node.mine_microblocks; - conf.miner.microblock_attempt_time_ms = conf_template.miner.microblock_attempt_time_ms; - conf.node.wait_time_for_microblocks = conf_template.node.wait_time_for_microblocks; - conf.node.microblock_frequency = conf_template.node.microblock_frequency; - conf.miner.first_attempt_time_ms = conf_template.miner.first_attempt_time_ms; - conf.miner.subsequent_attempt_time_ms = conf_template.miner.subsequent_attempt_time_ms; - conf.node.wait_time_for_blocks = conf_template.node.wait_time_for_blocks; - conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; - conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); - conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - - // multiple nodes so they must download from each other - conf.miner.wait_for_block_download = true; - - // nodes will selectively hide blocks from one another - conf.node.fault_injection_hide_blocks = true; - - let rpc_port = 41123 + 10 * i; - let p2p_port = 41123 + 10 * i + 1; - conf.node.rpc_bind = format!("127.0.0.1:{rpc_port}"); - conf.node.data_url = format!("http://127.0.0.1:{rpc_port}"); - conf.node.p2p_bind = format!("127.0.0.1:{p2p_port}"); - - confs.push(conf); - } - - let node_privkey_1 = Secp256k1PrivateKey::from_seed(&confs[0].node.local_peer_seed); - let chain_id = confs[0].burnchain.chain_id; - let peer_version = confs[0].burnchain.peer_version; - let p2p_bind = confs[0].node.p2p_bind.clone(); - for conf in confs.iter_mut().skip(1) { - conf.node.set_bootstrap_nodes( - format!( - "{}@{p2p_bind}", - &StacksPublicKey::from_private(&node_privkey_1).to_hex() - ), - chain_id, - peer_version, - ); - } - - // use short reward cycles - for conf in confs.iter() { - let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new( - reward_cycle_len, - prepare_phase_len, - 4 * prepare_phase_len / 5, - 5, - 15, - (1600 * reward_cycle_len - 1).into(), - (1700 * reward_cycle_len).into(), - v1_unlock_height, - u32::MAX, - u32::MAX, - u32::MAX, - ); - burnchain_config.pox_constants = pox_constants.clone(); - - burnchain_configs.push(burnchain_config); - } - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&confs[0]); - btcd_controller - .start_bitcoind() - .map_err(|_e| ()) - .expect("Failed starting bitcoind"); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - confs[0].clone(), - None, - Some(burnchain_configs[0].clone()), - None, - ); - - btc_regtest_controller.bootstrap_chain(1); - - // make sure all miners have BTC - for conf in confs.iter().skip(1) { - let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); - btc_regtest_controller - .set_mining_pubkey(conf.burnchain.local_mining_public_key.clone().unwrap()); - btc_regtest_controller.bootstrap_chain(1); - btc_regtest_controller.set_mining_pubkey(old_mining_pubkey); - } - - btc_regtest_controller.bootstrap_chain((199 - num_miners) as u64); - - eprintln!("Chain bootstrapped..."); - - for (i, burnchain_config) in burnchain_configs.into_iter().enumerate() { - let mut run_loop = neon::RunLoop::new(confs[i].clone()); - let blocks_processed_arc = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - let this_miner_status = run_loop.get_miner_status(); - - blocks_processed.push(blocks_processed_arc); - channels.push(channel); - miner_status.push(this_miner_status); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - } - - let http_origin = format!("http://{}", &confs[0].node.rpc_bind); - - // give the run loops some time to start up! - for bp in &blocks_processed { - wait_for_runloop(bp); - } - - // activate miners - eprintln!("\n\nBoot miner 0\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(&confs[0]); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner 0: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner 0...\n\n"); - } - next_block_and_iterate( - &mut btc_regtest_controller, - &blocks_processed[0], - block_time_ms, - ); - } - - for (i, conf) in confs.iter().enumerate().skip(1) { - eprintln!("\n\nBoot miner {i}\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(conf); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner {i}: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner {i}...\n\n"); - } - next_block_and_iterate(&mut btc_regtest_controller, &blocks_processed[i], 5_000); - } - } - - eprintln!("\n\nBegin transactions\n\n"); - - let pox_pubkey = Secp256k1PublicKey::from_hex( - "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", - ) - .unwrap(); - let pox_pubkey_hash = bytes_to_hex(&Hash160::from_node_public_key(&pox_pubkey).to_bytes()); - - let sort_height = channels[0].get_sortitions_processed(); - - // make everyone stack - let stacking_txs: Vec<_> = stack_privks - .iter() - .map(|pk| { - make_contract_call( - pk, - 0, - 1360, - conf_template.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "pox-2", - "stack-stx", - &[ - Value::UInt(2_000_000_000_000_000 - 30_000_000), - execute( - &format!("{{ hashbytes: 0x{pox_pubkey_hash}, version: 0x00 }}"), - ClarityVersion::Clarity1, - ) - .unwrap() - .unwrap(), - Value::UInt((sort_height + 1) as u128), - Value::UInt(12), - ], - ) - }) - .collect(); - - // keeps the mempool full, and makes it so miners will spend a nontrivial amount of time - // building blocks - let all_txs: Vec<_> = privks - .iter() - .enumerate() - .map(|(i, pk)| { - make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false) - }) - .collect(); - - // everyone locks up - for (cnt, tx) in stacking_txs.iter().enumerate() { - eprintln!("\n\nSubmit stacking tx {cnt}\n\n"); - submit_tx(&http_origin, tx); - } - - // run a reward cycle - let mut at_220 = false; - while !at_220 { - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - if tip_info.burn_block_height == 220 { - at_220 = true; - } - } - } - - // blast out the rest - let mut cnt = 0; - for tx_chain in all_txs { - for tx in tx_chain { - eprintln!("\n\nSubmit tx {cnt}\n\n"); - submit_tx(&http_origin, &tx); - cnt += 1; - } - } - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - assert!(tip_info.burn_block_height <= 220); - } - - eprintln!("\n\nBegin mining\n\n"); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // prevent Stacks at these heights from propagating. - // This means that both nodes affirm the absence of each others' anchor blocks. - env::set_var( - "STACKS_HIDE_BLOCKS_AT_HEIGHT", - "[220,221,222,223,224,225,226,227,228,229,230,231,232,232,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301]" - ); - - let mut max_stacks_tip = 0; - - // miners 0 and 1 take turns mining anchor blocks. - // this should cause them both to flip/flop their sortition histories multiple times - for _c in 0..2 { - // miner 0 mines two reward cycles and confirms a hidden anchor block. - // miner 1 is disabled for this reward cycle - signal_mining_blocked(miner_status[1].clone()); - for i in 0..20 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 1 mines two reward cycles and confirms a hidden anchor block. - // miner 0 is disabled for this reward cycle - signal_mining_blocked(miner_status[0].clone()); - for i in 0..20 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - } - signal_mining_ready(miner_status[0].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - // miner 1's history overtakes miner 0's. - // Miner 1 didn't see cycle 22's anchor block, but it just mined an anchor block for cycle - // 23 and affirmed cycle 22's anchor block's absence. - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - info!("####################### end of cycle ##############################"); - } - - // advance to start of next reward cycle - eprintln!("\n\nBuild final block\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - // resume block propagation - env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); - - // wait for all blocks to propagate - eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); - wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Final tip for miner {i}: {tip_info:?}"); - } -} - -/// Make sure the node can boot despite missing a ton of anchor blocks. -/// Miner 0 mines anchor blocks for cycles 22, 23, 24, 25, 26 and hides them. -/// Miner 1 doesn't see them until the start of cycle 27 -/// The test verifies that miner 1 is still able to sync with miner 0, despite having mined in the -/// absence of these anchor blocks while miner 1 was hiding blocks. -#[test] -#[ignore] -fn test_pox_missing_five_anchor_blocks() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_miners = 2; - - let reward_cycle_len = 10; - let prepare_phase_len = 3; - let v1_unlock_height = 152; - - let (mut conf_template, _) = neon_integration_test_conf(); - let block_time_ms = 10_000; - conf_template.node.mine_microblocks = true; - conf_template.miner.microblock_attempt_time_ms = 2_000; - conf_template.node.wait_time_for_microblocks = 0; - conf_template.node.microblock_frequency = 0; - conf_template.miner.first_attempt_time_ms = 2_000; - conf_template.miner.subsequent_attempt_time_ms = 5_000; - conf_template.burnchain.max_rbf = 1000000; - conf_template.node.wait_time_for_blocks = 1_000; - conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - - // make epoch 2.1 start in the middle of boot-up - let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); - epochs[StacksEpochId::Epoch20].end_height = 101; - epochs[StacksEpochId::Epoch2_05].start_height = 101; - epochs[StacksEpochId::Epoch2_05].end_height = 151; - epochs[StacksEpochId::Epoch21].start_height = 151; - conf_template.burnchain.epochs = Some(epochs); - - let privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let stack_privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let balances: Vec<_> = privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 30_000_000, - } - }) - .collect(); - - let stack_balances: Vec<_> = stack_privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 2_000_000_000_000_000, - } - }) - .collect(); - - let mut confs = vec![]; - let mut burnchain_configs = vec![]; - let mut blocks_processed = vec![]; - let mut channels = vec![]; - let mut miner_status = vec![]; - - for i in 0..num_miners { - let seed = StacksPrivateKey::random().to_bytes(); - let (mut conf, _) = neon_integration_test_conf_with_seed(seed); - - conf.initial_balances.clear(); - conf.initial_balances.append(&mut balances.clone()); - conf.initial_balances.append(&mut stack_balances.clone()); - - conf.node.mine_microblocks = conf_template.node.mine_microblocks; - conf.miner.microblock_attempt_time_ms = conf_template.miner.microblock_attempt_time_ms; - conf.node.wait_time_for_microblocks = conf_template.node.wait_time_for_microblocks; - conf.node.microblock_frequency = conf_template.node.microblock_frequency; - conf.miner.first_attempt_time_ms = conf_template.miner.first_attempt_time_ms; - conf.miner.subsequent_attempt_time_ms = conf_template.miner.subsequent_attempt_time_ms; - conf.node.wait_time_for_blocks = conf_template.node.wait_time_for_blocks; - conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; - conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); - conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - - // multiple nodes so they must download from each other - conf.miner.wait_for_block_download = true; - - // nodes will selectively hide blocks from one another - conf.node.fault_injection_hide_blocks = true; - - let rpc_port = 41103 + 10 * i; - let p2p_port = 41103 + 10 * i + 1; - conf.node.rpc_bind = format!("127.0.0.1:{rpc_port}"); - conf.node.data_url = format!("http://127.0.0.1:{rpc_port}"); - conf.node.p2p_bind = format!("127.0.0.1:{p2p_port}"); - - confs.push(conf); - } - - let node_privkey_1 = Secp256k1PrivateKey::from_seed(&confs[0].node.local_peer_seed); - let chain_id = confs[0].burnchain.chain_id; - let peer_version = confs[0].burnchain.peer_version; - let p2p_bind = confs[0].node.p2p_bind.clone(); - for conf in confs.iter_mut().skip(1) { - conf.node.set_bootstrap_nodes( - format!( - "{}@{p2p_bind}", - &StacksPublicKey::from_private(&node_privkey_1).to_hex() - ), - chain_id, - peer_version, - ); - } - - // use short reward cycles - for conf in &confs { - let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new( - reward_cycle_len, - prepare_phase_len, - 4 * prepare_phase_len / 5, - 5, - 15, - (1600 * reward_cycle_len - 1).into(), - (1700 * reward_cycle_len).into(), - v1_unlock_height, - u32::MAX, - u32::MAX, - u32::MAX, - ); - burnchain_config.pox_constants = pox_constants.clone(); - - burnchain_configs.push(burnchain_config); - } - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&confs[0]); - btcd_controller - .start_bitcoind() - .map_err(|_e| ()) - .expect("Failed starting bitcoind"); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - confs[0].clone(), - None, - Some(burnchain_configs[0].clone()), - None, - ); - - btc_regtest_controller.bootstrap_chain(1); - - // make sure all miners have BTC - for conf in confs.iter().skip(1) { - let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); - btc_regtest_controller - .set_mining_pubkey(conf.burnchain.local_mining_public_key.clone().unwrap()); - btc_regtest_controller.bootstrap_chain(1); - btc_regtest_controller.set_mining_pubkey(old_mining_pubkey); - } - - btc_regtest_controller.bootstrap_chain((199 - num_miners) as u64); - - eprintln!("Chain bootstrapped..."); - - for (i, burnchain_config) in burnchain_configs.into_iter().enumerate() { - let mut run_loop = neon::RunLoop::new(confs[i].clone()); - let blocks_processed_arc = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - let this_miner_status = run_loop.get_miner_status(); - - blocks_processed.push(blocks_processed_arc); - channels.push(channel); - miner_status.push(this_miner_status); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - } - - let http_origin = format!("http://{}", &confs[0].node.rpc_bind); - - // give the run loops some time to start up! - for bp in &blocks_processed { - wait_for_runloop(bp); - } - - // activate miners - eprintln!("\n\nBoot miner 0\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(&confs[0]); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner 0: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner 0...\n\n"); - } - next_block_and_iterate( - &mut btc_regtest_controller, - &blocks_processed[0], - block_time_ms, - ); - } - - for (i, conf) in confs.iter().enumerate().skip(1) { - eprintln!("\n\nBoot miner {i}\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(conf); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner {i}: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner {i}...\n\n"); - } - next_block_and_iterate(&mut btc_regtest_controller, &blocks_processed[i], 5_000); - } - } - - eprintln!("\n\nBegin transactions\n\n"); - - let pox_pubkey = Secp256k1PublicKey::from_hex( - "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", - ) - .unwrap(); - let pox_pubkey_hash = bytes_to_hex(&Hash160::from_node_public_key(&pox_pubkey).to_bytes()); - - let sort_height = channels[0].get_sortitions_processed(); - - // make everyone stack - let stacking_txs: Vec<_> = stack_privks - .iter() - .map(|pk| { - make_contract_call( - pk, - 0, - 1360, - conf_template.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "pox-2", - "stack-stx", - &[ - Value::UInt(2_000_000_000_000_000 - 30_000_000), - execute( - &format!("{{ hashbytes: 0x{pox_pubkey_hash}, version: 0x00 }}"), - ClarityVersion::Clarity1, - ) - .unwrap() - .unwrap(), - Value::UInt((sort_height + 1) as u128), - Value::UInt(12), - ], - ) - }) - .collect(); - - // keeps the mempool full, and makes it so miners will spend a nontrivial amount of time - // building blocks - let all_txs: Vec<_> = privks - .iter() - .enumerate() - .map(|(i, pk)| { - make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false) - }) - .collect(); - - // everyone locks up - for (cnt, tx) in stacking_txs.iter().enumerate() { - eprintln!("\n\nSubmit stacking tx {cnt}\n\n"); - submit_tx(&http_origin, tx); - } - - // run a reward cycle - let mut at_220 = false; - while !at_220 { - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - if tip_info.burn_block_height == 220 { - at_220 = true; - } - } - } - - // blast out the rest - let mut cnt = 0; - for tx_chain in all_txs { - for tx in tx_chain { - eprintln!("\n\nSubmit tx {cnt}\n\n"); - submit_tx(&http_origin, &tx); - cnt += 1; - } - } - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - assert!(tip_info.burn_block_height <= 220); - } - - eprintln!("\n\nBegin mining\n\n"); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // prevent Stacks at these heights from propagating - env::set_var( - "STACKS_HIDE_BLOCKS_AT_HEIGHT", - "[226,227,228,229,230,236,237,238,239,240,246,247,248,249,250,256,257,258,259,260,266,267,268,269,270,276,277,278,279,280,286,287,288,289,290]" - ); - - let mut max_stacks_tip = 0; - for c in 0..5 { - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i} cycle {c}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - info!("####################### end of cycle ##############################"); - } - - // advance to start of next reward cycle - eprintln!("\n\nBuild final block\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - // resume block propagation - env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); - - // wait for all blocks to propagate. - // miner 1 should learn about all of miner 0's blocks - info!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}",); - wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Final tip for miner {i}: {tip_info:?}"); - } -} - #[test] #[ignore] /// Verify that if the sortition AM declares that an anchor block is present in epoch 2.05, but the diff --git a/stacks-node/src/tests/epoch_22.rs b/stacks-node/src/tests/epoch_22.rs index d0d4c0070f..2d55031200 100644 --- a/stacks-node/src/tests/epoch_22.rs +++ b/stacks-node/src/tests/epoch_22.rs @@ -1223,411 +1223,3 @@ fn pox_2_unlock_all() { test_observer::clear(); channel.stop_chains_coordinator(); } - -/// PoX reorg with just one flap. Epoch 2.2 activates during bootup -/// Miner 0 mines and hides the anchor block for cycle 22. -/// Miner 1 mines and hides the anchor block for cycle 23, causing a PoX reorg in miner 0. -/// At the very end, miners stop hiding their blocks, and the test verifies that both miners -/// converge on having anchor blocks for cycles 22 and 24, but not 23. -#[test] -#[ignore] -fn test_pox_reorg_one_flap() { - if env::var("BITCOIND_TEST") != Ok("1".into()) { - return; - } - - let num_miners = 2; - - let reward_cycle_len = 10; - let prepare_phase_len = 3; - let v1_unlock_height = 152; - let epoch_2_2 = 175; - let v2_unlock_height = epoch_2_2 + 1; - - let (mut conf_template, _) = neon_integration_test_conf(); - let block_time_ms = 10_000; - conf_template.node.mine_microblocks = true; - conf_template.miner.microblock_attempt_time_ms = 2_000; - conf_template.node.wait_time_for_microblocks = 0; - conf_template.node.microblock_frequency = 0; - conf_template.miner.first_attempt_time_ms = 2_000; - conf_template.miner.subsequent_attempt_time_ms = 5_000; - conf_template.burnchain.max_rbf = 1000000; - conf_template.node.wait_time_for_blocks = 1_000; - conf_template.burnchain.pox_2_activation = Some(v1_unlock_height); - - // make epoch 2.1 and 2.2 start in the middle of boot-up - let mut epochs = EpochList::new(&*core::STACKS_EPOCHS_REGTEST); - epochs[StacksEpochId::Epoch20].end_height = 101; - epochs[StacksEpochId::Epoch2_05].start_height = 101; - epochs[StacksEpochId::Epoch2_05].end_height = 151; - epochs[StacksEpochId::Epoch21].start_height = 151; - epochs[StacksEpochId::Epoch21].end_height = epoch_2_2; - epochs[StacksEpochId::Epoch22].start_height = epoch_2_2; - epochs[StacksEpochId::Epoch22].end_height = STACKS_EPOCH_MAX; - epochs.truncate_after(StacksEpochId::Epoch22); - conf_template.burnchain.epochs = Some(epochs); - - let privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let stack_privks: Vec<_> = (0..5).map(|_| StacksPrivateKey::random()).collect(); - - let balances: Vec<_> = privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 30_000_000, - } - }) - .collect(); - - let stack_balances: Vec<_> = stack_privks - .iter() - .map(|privk| { - let addr = to_addr(privk); - InitialBalance { - address: addr.into(), - amount: 2_000_000_000_000_000, - } - }) - .collect(); - - let mut confs = vec![]; - let mut burnchain_configs = vec![]; - let mut blocks_processed = vec![]; - let mut channels = vec![]; - let mut miner_status = vec![]; - - for i in 0..num_miners { - let seed = StacksPrivateKey::random().to_bytes(); - let (mut conf, _) = neon_integration_test_conf_with_seed(seed); - - conf.initial_balances.clear(); - conf.initial_balances.append(&mut balances.clone()); - conf.initial_balances.append(&mut stack_balances.clone()); - - conf.node.mine_microblocks = conf_template.node.mine_microblocks; - conf.miner.microblock_attempt_time_ms = conf_template.miner.microblock_attempt_time_ms; - conf.node.wait_time_for_microblocks = conf_template.node.wait_time_for_microblocks; - conf.node.microblock_frequency = conf_template.node.microblock_frequency; - conf.miner.first_attempt_time_ms = conf_template.miner.first_attempt_time_ms; - conf.miner.subsequent_attempt_time_ms = conf_template.miner.subsequent_attempt_time_ms; - conf.node.wait_time_for_blocks = conf_template.node.wait_time_for_blocks; - conf.burnchain.max_rbf = conf_template.burnchain.max_rbf; - conf.burnchain.epochs = conf_template.burnchain.epochs.clone(); - conf.burnchain.pox_2_activation = conf_template.burnchain.pox_2_activation; - - // multiple nodes so they must download from each other - conf.miner.wait_for_block_download = true; - - // nodes will selectively hide blocks from one another - conf.node.fault_injection_hide_blocks = true; - - let rpc_port = 41063 + 10 * i; - let p2p_port = 41063 + 10 * i + 1; - conf.node.rpc_bind = format!("127.0.0.1:{rpc_port}"); - conf.node.data_url = format!("http://127.0.0.1:{rpc_port}"); - conf.node.p2p_bind = format!("127.0.0.1:{p2p_port}"); - - confs.push(conf); - } - - let node_privkey_1 = - StacksNode::make_node_private_key_from_seed(&confs[0].node.local_peer_seed); - let chain_id = confs[0].burnchain.chain_id; - let peer_version = confs[0].burnchain.peer_version; - let p2p_bind = confs[0].node.p2p_bind.clone(); - for conf in confs.iter_mut().skip(1) { - conf.node.set_bootstrap_nodes( - format!( - "{}@{p2p_bind}", - &StacksPublicKey::from_private(&node_privkey_1).to_hex() - ), - chain_id, - peer_version, - ); - } - - // use short reward cycles - for conf in &confs { - let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); - let pox_constants = PoxConstants::new( - reward_cycle_len, - prepare_phase_len, - 4 * prepare_phase_len / 5, - 5, - 15, - (1600 * reward_cycle_len - 1).into(), - (1700 * reward_cycle_len).into(), - v1_unlock_height, - v2_unlock_height.try_into().unwrap(), - u32::MAX, - u32::MAX, - ); - burnchain_config.pox_constants = pox_constants.clone(); - - burnchain_configs.push(burnchain_config); - } - - let mut btcd_controller = BitcoinCoreController::from_stx_config(&confs[0]); - btcd_controller - .start_bitcoind() - .map_err(|_e| ()) - .expect("Failed starting bitcoind"); - - let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( - confs[0].clone(), - None, - Some(burnchain_configs[0].clone()), - None, - ); - - btc_regtest_controller.bootstrap_chain(1); - - // make sure all miners have BTC - for conf in confs.iter().skip(1) { - let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); - btc_regtest_controller - .set_mining_pubkey(conf.burnchain.local_mining_public_key.clone().unwrap()); - btc_regtest_controller.bootstrap_chain(1); - btc_regtest_controller.set_mining_pubkey(old_mining_pubkey); - } - - btc_regtest_controller.bootstrap_chain((199 - num_miners) as u64); - - eprintln!("Chain bootstrapped..."); - - for (i, burnchain_config) in burnchain_configs.into_iter().enumerate() { - let mut run_loop = neon::RunLoop::new(confs[i].clone()); - let blocks_processed_arc = run_loop.get_blocks_processed_arc(); - let channel = run_loop.get_coordinator_channel().unwrap(); - let this_miner_status = run_loop.get_miner_status(); - - blocks_processed.push(blocks_processed_arc); - channels.push(channel); - miner_status.push(this_miner_status); - - thread::spawn(move || run_loop.start(Some(burnchain_config), 0)); - } - - let http_origin = format!("http://{}", &confs[0].node.rpc_bind); - - // give the run loops some time to start up! - for bp in &blocks_processed { - wait_for_runloop(bp); - } - - // activate miners - eprintln!("\n\nBoot miner 0\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(&confs[0]); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner 0: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner 0...\n\n"); - } - next_block_and_iterate( - &mut btc_regtest_controller, - &blocks_processed[0], - block_time_ms, - ); - } - - for (i, conf) in confs.iter().enumerate().skip(1) { - eprintln!("\n\nBoot miner {i}\n\n"); - loop { - let tip_info_opt = get_chain_info_opt(conf); - if let Some(tip_info) = tip_info_opt { - eprintln!("\n\nMiner {i}: {tip_info:?}\n\n"); - if tip_info.stacks_tip_height > 0 { - break; - } - } else { - eprintln!("\n\nWaiting for miner {i}...\n\n"); - } - next_block_and_iterate(&mut btc_regtest_controller, &blocks_processed[i], 5_000); - } - } - - eprintln!("\n\nBegin transactions\n\n"); - - let pox_pubkey = Secp256k1PublicKey::from_hex( - "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", - ) - .unwrap(); - let pox_pubkey_hash = bytes_to_hex(&Hash160::from_node_public_key(&pox_pubkey).to_bytes()); - - let sort_height = channels[0].get_sortitions_processed(); - - // make everyone stack - let stacking_txs: Vec<_> = stack_privks - .iter() - .map(|pk| { - make_contract_call( - pk, - 0, - 1360, - conf_template.burnchain.chain_id, - &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), - "pox-2", - "stack-stx", - &[ - Value::UInt(2_000_000_000_000_000 - 30_000_000), - execute( - &format!("{{ hashbytes: 0x{pox_pubkey_hash}, version: 0x00 }}"), - ClarityVersion::Clarity1, - ) - .unwrap() - .unwrap(), - Value::UInt((sort_height + 1) as u128), - Value::UInt(12), - ], - ) - }) - .collect(); - - // keeps the mempool full, and makes it so miners will spend a nontrivial amount of time - // building blocks - let all_txs: Vec<_> = privks - .iter() - .enumerate() - .map(|(i, pk)| { - make_random_tx_chain(pk, (25 * i) as u64, conf_template.burnchain.chain_id, false) - }) - .collect(); - - // everyone locks up - for (cnt, tx) in stacking_txs.iter().enumerate() { - eprintln!("\n\nSubmit stacking tx {cnt}\n\n"); - submit_tx(&http_origin, tx); - } - - // run a reward cycle - let mut at_220 = false; - while !at_220 { - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - if tip_info.burn_block_height == 220 { - at_220 = true; - } - } - } - - // blast out the rest - let mut cnt = 0; - for tx_chain in all_txs { - for tx in tx_chain { - eprintln!("\n\nSubmit tx {cnt}\n\n"); - submit_tx(&http_origin, &tx); - cnt += 1; - } - } - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - assert!(tip_info.burn_block_height <= 220); - } - - eprintln!("\n\nBegin mining\n\n"); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // prevent Stacks at these heights from propagating - env::set_var( - "STACKS_HIDE_BLOCKS_AT_HEIGHT", - "[226,227,228,229,230,236,237,238,239,240,246,247,248,249,250,256,257,258,259,260,266,267,268,269,270,276,277,278,279,280,286,287,288,289,290]" - ); - - // miner 0 mines a prepare phase and confirms a hidden anchor block. - // miner 1 is disabled for these prepare phases - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[1].clone()); - } - } - signal_mining_ready(miner_status[1].clone()); - - info!("####################### end of cycle ##############################"); - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - info!("####################### end of cycle ##############################"); - - // miner 1 mines a prepare phase and confirms a hidden anchor block. - // miner 0 is disabled for this prepare phase - for i in 0..10 { - eprintln!("\n\nBuild block {i}\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - if i >= reward_cycle_len - prepare_phase_len - 2 { - signal_mining_blocked(miner_status[0].clone()); - } - } - signal_mining_ready(miner_status[0].clone()); - - info!("####################### end of cycle ##############################"); - let mut max_stacks_tip = 0; - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - - // miner 1's history overtakes miner 0's. - // Miner 1 didn't see cycle 22's anchor block, but it just mined an anchor block for cycle - // 23 and affirmed cycle 22's anchor block's absence. - max_stacks_tip = std::cmp::max(tip_info.stacks_tip_height, max_stacks_tip); - } - info!("####################### end of cycle ##############################"); - - // advance to start of next reward cycle - eprintln!("\n\nBuild final block\n\n"); - btc_regtest_controller.build_next_block(1); - sleep_ms(block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Tip for miner {i}: {tip_info:?}"); - } - - // resume block propagation - env::set_var("STACKS_HIDE_BLOCKS_AT_HEIGHT", "[]"); - - // wait for all blocks to propagate - eprintln!("Wait for all blocks to propagate; stacks tip height is {max_stacks_tip}"); - wait_pox_stragglers(&confs, max_stacks_tip, block_time_ms); - - for (i, c) in confs.iter().enumerate() { - let tip_info = get_chain_info(c); - info!("Final tip for miner {i}: {tip_info:?}"); - } -} From 7cfff9593d2b913c4a4429868fd2623b6ab62146 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 4 Sep 2025 13:55:04 -0700 Subject: [PATCH 34/35] CRC: clean up CHAINSTATE_SCHEMA_5 to simply drop affirmation_weight column and add tests Signed-off-by: Jacinta Ferrant --- stackslib/src/burnchains/db.rs | 9 +- stackslib/src/burnchains/tests/db.rs | 130 +++++++++++ stackslib/src/chainstate/stacks/db/mod.rs | 249 +++++++++++++++------- 3 files changed, 303 insertions(+), 85 deletions(-) diff --git a/stackslib/src/burnchains/db.rs b/stackslib/src/burnchains/db.rs index 579e2deb39..a92d68af5a 100644 --- a/stackslib/src/burnchains/db.rs +++ b/stackslib/src/burnchains/db.rs @@ -329,12 +329,12 @@ const BURNCHAIN_DB_MIGRATION_V2_TO_V3: &str = r#" DROP TABLE affirmation_maps; "#; -static SCHEMA_2: &[&str] = &[ +pub static SCHEMA_2: &[&str] = &[ BURNCHAIN_DB_SCHEMA_2, "INSERT INTO db_config (version) VALUES (2);", ]; -static SCHEMA_3: &[&str] = &[ +pub static SCHEMA_3: &[&str] = &[ BURNCHAIN_DB_MIGRATION_V2_TO_V3, "INSERT INTO db_config (version) VALUES (3);", ]; @@ -506,10 +506,7 @@ impl BurnchainDB { // Query all version values and get the max let mut stmt = conn.prepare("SELECT version FROM db_config")?; let max_version: u32 = stmt - .query_map([], |row| { - // Always read as String - row.get::<_, String>(0) - })? + .query_map([], |row| row.get::<_, String>(0))? .filter_map(Result::ok) .filter_map(|v| v.parse::().ok()) .max() diff --git a/stackslib/src/burnchains/tests/db.rs b/stackslib/src/burnchains/tests/db.rs index 2686a08967..109d903c9c 100644 --- a/stackslib/src/burnchains/tests/db.rs +++ b/stackslib/src/burnchains/tests/db.rs @@ -15,7 +15,9 @@ // along with this program. If not, see . use std::cmp; +use std::path::PathBuf; +use rusqlite::{params, Connection}; use stacks_common::address::AddressHashMode; use stacks_common::deps_common::bitcoin::blockdata::transaction::Transaction as BtcTx; use stacks_common::deps_common::bitcoin::network::serialize::deserialize; @@ -1052,3 +1054,131 @@ fn test_classify_delegate_stx() { panic!("EXPECTED to parse a delegate stx op"); } } + +// Mock Burnchain for testing +fn mock_burnchain() -> Burnchain { + let first_block_height = 100; + Burnchain { + pox_constants: PoxConstants::test_default(), + peer_version: 0x012345678, + network_id: 0x9abcdef0, + chain_name: "bitcoin".to_string(), + network_name: "testnet".to_string(), + working_dir: "/nope".to_string(), + consensus_hash_lifetime: 24, + stable_confirmations: 7, + first_block_height, + initial_reward_start_block: first_block_height, + first_block_timestamp: 0, + first_block_hash: BurnchainHeaderHash::zero(), + } +} + +/// Create a temporary db path for testing purposes +pub fn tmp_db_path() -> PathBuf { + std::env::temp_dir().join(format!( + "burnchain-db-test-{}.sqlite", + rand::random::() + )) +} + +#[test] +fn burnchain_db_migration_v2_to_v3() -> Result<(), BurnchainError> { + // Create an in-memory database + let tmp_path = tmp_db_path(); + let conn = Connection::open(tmp_path.clone())?; + + // Initialize database with schema version 2 using SCHEMA_2 + for statement in SCHEMA_2.iter() { + conn.execute_batch(statement)?; + } + + // Insert sample data to verify data integrity post-migration + let sample_block_hash = BurnchainHeaderHash([1u8; 32]); + let sample_parent_block_hash = BurnchainHeaderHash([0u8; 32]); + let sample_txid = "txid1".to_string(); + conn.execute( + "INSERT INTO burnchain_db_block_headers (block_height, block_hash, parent_block_hash, num_txs, timestamp) VALUES (?, ?, ?, ?, ?)", + params![1, &sample_block_hash, &sample_parent_block_hash, 1, 1234567890], + )?; + conn.execute( + "INSERT INTO affirmation_maps (weight, affirmation_map) VALUES (?, ?)", + params![1, "test_map"], + )?; + conn.execute( + "INSERT INTO block_commit_metadata (burn_block_hash, txid, block_height, vtxindex, affirmation_id, anchor_block, anchor_block_descendant) VALUES (?, ?, ?, ?, ?, ?, ?)", + params![&sample_block_hash, &sample_txid, 1, 0, 0, None::, None::], + )?; + + // Create BurnchainDB using connect to trigger migration code + let burnchain = mock_burnchain(); + let db = BurnchainDB::connect(tmp_path.to_str().unwrap(), &burnchain, true)?; + + // Verify schema version is updated to 3 + let mut stmt = conn.prepare("SELECT version FROM db_config")?; + let version: u32 = stmt + .query_map([], |row| row.get::<_, String>(0))? + .filter_map(Result::ok) + .filter_map(|v| v.parse::().ok()) + .max() + .expect("Expected db_config to have a version"); + assert_eq!(version, 3, "Database version should be 3 after migration"); + + // Verify affirmation_maps table is dropped + assert!( + !table_exists(&db.conn, "affirmation_maps")?, + "affirmation_maps table should be dropped" + ); + + // Verify affirmation_id column is dropped from block_commit_metadata + let columns: Vec = db + .conn + .prepare("PRAGMA table_info(block_commit_metadata)")? + .query_map([], |row| row.get(1))? + .collect::, _>>()?; + assert!( + !columns.contains(&"affirmation_id".to_string()), + "affirmation_id column should be dropped" + ); + + // Verify other tables and data remain intact + assert!( + table_exists(&db.conn, "burnchain_db_block_headers")?, + "burnchain_db_block_headers table should exist" + ); + assert!( + table_exists(&db.conn, "block_commit_metadata")?, + "block_commit_metadata table should exist" + ); + let header: Option = query_row( + &db.conn, + "SELECT * FROM burnchain_db_block_headers WHERE block_hash = ?", + params![&sample_block_hash], + )?; + assert!( + header.is_some(), + "Sample block header should remain after migration" + ); + let metadata: Option = query_row( + &db.conn, + "SELECT txid FROM block_commit_metadata WHERE burn_block_hash = ?", + params![&sample_block_hash], + )?; + assert_eq!( + metadata, + Some(sample_txid), + "Sample block_commit_metadata should remain after migration" + ); + + // Verify indexes are still present + let indexes: Vec = db.conn + .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'index_block_commit_metadata%'")? + .query_map([], |row| row.get(0))? + .collect::, _>>()?; + assert!( + indexes.contains(&"index_block_commit_metadata_burn_block_hash_anchor_block".to_string()), + "Expected index should still exist" + ); + + Ok(()) +} diff --git a/stackslib/src/chainstate/stacks/db/mod.rs b/stackslib/src/chainstate/stacks/db/mod.rs index 44aa0d7dfe..b54fef7eab 100644 --- a/stackslib/src/chainstate/stacks/db/mod.rs +++ b/stackslib/src/chainstate/stacks/db/mod.rs @@ -856,85 +856,9 @@ pub static CHAINSTATE_SCHEMA_5: &[&str] = &[ // Schema change: drop the affirmation_weight column from pre_nakamoto block_headers and any indexes that reference it // but leave everything else the same r#" - -- Rename old block_headers table - ALTER TABLE block_headers RENAME TO block_headers_old; - - -- Create new schema without affirmation_weight - CREATE TABLE block_headers( - version INTEGER NOT NULL, - total_burn TEXT NOT NULL, -- converted to/from u64 - total_work TEXT NOT NULL, -- converted to/from u64 - proof TEXT NOT NULL, - parent_block TEXT NOT NULL, -- hash of parent Stacks block - parent_microblock TEXT NOT NULL, - parent_microblock_sequence INTEGER NOT NULL, - tx_merkle_root TEXT NOT NULL, - state_index_root TEXT NOT NULL, - microblock_pubkey_hash TEXT NOT NULL, - block_hash TEXT NOT NULL, -- NOTE: not unique, as two burn chain forks can commit to the same Stacks block - index_block_hash TEXT UNIQUE NOT NULL, -- globally unique index hash - block_height INTEGER NOT NULL, - index_root TEXT NOT NULL, -- root hash of internal MARF for chainstate/fork metadata - consensus_hash TEXT UNIQUE NOT NULL, -- guaranteed to be unique - burn_header_hash TEXT NOT NULL, -- burn header hash corresponding to consensus hash - burn_header_height INT NOT NULL, -- height of the burnchain block header - burn_header_timestamp INT NOT NULL,-- timestamp from burnchain block header - parent_block_id TEXT NOT NULL, -- parent index_block_hash - cost TEXT NOT NULL, - block_size TEXT NOT NULL, -- converted to/from u64 - PRIMARY KEY(consensus_hash, block_hash) - ); - - -- Copy data from old table, ignoring affirmation_weight - INSERT INTO block_headers( - version, - total_burn, - total_work, - proof, - parent_block, - parent_microblock, - parent_microblock_sequence, - tx_merkle_root, - state_index_root, - microblock_pubkey_hash, - block_hash, - index_block_hash, - block_height, - index_root, - consensus_hash, - burn_header_hash, - burn_header_height, - burn_header_timestamp, - parent_block_id, - cost, - block_size - ) - SELECT - version, - total_burn, - total_work, - proof, - parent_block, - parent_microblock, - parent_microblock_sequence, - tx_merkle_root, - state_index_root, - microblock_pubkey_hash, - block_hash, - index_block_hash, - block_height, - index_root, - consensus_hash, - burn_header_hash, - burn_header_height, - burn_header_timestamp, - parent_block_id, - cost, - block_size - FROM block_headers_old; - - -- Drop old block_headers table - DROP TABLE block_headers_old; + DROP INDEX IF EXISTS index_block_header_by_affirmation_weight; + DROP INDEX IF EXISTS index_block_header_by_height_and_affirmation_weight; + ALTER TABLE block_headers DROP COLUMN affirmation_weight; "#, r#"UPDATE db_config SET version = "11";"#, ]; @@ -3083,4 +3007,171 @@ pub mod test { Some("3.45.0".to_string()) ); } + + pub fn tmp_db_path() -> PathBuf { + std::env::temp_dir().join(format!("chainstate-test-{}.sqlite", rand::random::())) + } + + #[test] + fn chainstate_migration_v10_to_v11() -> Result<(), Error> { + let test_name = "test_chainstate_migration_v10_to_v11"; + // Create an in-memory database + let tmp_path = tmp_db_path(); + let conn = Connection::open(tmp_path.clone())?; + + // Simulate schema version 10 by applying all schemas up to NAKAMOTO_CHAINSTATE_SCHEMA_6 + for schema in CHAINSTATE_INITIAL_SCHEMA.iter() { + conn.execute_batch(schema)?; + } + // Manually insert a version since chainstate initial schema just creates but doesn't insert anything + // required for subsequent "updates" to be successful + conn.execute( + "INSERT INTO db_config (version, mainnet, chain_id) VALUES (?, ?, ?)", + params!["1", 1, 1], // initial version 1 + )?; + for schema in CHAINSTATE_SCHEMA_2.iter() { + conn.execute_batch(schema)?; + } + for schema in CHAINSTATE_SCHEMA_3.iter() { + conn.execute_batch(schema)?; + } + for schema in NAKAMOTO_CHAINSTATE_SCHEMA_1.iter() { + conn.execute_batch(schema)?; + } + for schema in NAKAMOTO_CHAINSTATE_SCHEMA_2.iter() { + conn.execute_batch(schema)?; + } + for schema in NAKAMOTO_CHAINSTATE_SCHEMA_3.iter() { + conn.execute_batch(schema)?; + } + for schema in NAKAMOTO_CHAINSTATE_SCHEMA_4.iter() { + conn.execute_batch(schema)?; + } + for schema in NAKAMOTO_CHAINSTATE_SCHEMA_5.iter() { + conn.execute_batch(schema)?; + } + for schema in CHAINSTATE_SCHEMA_4.iter() { + conn.execute_batch(schema)?; + } + for schema in NAKAMOTO_CHAINSTATE_SCHEMA_6.iter() { + conn.execute_batch(schema)?; + } + + // Insert dummy data into pre-nakamoto block_headers + let sample_block_hash = BlockHeaderHash([1u8; 32]); + let sample_consensus_hash = ConsensusHash([2u8; 20]); + let sample_burn_header_hash = BurnchainHeaderHash([3u8; 32]); + let sample_parent_block_id = StacksBlockId([0u8; 32]); + let sample_index_block_hash = + StacksBlockId::new(&sample_consensus_hash, &sample_block_hash); + conn.execute( + "INSERT INTO block_headers ( + version, total_burn, total_work, proof, parent_block, parent_microblock, + parent_microblock_sequence, tx_merkle_root, state_index_root, microblock_pubkey_hash, + block_hash, index_block_hash, block_height, index_root, consensus_hash, + burn_header_hash, burn_header_height, burn_header_timestamp, parent_block_id, + cost, block_size, affirmation_weight + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + params![ + 1, + "1000", + "1", + to_hex(&[0u8; 48]), + to_hex(&[0u8; 32]), + to_hex(&[0u8; 32]), + 0, + to_hex(&[0u8; 32]), + to_hex(&[0u8; 32]), + to_hex(&[0u8; 20]), + &sample_block_hash, + &sample_index_block_hash, + 1, + to_hex(&[0u8; 32]), + &sample_consensus_hash, + &sample_burn_header_hash, + 100, + 1234567890, + &sample_parent_block_id, + serde_json::to_string(&ExecutionCost::ZERO).unwrap(), + "1000", + 10 + ], + )?; + + // Verify schema version is 10 before migration + let version: String = query_row(&conn, "SELECT version FROM db_config", NO_PARAMS)? + .expect("Expected db_config to have a version"); + assert_eq!( + version, "10", + "Database version should be 10 before migration" + ); + + // Apply the simplified CHAINSTATE_SCHEMA_5 migration + for statement in CHAINSTATE_SCHEMA_5.iter() { + conn.execute_batch(statement)?; + } + // Verify schema version is updated to 11 + let version: String = query_row(&conn, "SELECT version FROM db_config", NO_PARAMS)? + .expect("Expected db_config to have a version"); + assert_eq!( + version, "11", + "Database version should be 11 after migration" + ); + + // Verify affirmation_weight column is dropped + let columns: Vec = conn + .prepare("PRAGMA table_info(block_headers)")? + .query_map([], |row| row.get(1))? + .collect::, _>>()?; + assert!( + !columns.contains(&"affirmation_weight".to_string()), + "affirmation_weight column should be dropped" + ); + + // Verify indexes are dropped + let indexes: Vec = conn + .prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'block_headers'")? + .query_map([], |row| row.get(0))? + .collect::, _>>()?; + assert!( + !indexes.contains(&"index_block_header_by_affirmation_weight".to_string()), + "index_block_header_by_affirmation_weight should be dropped" + ); + assert!( + !indexes.contains(&"index_block_header_by_height_and_affirmation_weight".to_string()), + "index_block_header_by_height_and_affirmation_weight should be dropped" + ); + + // Verify data integrity + let row: Option<(String, String, String)> = conn + .query_row( + "SELECT block_hash, consensus_hash, block_size + FROM block_headers WHERE index_block_hash = ?", + params![&sample_index_block_hash], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, String>(2)?, + )) + }, + ) + .optional()?; + assert!(row.is_some(), "Sample data should remain after migration"); + + let (block_hash, consensus_hash, block_size) = row.unwrap(); + assert_eq!( + block_hash, + sample_block_hash.to_string(), + "Block hash should be preserved" + ); + assert_eq!( + consensus_hash, + sample_consensus_hash.to_string(), + "Consensus hash should be preserved" + ); + assert_eq!(block_size, "1000", "Block size should be preserved"); + + Ok(()) + } } From f21093469f530d2bf1a7673fd2ca3bbbc76ced84 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 5 Sep 2025 11:10:46 -0700 Subject: [PATCH 35/35] CRC: cleanup get_schema_version Signed-off-by: Jacinta Ferrant --- stackslib/src/burnchains/db.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/stackslib/src/burnchains/db.rs b/stackslib/src/burnchains/db.rs index a92d68af5a..04777ad4a7 100644 --- a/stackslib/src/burnchains/db.rs +++ b/stackslib/src/burnchains/db.rs @@ -502,16 +502,9 @@ impl BurnchainDB { if !table_exists(conn, "db_config")? { return Ok(1); } - - // Query all version values and get the max - let mut stmt = conn.prepare("SELECT version FROM db_config")?; - let max_version: u32 = stmt - .query_map([], |row| row.get::<_, String>(0))? - .filter_map(Result::ok) - .filter_map(|v| v.parse::().ok()) - .max() - .unwrap_or(1); // default to 1 if table exists but empty - + let mut stmt = + conn.prepare("SELECT COALESCE(MAX(CAST(version AS INTEGER)), 1) FROM db_config")?; + let max_version: u32 = stmt.query_row([], |row| row.get(0))?; Ok(max_version) }