diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index c42ece0f1..7f096c475 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -94,6 +94,7 @@ jobs: - tests::epoch_21::test_pox_reorg_flap_duel - tests::epoch_21::test_pox_reorg_flap_reward_cycles - tests::epoch_21::test_pox_missing_five_anchor_blocks + - tests::epoch_21::test_sortition_divergence_pre_21 - tests::neon_integrations::bad_microblock_pubkey steps: - uses: actions/checkout@v2 diff --git a/clarity/src/vm/functions/arithmetic.rs b/clarity/src/vm/functions/arithmetic.rs index a1b6e945b..ec493d9de 100644 --- a/clarity/src/vm/functions/arithmetic.rs +++ b/clarity/src/vm/functions/arithmetic.rs @@ -389,9 +389,9 @@ fn special_geq_v1( context: &LocalContext, ) -> InterpreterResult { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Geq, env, 0)?; let a = eval(&args[0], env, context)?; let b = eval(&args[1], env, context)?; + runtime_cost(ClarityCostFunction::Geq, env, args.len())?; type_force_binary_comparison_v1!(geq, a, b) } @@ -432,9 +432,9 @@ fn special_leq_v1( context: &LocalContext, ) -> InterpreterResult { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Leq, env, 0)?; let a = eval(&args[0], env, context)?; let b = eval(&args[1], env, context)?; + runtime_cost(ClarityCostFunction::Leq, env, args.len())?; type_force_binary_comparison_v1!(leq, a, b) } @@ -474,9 +474,9 @@ fn special_greater_v1( context: &LocalContext, ) -> InterpreterResult { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Ge, env, 0)?; let a = eval(&args[0], env, context)?; let b = eval(&args[1], env, context)?; + runtime_cost(ClarityCostFunction::Ge, env, args.len())?; type_force_binary_comparison_v1!(greater, a, b) } @@ -516,9 +516,9 @@ fn special_less_v1( context: &LocalContext, ) -> InterpreterResult { check_argument_count(2, args)?; - runtime_cost(ClarityCostFunction::Le, env, 0)?; let a = eval(&args[0], env, context)?; let b = eval(&args[1], env, context)?; + runtime_cost(ClarityCostFunction::Le, env, args.len())?; type_force_binary_comparison_v1!(less, a, b) } diff --git a/clarity/src/vm/variables.rs b/clarity/src/vm/variables.rs index e88d963e3..ca5154b81 100644 --- a/clarity/src/vm/variables.rs +++ b/clarity/src/vm/variables.rs @@ -63,7 +63,9 @@ pub fn lookup_reserved_variable( _context: &LocalContext, env: &mut Environment, ) -> Result> { - if let Some(variable) = NativeVariables::lookup_by_name(name) { + if let Some(variable) = + NativeVariables::lookup_by_name_at_version(name, env.contract_context.get_clarity_version()) + { match variable { NativeVariables::TxSender => { let sender = env diff --git a/src/burnchains/affirmation.rs b/src/burnchains/affirmation.rs index b63b87d1b..56612e36c 100644 --- a/src/burnchains/affirmation.rs +++ b/src/burnchains/affirmation.rs @@ -290,7 +290,7 @@ impl AffirmationMapEntry { /// list behind accessor and mutator methods. #[derive(Clone, PartialEq)] pub struct AffirmationMap { - affirmations: Vec, + pub affirmations: Vec, } impl fmt::Display for AffirmationMapEntry { diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index 2c6e0160f..c21c57c9e 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -3009,6 +3009,27 @@ impl SortitionDB { } } +impl<'a> SortitionDBTx<'a> { + 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. + Ok(match affirmation_map.as_slice() { + [] => AffirmationMap::empty(), + a => AffirmationMap::new(a[1..].to_vec()), + }) + } +} + impl<'a> SortitionDBConn<'a> { pub fn as_handle<'b>(&'b self, chain_tip: &SortitionId) -> SortitionHandleConn<'b> { SortitionHandleConn { @@ -3771,11 +3792,10 @@ impl SortitionDB { /// Find the affirmation map represented by a given sortition ID. pub fn find_sortition_tip_affirmation_map( - _burnchain_db: &BurnchainDB, - sortition_db: &SortitionDB, + &self, tip_id: &SortitionId, ) -> Result { - let ih = sortition_db.index_handle(tip_id); + 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 @@ -4417,6 +4437,34 @@ impl SortitionDB { query_row(conn, sql, args) } + /// Get the last reward cycle in epoch 2.05 + pub fn get_last_epoch_2_05_reward_cycle(&self) -> Result { + Self::static_get_last_epoch_2_05_reward_cycle( + self.conn(), + self.first_block_height, + &self.pox_constants, + ) + } + + /// Get the last reward cycle in epoch 2.05 + pub fn static_get_last_epoch_2_05_reward_cycle( + conn: &DBConn, + first_block_height: u64, + pox_constants: &PoxConstants, + ) -> Result { + let epochs = SortitionDB::get_stacks_epochs(conn)?; + + for epoch in epochs { + if epoch.epoch_id == StacksEpochId::Epoch2_05 { + return Ok(pox_constants + .block_height_to_reward_cycle(first_block_height, epoch.end_height) + .expect("FATAL: end block of epoch 2.05 is before system start height")); + } + } + + Ok(u64::MAX) + } + /// Get the latest block snapshot on this fork where a sortition occured. /// Search snapshots up to (but excluding) the given block height. /// Will always return a snapshot -- even if it's the initial sentinel snapshot. diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index ddc4e8c9c..e6fdc5d1b 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -682,6 +682,131 @@ fn forget_orphan_stacks_blocks( Ok(()) } +/// Consolidate affirmation maps. +/// `sort_am` will be the prefix of the resulting AM. +/// If `given_am` represents more reward cycles than `last_2_05_rc`, then its affirmations will be +/// appended to `sort_am` to compute the consolidated affirmation map. +/// +/// This way, the affirmation map reflects affirmations made under the 2.05 rules during epoch 2.05 +/// reward cycles, and affirmations made under the 2.1 rules during epoch 2.1. +fn consolidate_affirmation_maps( + given_am: AffirmationMap, + sort_am: &AffirmationMap, + last_2_05_rc: usize, +) -> AffirmationMap { + let mut am_entries = vec![]; + for i in 0..last_2_05_rc { + if i < sort_am.affirmations.len() { + am_entries.push(sort_am.affirmations[i]); + } else { + return AffirmationMap::new(am_entries); + } + } + for i in last_2_05_rc..given_am.len() { + am_entries.push(given_am.affirmations[i]); + } + + 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, + 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, + )?; + + 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, + 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, + 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< 'a, T: BlockEventDispatcher, @@ -714,17 +839,40 @@ impl< let sn = SortitionDB::get_block_snapshot(self.sortition_db.conn(), &sort_id)? .expect("FATAL: have sortition ID without snapshot"); - let sort_am = SortitionDB::find_sortition_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db, - &sort_id, - )?; + 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, + ) -> Result { + static_get_heaviest_affirmation_map( + &self.burnchain, + &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_blocks_db, + &self.sortition_db, + &self.chain_state_db, + 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( @@ -735,6 +883,12 @@ impl< 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, @@ -757,14 +911,18 @@ impl< for hdr in all_headers { // load this block's affirmation map - let am = match StacksChainState::find_stacks_tip_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(ChainstateError::DBError(DBError::InvalidPoxSortition)) => { + Err(Error::ChainstateError(ChainstateError::DBError( + DBError::InvalidPoxSortition, + ))) => { debug!( "Stacks tip {}/{} is not on a valid sortition", &hdr.consensus_hash, @@ -883,13 +1041,6 @@ impl< let (canonical_ch, canonical_bhh) = SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortition_db.conn())?; - let stacks_tip_affirmation_map = StacksChainState::find_stacks_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db.conn(), - &canonical_ch, - &canonical_bhh, - )?; - let sortition_tip = match &self.canonical_sortition_tip { Some(tip) => tip.clone(), None => { @@ -898,24 +1049,21 @@ impl< sn.sortition_id } }; - - let sortition_tip_affirmation_map = SortitionDB::find_sortition_tip_affirmation_map( + 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 heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - self.burnchain_blocks_db.conn(), - &self.burnchain, - )?; + let sortition_tip_affirmation_map = self + .sortition_db + .find_sortition_tip_affirmation_map(&sortition_tip)?; - let canonical_affirmation_map = StacksChainState::find_canonical_affirmation_map( - &self.burnchain, - &self.burnchain_blocks_db, - &self.chain_state_db, - ) - .expect("FATAL: failed to load canonical Stacks affirmation map"); + 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 `{}`", @@ -1107,11 +1255,9 @@ impl< // of the heaviest affirmation map let mut found_diverged = false; for sort_id in sort_ids.iter() { - let sort_am = SortitionDB::find_sortition_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db, - &sort_id, - )?; + let sort_am = self + .sortition_db + .find_sortition_tip_affirmation_map(&sort_id)?; debug!( "Compare {} as prefix of {}? {}", @@ -1269,6 +1415,8 @@ impl< "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)? .expect(&format!("FATAL: no sortition {}", &sortition_tip)) @@ -1279,10 +1427,7 @@ impl< .block_height_to_reward_cycle(sortition_height) .unwrap_or(0); - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - self.burnchain_blocks_db.conn(), - &self.burnchain, - )?; + let heaviest_am = self.get_heaviest_affirmation_map(&sortition_tip)?; if let Some(changed_reward_cycle) = self.check_chainstate_against_burnchain_affirmations()? @@ -1321,39 +1466,36 @@ impl< // 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 = - StacksChainState::find_canonical_affirmation_map( - &self.burnchain, - &self.burnchain_blocks_db, - &self.chain_state_db, - )?; - let sort_am = SortitionDB::find_sortition_tip_affirmation_map( - &self.burnchain_blocks_db, - &self.sortition_db, - &sortition_tip, - )?; + 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 { - let diverged_rc = canonical_affirmation_map - .find_divergence(&sort_am) - .expect("FATAL: unequal affirmations maps should diverge"); - debug!( - "Sortition AM `{}` diverges from canonical AM `{}` at cycle {}", - &sort_am, &canonical_affirmation_map, 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, - )) + if let Some(diverged_rc) = + canonical_affirmation_map.find_divergence(&sort_am) + { + debug!( + "Sortition AM `{}` diverges from canonical AM `{}` at cycle {}", + &sort_am, &canonical_affirmation_map, 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 }; @@ -1367,6 +1509,9 @@ impl< // 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( @@ -1376,9 +1521,12 @@ impl< &mut sort_tx, &self.chain_state_db.db(), )?; - let stacks_am = StacksChainState::find_stacks_tip_affirmation_map( + + let stacks_am = inner_static_get_stacks_tip_affirmation_map( &self.burnchain_blocks_db, - &mut sort_tx, + last_2_05_rc, + &sort_tx.find_sortition_tip_affirmation_map(&sortition_tip)?, + &sort_tx, &canonical_ch, &canonical_bhh, )?; @@ -1489,8 +1637,16 @@ impl< // 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 = StacksChainState::find_stacks_tip_affirmation_map(&self.burnchain_blocks_db, sort_tx, &canonical_ch, &canonical_bhh) - .expect("FATAL: failed to query stacks 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(&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); @@ -1537,8 +1693,16 @@ impl< // 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 = StacksChainState::find_stacks_tip_affirmation_map(&self.burnchain_blocks_db, sort_tx, &canonical_ch, &canonical_bhh) - .expect("FATAL: failed to query stacks 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(&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); @@ -1570,8 +1734,16 @@ impl< // 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 = StacksChainState::find_stacks_tip_affirmation_map(&self.burnchain_blocks_db, sort_tx, &canonical_ch, &canonical_bhh) - .expect("FATAL: failed to query stacks 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(&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); @@ -1630,16 +1802,24 @@ impl< )? .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, - &SortitionDB::find_sortition_tip_affirmation_map(&self.burnchain_blocks_db, &self.sortition_db, &highest_valid_snapshot.sortition_id)?, + &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, - &StacksChainState::find_stacks_tip_affirmation_map(&self.burnchain_blocks_db, &self.sortition_db.conn(), &highest_valid_snapshot.canonical_stacks_tip_consensus_hash, &highest_valid_snapshot.canonical_stacks_tip_hash)?, + &stacks_tip_affirmation_map, &heaviest_am ); @@ -1649,16 +1829,24 @@ impl< 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, - &SortitionDB::find_sortition_tip_affirmation_map(&self.burnchain_blocks_db, &self.sortition_db, &highest_valid_snapshot.sortition_id)?, + &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, - &StacksChainState::find_stacks_tip_affirmation_map(&self.burnchain_blocks_db, &self.sortition_db.conn(), &highest_valid_snapshot.canonical_stacks_tip_consensus_hash, &highest_valid_snapshot.canonical_stacks_tip_hash)?, + &stacks_tip_affirmation_map, &heaviest_am ); } @@ -1776,19 +1964,6 @@ impl< Ok(None) } - /// Get the canonical affirmation map in the system. - /// This is the heaviest affirmation map, concatenated with our tentative affirmation status - /// for unaffirmed PoX anchor blocks (i.e. we treat them as affirmed if we have them, and - /// unaffirmed if we don't). - pub fn get_canonical_affirmation_map(&self) -> Result { - StacksChainState::find_canonical_affirmation_map( - &self.burnchain, - &self.burnchain_blocks_db, - &self.chain_state_db, - ) - .map_err(|e| e.into()) - } - /// Try to revalidate a sortition if it exists already. This can happen if the node flip/flops /// between two PoX forks. /// @@ -1917,6 +2092,8 @@ impl< ) -> Result, Error> { debug!("Handle new burnchain block"); + 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. // @@ -1953,12 +2130,11 @@ impl< }; let canonical_burnchain_tip = self.burnchain_blocks_db.get_canonical_chain_tip()?; - let canonical_affirmation_map = self.get_canonical_affirmation_map()?; + // let canonical_affirmation_map = self.get_canonical_affirmation_map()?; + let canonical_affirmation_map = + self.get_canonical_affirmation_map(&canonical_snapshot.sortition_id)?; - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - self.burnchain_blocks_db.conn(), - &self.burnchain, - )?; + let heaviest_am = self.get_heaviest_affirmation_map(&canonical_snapshot.sortition_id)?; debug!("Handle new canonical burnchain tip"; "height" => %canonical_burnchain_tip.block_height, @@ -2160,9 +2336,11 @@ impl< &ch, &bhh, height, &heaviest_am ); - let am = StacksChainState::find_stacks_tip_affirmation_map( + let am = inner_static_get_stacks_tip_affirmation_map( &self.burnchain_blocks_db, - &mut sort_tx, + last_2_05_rc, + &sort_tx.find_sortition_tip_affirmation_map(&next_snapshot.sortition_id)?, + &sort_tx, &ch, &bhh, )?; @@ -2278,9 +2456,11 @@ impl< ) .expect("FATAL: could not find a valid parent Stacks block"); - let stacks_am = StacksChainState::find_stacks_tip_affirmation_map( + let stacks_am = inner_static_get_stacks_tip_affirmation_map( &self.burnchain_blocks_db, - &mut sort_tx, + last_2_05_rc, + &sort_tx.find_sortition_tip_affirmation_map(&highest_valid_sortition_id)?, + &sort_tx, &canonical_ch, &canonical_bhh, ) @@ -2532,10 +2712,10 @@ impl< &winner_snapshot.winning_block_txid, )? { // affirmed? - let heaviest_am = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - self.burnchain_blocks_db.conn(), - &self.burnchain, - )?; + 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(), diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 808776c21..7cb5354b2 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -2581,7 +2581,8 @@ impl StacksChainState { Ok(AffirmationMap::empty()) } - /// Get the affirmation map represented by the Stacks chain tip + /// 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, diff --git a/src/chainstate/stacks/miner.rs b/src/chainstate/stacks/miner.rs index 2075d6da4..a65eeab78 100644 --- a/src/chainstate/stacks/miner.rs +++ b/src/chainstate/stacks/miner.rs @@ -83,12 +83,14 @@ use clarity::vm::types::TypeSignature; #[derive(Debug, Clone, PartialEq)] pub struct MinerStatus { blockers: HashSet, + spend_amount: u64, } impl MinerStatus { - pub fn make_ready() -> MinerStatus { + pub fn make_ready(spend_amount: u64) -> MinerStatus { MinerStatus { blockers: HashSet::new(), + spend_amount, } } @@ -108,6 +110,13 @@ impl MinerStatus { false } } + + pub fn get_spend_amount(&self) -> u64 { + return self.spend_amount; + } + pub fn set_spend_amount(&mut self, amt: u64) { + self.spend_amount = amt; + } } impl std::fmt::Display for MinerStatus { @@ -140,6 +149,24 @@ pub fn signal_mining_ready(miner_status: Arc>) { } } +/// get the mining amount +pub fn get_mining_spend_amount(miner_status: Arc>) -> u64 { + match miner_status.lock() { + Ok(status) => status.get_spend_amount(), + Err(_e) => { + panic!("FATAL: mutex poisoned"); + } + } +} + +/// set the mining amount +pub fn set_mining_spend_amount(miner_status: Arc>, amt: u64) { + miner_status + .lock() + .expect("FATAL: mutex poisoned") + .set_spend_amount(amt); +} + #[derive(Debug, Clone)] pub struct BlockBuilderSettings { pub max_miner_time_ms: u64, @@ -152,7 +179,7 @@ impl BlockBuilderSettings { BlockBuilderSettings { max_miner_time_ms: u64::max_value(), mempool_settings: MemPoolWalkSettings::default(), - miner_status: Arc::new(Mutex::new(MinerStatus::make_ready())), + miner_status: Arc::new(Mutex::new(MinerStatus::make_ready(0))), } } @@ -160,7 +187,7 @@ impl BlockBuilderSettings { BlockBuilderSettings { max_miner_time_ms: u64::max_value(), mempool_settings: MemPoolWalkSettings::zero(), - miner_status: Arc::new(Mutex::new(MinerStatus::make_ready())), + miner_status: Arc::new(Mutex::new(MinerStatus::make_ready(0))), } } } diff --git a/src/main.rs b/src/main.rs index 331c5e7c2..65c73c71a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -943,7 +943,8 @@ simulating a miner. let itip = StacksBlockHeader::make_index_block_hash(&consensustip, &tip); let key = &argv[5]; - let marf_opts = MARFOpenOpts::default(); + let mut marf_opts = MARFOpenOpts::default(); + marf_opts.external_blobs = true; let mut marf = MARF::from_path(path, marf_opts).unwrap(); let res = marf.get(&itip, key).expect("MARF error."); match res { diff --git a/src/net/p2p.rs b/src/net/p2p.rs index 03a88900d..c5096e8fa 100644 --- a/src/net/p2p.rs +++ b/src/net/p2p.rs @@ -46,6 +46,10 @@ use crate::burnchains::BurnchainView; use crate::burnchains::PublicKey; use crate::chainstate::burn::db::sortdb::{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, +}; use crate::chainstate::stacks::db::StacksChainState; use crate::chainstate::stacks::{MAX_BLOCK_LEN, MAX_TRANSACTION_LEN}; use crate::monitoring::{update_inbound_neighbors, update_outbound_neighbors}; @@ -4983,20 +4987,30 @@ impl PeerNetwork { // update heaviest affirmation map view let burnchain_db = self.burnchain.open_burnchain_db(false)?; - self.heaviest_affirmation_map = BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), + + self.heaviest_affirmation_map = static_get_heaviest_affirmation_map( &self.burnchain, - )?; - self.tentative_best_affirmation_map = StacksChainState::find_canonical_affirmation_map( - &self.burnchain, - &burnchain_db, - chainstate, - )?; - self.sortition_tip_affirmation_map = SortitionDB::find_sortition_tip_affirmation_map( &burnchain_db, sortdb, &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, + &burnchain_db, + sortdb, + chainstate, + &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, &sn.sortition_id)?; // update last anchor data let ih = sortdb.index_handle(&sn.sortition_id); @@ -5014,12 +5028,16 @@ impl PeerNetwork { { // update stacks tip affirmation map view let burnchain_db = self.burnchain.open_burnchain_db(false)?; - self.stacks_tip_affirmation_map = StacksChainState::find_stacks_tip_affirmation_map( + self.stacks_tip_affirmation_map = static_get_stacks_tip_affirmation_map( &burnchain_db, - sortdb.conn(), + sortdb, + &sn.sortition_id, &sn.canonical_stacks_tip_consensus_hash, &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 diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index eec52d852..487de0339 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -611,6 +611,7 @@ impl Config { pub fn from_config_file(config_file: ConfigFile) -> Result { let default_node_config = NodeConfig::default(); + let mut has_require_affirmed_anchor_blocks = false; let (mut node, bootstrap_node, deny_nodes) = match config_file.node { Some(node) => { let rpc_bind = node.rpc_bind.unwrap_or(default_node_config.rpc_bind); @@ -669,9 +670,16 @@ impl Config { .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: node - .require_affirmed_anchor_blocks - .unwrap_or(!node.miner.unwrap_or(!default_node_config.miner)), + require_affirmed_anchor_blocks: match node.require_affirmed_anchor_blocks { + Some(x) => { + has_require_affirmed_anchor_blocks = true; + x + } + None => { + has_require_affirmed_anchor_blocks = false; + !node.miner.unwrap_or(!default_node_config.miner) + } + }, // chainstate fault_injection activation for hide_blocks. // you can't set this in the config file. fault_injection_hide_blocks: false, @@ -717,6 +725,14 @@ impl Config { )); } } + } else { + // testnet requires that we use the 2.05 rules for anchor block affirmations, + // because reward cycle 360 (and possibly future ones) has a different anchor + // block choice in 2.05 rules than in 2.1 rules. + if !has_require_affirmed_anchor_blocks { + debug!("Set `require_affirmed_anchor_blocks` to `false` for non-mainnet config"); + node.require_affirmed_anchor_blocks = false; + } } let mut result = BurnchainConfig { diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index d83a69af3..59317e44b 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -166,8 +166,9 @@ use stacks::chainstate::stacks::db::{StacksChainState, MINER_REWARD_MATURITY}; use stacks::chainstate::stacks::Error as ChainstateError; use stacks::chainstate::stacks::StacksPublicKey; use stacks::chainstate::stacks::{ - miner::signal_mining_blocked, miner::signal_mining_ready, miner::BlockBuilderSettings, - miner::MinerStatus, miner::StacksMicroblockBuilder, StacksBlockBuilder, StacksBlockHeader, + miner::get_mining_spend_amount, miner::signal_mining_blocked, miner::signal_mining_ready, + miner::BlockBuilderSettings, miner::MinerStatus, miner::StacksMicroblockBuilder, + StacksBlockBuilder, StacksBlockHeader, }; use stacks::chainstate::stacks::{ CoinbasePayload, StacksBlock, StacksMicroblock, StacksTransaction, StacksTransactionSigner, @@ -1712,7 +1713,8 @@ impl BlockMinerThread { } }; - let burn_fee_cap = self.config.burnchain.burn_fee_cap; + // let burn_fee_cap = self.config.burnchain.burn_fee_cap; + let burn_fee_cap = get_mining_spend_amount(self.globals.get_miner_status()); let sunset_burn = self.burnchain.expected_sunset_burn( self.burn_block.block_height + 1, burn_fee_cap, diff --git a/testnet/stacks-node/src/run_loop/neon.rs b/testnet/stacks-node/src/run_loop/neon.rs index 12c0e676e..3dd8ac01b 100644 --- a/testnet/stacks-node/src/run_loop/neon.rs +++ b/testnet/stacks-node/src/run_loop/neon.rs @@ -17,14 +17,14 @@ use stacks::deps::ctrlc as termination; use stacks::deps::ctrlc::SignalId; use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType}; -use stacks::burnchains::db::BurnchainDB; use stacks::burnchains::Burnchain; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::BlockSnapshot; use stacks::chainstate::coordinator::comm::{CoordinatorChannels, CoordinatorReceivers}; use stacks::chainstate::coordinator::{ - migrate_chainstate_dbs, ChainsCoordinator, ChainsCoordinatorConfig, CoordinatorCommunication, - Error as coord_error, + 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, }; use stacks::chainstate::stacks::db::{ChainStateBootData, StacksChainState}; use stacks::core::StacksEpochId; @@ -59,6 +59,12 @@ pub type RunLoopCounter = Arc; #[cfg(not(test))] pub type RunLoopCounter = (); +#[cfg(test)] +const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 30; + +#[cfg(not(test))] +const UNCONDITIONAL_CHAIN_LIVENESS_CHECK: u64 = 300; + #[derive(Clone)] pub struct Counters { pub blocks_processed: RunLoopCounter, @@ -174,7 +180,9 @@ impl RunLoop { let channels = CoordinatorCommunication::instantiate(); let should_keep_running = Arc::new(AtomicBool::new(true)); let pox_watchdog_comms = PoxSyncWatchdogComms::new(should_keep_running.clone()); - let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready())); + let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready( + config.burnchain.burn_fee_cap, + ))); let mut event_dispatcher = EventDispatcher::new(); for observer in config.events_observers.iter() { @@ -635,9 +643,14 @@ impl RunLoop { .open_burnchain_db(false) .expect("FATAL: failed to open burnchain DB"); - let heaviest_affirmation_map = match BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - burnchain, + let sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) + .expect("FATAL: could not read sortition DB"); + + let heaviest_affirmation_map = match static_get_heaviest_affirmation_map( + &burnchain, + &burnchain_db, + sortdb, + &sn.sortition_id, ) { Ok(am) => am, Err(e) => { @@ -646,9 +659,6 @@ impl RunLoop { } }; - let sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) - .expect("FATAL: could not read sortition DB"); - let highest_sn = SortitionDB::get_highest_known_burn_chain_tip(sortdb.conn()) .expect("FATAL: could not read sortition DB"); @@ -656,21 +666,19 @@ impl RunLoop { .get_canonical_chain_tip() .expect("FATAL: could not read burnchain DB"); - let sortition_tip_affirmation_map = match SortitionDB::find_sortition_tip_affirmation_map( + 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, - ) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find sortition affirmation map: {:?}", &e); - return; - } - }; - - let stacks_tip_affirmation_map = StacksChainState::find_stacks_tip_affirmation_map( - &burnchain_db, - sortdb.conn(), &sn.canonical_stacks_tip_consensus_hash, &sn.canonical_stacks_tip_hash, ) @@ -730,6 +738,7 @@ impl RunLoop { sortdb: &SortitionDB, chain_state_db: &StacksChainState, last_burn_pox_reorg_recover_time: &mut u128, + last_announce_time: &mut u128, ) { let delay = cmp::max( 1, @@ -770,21 +779,20 @@ impl RunLoop { 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( + 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 heaviest_affirmation_map = match static_get_heaviest_affirmation_map( + &burnchain, &burnchain_db, sortdb, &sn.sortition_id, - ) { - Ok(am) => am, - Err(e) => { - warn!("Failed to find sortition affirmation map: {:?}", &e); - return; - } - }; - - let heaviest_affirmation_map = match BurnchainDB::get_heaviest_anchor_block_affirmation_map( - burnchain_db.conn(), - burnchain, ) { Ok(am) => am, Err(e) => { @@ -793,12 +801,19 @@ impl RunLoop { } }; - let canonical_affirmation_map = StacksChainState::find_canonical_affirmation_map( + let canonical_affirmation_map = match static_get_canonical_affirmation_map( &burnchain, &burnchain_db, - chain_state_db, - ) - .expect("FATAL: failed to load canonical Stacks affirmation map"); + 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 @@ -809,6 +824,7 @@ impl RunLoop { debug!("Drive burn block processing: possible PoX reorg (sortition tip: {}, heaviest: {}, {} = heaviest_affirmation_map.len() && sortition_tip_affirmation_map.len() <= canonical_affirmation_map.len() { @@ -820,6 +836,7 @@ impl RunLoop { debug!("Drive burnchain processing: possible PoX reorg from unprocessed anchor block(s) (sortition tip: {}, heaviest: {}, canonical: {})", &sortition_tip_affirmation_map, &heaviest_affirmation_map, &canonical_affirmation_map); globals.coord().announce_new_burn_block(); globals.coord().announce_new_stacks_block(); + *last_announce_time = get_epoch_time_secs().into(); } } } else { @@ -833,6 +850,18 @@ impl RunLoop { } *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 @@ -846,6 +875,7 @@ impl RunLoop { ) { 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!"); @@ -858,6 +888,7 @@ impl RunLoop { &sortdb, &chain_state_db, &mut last_burn_pox_reorg_recover_time, + &mut last_burn_announce_time, ); Self::drive_pox_reorg_stacks_block_processing( &globals, diff --git a/testnet/stacks-node/src/tests/epoch_21.rs b/testnet/stacks-node/src/tests/epoch_21.rs index eab1bd3a3..c14c228e2 100644 --- a/testnet/stacks-node/src/tests/epoch_21.rs +++ b/testnet/stacks-node/src/tests/epoch_21.rs @@ -3,7 +3,6 @@ use std::collections::HashSet; use std::env; use std::thread; -use stacks::burnchains::affirmation::AffirmationMap; use stacks::burnchains::Burnchain; use stacks::chainstate::stacks::db::StacksChainState; use stacks::chainstate::stacks::StacksBlockHeader; @@ -52,6 +51,7 @@ use stacks_common::util::hash::Sha256Sum; use stacks_common::util::secp256k1::Secp256k1PublicKey; use stacks::chainstate::coordinator::comm::CoordinatorChannels; +use stacks::chainstate::stacks::miner::set_mining_spend_amount; use stacks::chainstate::stacks::miner::signal_mining_blocked; use stacks::chainstate::stacks::miner::signal_mining_ready; @@ -1979,9 +1979,6 @@ fn transition_empty_blocks() { fn wait_pox_stragglers(confs: &[Config], max_stacks_tip: u64, block_time_ms: u64) { loop { let mut straggler = false; - let mut stacker_am = None; - let mut heaviest_am = None; - let mut sortition_am = None; let mut stacks_tip_ch = None; let mut stacks_tip_bhh = None; @@ -1993,22 +1990,6 @@ fn wait_pox_stragglers(confs: &[Config], max_stacks_tip: u64, block_time_ms: u64 straggler = true; } - let affirmations = tip_info.affirmations.unwrap(); - - if let Some(stacker_am) = stacker_am.as_ref() { - if affirmations.stacks_tip != *stacker_am { - straggler = true; - } - } else { - stacker_am = Some(affirmations.stacks_tip); - } - if let Some(heaviest_am) = heaviest_am.as_ref() { - if affirmations.heaviest != *heaviest_am { - straggler = true; - } - } else { - heaviest_am = Some(affirmations.heaviest); - } if let Some(stacks_tip_ch) = stacks_tip_ch.as_ref() { if *stacks_tip_ch != tip_info.stacks_tip_consensus_hash { straggler = true; @@ -2023,19 +2004,6 @@ fn wait_pox_stragglers(confs: &[Config], max_stacks_tip: u64, block_time_ms: u64 } else { stacks_tip_bhh = Some(tip_info.stacks_tip); } - - // sortition affirmation map can differ by the last affirmation - if let Some(sortition_am) = sortition_am.as_ref() { - let mut tip_sort_am = affirmations.sortition_tip.clone(); - tip_sort_am.pop(); - if tip_sort_am != *sortition_am { - straggler = true; - } - } else { - let mut tip_sort_am = affirmations.sortition_tip.clone(); - tip_sort_am.pop(); - sortition_am = Some(tip_sort_am); - } } if !straggler { break; @@ -4261,6 +4229,454 @@ fn test_pox_missing_five_anchor_blocks() { } } +#[test] +#[ignore] +/// Verify that if the sortition AM declares that an anchor block is present in epoch 2.05, but the +/// heaviest AM declares it absent, that this is _not_ treated as a divergence of this behavior +/// manifests in epoch 2.05. +fn test_sortition_divergence_pre_21() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let num_miners = 2; + + let reward_cycle_len = 10; + let prepare_phase_len = 5; + let v1_unlock_height = 242; + + 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); + + 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 + let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec(); + epochs[1].end_height = 101; + epochs[2].start_height = 101; + epochs[2].end_height = 241; + epochs[3].start_height = 241; + conf_template.burnchain.epochs = Some(epochs); + + let privks: Vec<_> = (0..5) + .into_iter() + .map(|_| StacksPrivateKey::new()) + .collect(); + + let stack_privks: Vec<_> = (0..5) + .into_iter() + .map(|_| StacksPrivateKey::new()) + .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::new().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.clone(); + conf.node.require_affirmed_anchor_blocks = + conf_template.node.require_affirmed_anchor_blocks; + + conf.node.always_use_affirmation_maps = false; + + // 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_sync_interval = 6; + + let rpc_port = 41113 + 10 * i; + let p2p_port = 41113 + 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); + for i in 1..num_miners { + 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(); + + confs[i].node.set_bootstrap_nodes( + format!( + "{}@{}", + &StacksPublicKey::from_private(&node_privkey_1).to_hex(), + p2p_bind + ), + chain_id, + peer_version, + ); + } + + // use short reward cycles + for i in 0..num_miners { + let mut burnchain_config = Burnchain::regtest(&confs[i].get_burn_db_path()); + let pox_constants = PoxConstants::new( + reward_cycle_len, + prepare_phase_len, + 3, + 5, + 15, + (1600 * reward_cycle_len - 1).into(), + (1700 * reward_cycle_len).into(), + v1_unlock_height, + ); + burnchain_config.pox_constants = pox_constants.clone(); + + burnchain_configs.push(burnchain_config); + } + + let mut btcd_controller = BitcoinCoreController::new(confs[0].clone()); + 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 i in 1..num_miners { + let old_mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); + btc_regtest_controller + .set_mining_pubkey(confs[i].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 i in 0..num_miners { + wait_for_runloop(&blocks_processed[i as usize]); + } + + // 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: {:?}\n\n", &tip_info); + 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 in 1..num_miners { + eprintln!("\n\nBoot miner {}\n\n", i); + loop { + let tip_info_opt = get_chain_info_opt(&confs[i]); + if let Some(tip_info) = tip_info_opt { + eprintln!("\n\nMiner 2: {:?}\n\n", &tip_info); + if tip_info.stacks_tip_height > 0 { + break; + } + } else { + eprintln!("\n\nWaiting for miner {}...\n\n", i); + } + next_block_and_iterate( + &mut btc_regtest_controller, + &blocks_processed[i as usize], + 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() + .to_vec(), + ); + + let sort_height = channels[0].get_sortitions_processed(); + + // make everyone stack + let stacking_txs: Vec<_> = stack_privks + .iter() + .enumerate() + .map(|(_i, pk)| { + make_contract_call( + pk, + 0, + 1360, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox", + "stack-stx", + &[ + Value::UInt(2_000_000_000_000_000 - 30_000_000), + execute( + &format!("{{ hashbytes: 0x{}, version: 0x00 }}", pox_pubkey_hash), + 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, false)) + .collect(); + + // everyone locks up + let mut cnt = 0; + for tx in stacking_txs { + eprintln!("\n\nSubmit stacking tx {}\n\n", &cnt); + submit_tx(&http_origin, &tx); + cnt += 1; + } + + // 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 {}\n\n", &cnt); + 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 ##############################"); + + let mut max_stacks_tip = 0; + + // prevent Stacks at these heights from propagating + env::set_var( + "STACKS_HIDE_BLOCKS_AT_HEIGHT", + "[223,224,225,226,227,228,229,230]", + ); + + // mine a reward cycle in which the 2.05 rules choose a PoX anchor block, but the 2.1 rules do + // not. + for i in 0..10 { + eprintln!("\n\nBuild block {}\n\n", i); + 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 && i < reward_cycle_len - prepare_phase_len + 3 + { + // only miner 0 mines. + signal_mining_ready(miner_status[0].clone()); + signal_mining_blocked(miner_status[1].clone()); + } else if i >= reward_cycle_len - prepare_phase_len + 3 && i < reward_cycle_len { + // only miner 1 mines, and they mine *hard*. + signal_mining_blocked(miner_status[0].clone()); + signal_mining_ready(miner_status[1].clone()); + set_mining_spend_amount( + miner_status[1].clone(), + 10 * conf_template.burnchain.burn_fee_cap, + ); + } + } + + signal_mining_ready(miner_status[0].clone()); + signal_mining_ready(miner_status[1].clone()); + set_mining_spend_amount( + miner_status[1].clone(), + conf_template.burnchain.burn_fee_cap, + ); + + 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 ##############################"); + + for i in 0..10 { + eprintln!("\n\nBuild block {}\n\n", i); + 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); + } + } + + 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 ##############################"); + + // run some cycles in 2.1 + for _ in 0..2 { + for i in 0..10 { + eprintln!("\n\nBuild block {}\n\n", i); + 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); + } + } + } + + // 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); + } + + 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); + + // 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); + } +} + /* #[test] #[ignore] diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 02a313378..cd8161e5c 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -6267,6 +6267,8 @@ 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!( @@ -6289,6 +6291,8 @@ fn atlas_integration_test() { events_keys: vec![EventKeyType::AnyEvent], }); + 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 @@ -6802,6 +6806,8 @@ 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!( @@ -6834,6 +6840,8 @@ 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 @@ -7076,6 +7084,8 @@ 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(); @@ -8550,6 +8560,10 @@ fn spawn_follower_node( conf.burnchain.ast_precheck_size_height = initial_conf.burnchain.ast_precheck_size_height.clone(); + 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(); @@ -9764,6 +9778,8 @@ fn test_problematic_microblocks_are_not_relayed_or_stored() { conf.miner.microblock_attempt_time_ms = 1_000; conf.node.wait_time_for_microblocks = 0; + conf.connection_options.inv_sync_interval = 3; + test_observer::spawn(); conf.events_observers.push(EventObserverConfig {