diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 3eea180f08..925fd901fc 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -72,6 +72,9 @@ jobs: - tests::epoch_205::test_cost_limit_switch_version205 - tests::epoch_205::test_exact_block_costs - tests::epoch_205::bigger_microblock_streams_in_2_05 + - tests::epoch_21::transition_fixes_utxo_chaining + - tests::epoch_21::transition_adds_burn_block_height + - tests::epoch_21::transition_caps_discount_mining_upside steps: - uses: actions/checkout@v2 - name: Download docker image diff --git a/src/burnchains/burnchain.rs b/src/burnchains/burnchain.rs index 97ac593d22..4d49b92176 100644 --- a/src/burnchains/burnchain.rs +++ b/src/burnchains/burnchain.rs @@ -120,6 +120,11 @@ impl BurnchainStateTransition { let mut block_commits: Vec = vec![]; let mut user_burns: Vec = vec![]; let mut accepted_ops = Vec::with_capacity(block_ops.len()); + let epoch = SortitionDB::get_stacks_epoch(sort_tx, parent_snapshot.block_height + 1)? + .expect(&format!( + "FATAL: no epoch known at burn height {}", + parent_snapshot.block_height + 1 + )); assert!(Burnchain::ops_are_sorted(block_ops)); @@ -240,6 +245,7 @@ impl BurnchainStateTransition { // calculate the burn distribution from these operations. // The resulting distribution will contain the user burns that match block commits let burn_dist = BurnSamplePoint::make_min_median_distribution( + epoch.epoch_id, windowed_block_commits, windowed_missed_commits, burn_blocks, @@ -461,10 +467,22 @@ impl Burnchain { (effective_height % (self.pox_constants.reward_cycle_length as u64)) == 1 } - pub fn reward_cycle_to_block_height(&self, reward_cycle: u64) -> u64 { + pub fn static_reward_cycle_to_block_height( + reward_cycle: u64, + first_block_ht: u64, + reward_cycle_len: u64, + ) -> u64 { // NOTE: the `+ 1` is because the height of the first block of a reward cycle is mod 1, not // mod 0. - self.first_block_height + reward_cycle * (self.pox_constants.reward_cycle_length as u64) + 1 + first_block_ht + reward_cycle * reward_cycle_len + 1 + } + + pub fn reward_cycle_to_block_height(&self, reward_cycle: u64) -> u64 { + Self::static_reward_cycle_to_block_height( + reward_cycle, + self.first_block_height, + self.pox_constants.reward_cycle_length.into(), + ) } /// Returns the active reward cycle at the given burn block height @@ -1834,6 +1852,7 @@ pub mod tests { memo: vec![0x80], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1874,6 +1893,7 @@ pub mod tests { memo: vec![0x80], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1914,6 +1934,7 @@ pub mod tests { memo: vec![0x80], burn_fee: 23456, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2232,7 +2253,7 @@ pub mod tests { let burn_total = block_ops_124.iter().fold(0u64, |mut acc, op| { let bf = match op { - BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, + BlockstackOperationType::LeaderBlockCommit(ref op) => op.total_spend(), BlockstackOperationType::UserBurnSupport(ref op) => 0, _ => 0, }; @@ -2480,6 +2501,7 @@ pub mod tests { memo: vec![i], burn_fee: i as u64, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( diff --git a/src/chainstate/burn/db/processing.rs b/src/chainstate/burn/db/processing.rs index 96f66f62d7..034c912a37 100644 --- a/src/chainstate/burn/db/processing.rs +++ b/src/chainstate/burn/db/processing.rs @@ -20,6 +20,7 @@ use crate::burnchains::{ Burnchain, BurnchainBlockHeader, BurnchainStateTransition, Error as BurnchainError, }; +use crate::chainstate::burn::db::sortdb::SortitionDB; use crate::chainstate::burn::db::sortdb::{InitialMiningBonus, SortitionHandleTx}; use crate::chainstate::burn::operations::{ leader_block_commit::{MissedBlockCommit, RewardSetInfo}, @@ -114,6 +115,12 @@ impl<'a> SortitionHandleTx<'a> { ) -> Result<(BlockSnapshot, BurnchainStateTransition), BurnchainError> { let this_block_height = block_header.block_height; let this_block_hash = block_header.block_hash.clone(); + let epoch = SortitionDB::get_stacks_epoch(self, parent_snapshot.block_height + 1)?.expect( + &format!( + "FATAL: no epoch known at burn height {}", + parent_snapshot.block_height + 1 + ), + ); // make the burn distribution, and in doing so, identify the user burns that we'll keep let state_transition = BurnchainStateTransition::from_block_ops(self, burnchain, parent_snapshot, this_block_ops, missed_commits) @@ -128,7 +135,9 @@ impl<'a> SortitionHandleTx<'a> { .fold(Some(0u64), |acc, op| { if let Some(acc) = acc { let bf = match op { - BlockstackOperationType::LeaderBlockCommit(ref op) => op.burn_fee, + BlockstackOperationType::LeaderBlockCommit(ref op) => { + op.sortition_spend(epoch.epoch_id) + } BlockstackOperationType::UserBurnSupport(ref op) => op.burn_fee, _ => 0, }; @@ -425,6 +434,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( diff --git a/src/chainstate/burn/db/sortdb.rs b/src/chainstate/burn/db/sortdb.rs index 5e9533b66f..26d4465947 100644 --- a/src/chainstate/burn/db/sortdb.rs +++ b/src/chainstate/burn/db/sortdb.rs @@ -241,6 +241,7 @@ impl FromRow for LeaderBlockCommitOp { let key_vtxindex: u16 = row.get_unwrap("key_vtxindex"); let memo_hex: String = row.get_unwrap("memo"); let burn_fee_str: String = row.get_unwrap("burn_fee"); + let destroyed_str: String = row.get_unwrap("destroyed"); let input_json: String = row.get_unwrap("input"); let apparent_sender_json: String = row.get_unwrap("apparent_sender"); @@ -261,6 +262,10 @@ impl FromRow for LeaderBlockCommitOp { .parse::() .expect("DB Corruption: burn is not parseable as u64"); + let destroyed = destroyed_str + .parse::() + .expect("DB Corruption: destroyed is not parseable as u64"); + let burn_parent_modulus: u8 = row.get_unwrap("burn_parent_modulus"); let block_commit = LeaderBlockCommitOp { @@ -274,6 +279,7 @@ impl FromRow for LeaderBlockCommitOp { burn_parent_modulus, burn_fee, + destroyed, input, apparent_sender, commit_outs, @@ -428,7 +434,7 @@ impl FromRow for StacksEpoch { } } -pub const SORTITION_DB_VERSION: &'static str = "3"; +pub const SORTITION_DB_VERSION: &'static str = "4"; const SORTITION_DB_INITIAL_SCHEMA: &'static [&'static str] = &[ r#" @@ -510,7 +516,7 @@ const SORTITION_DB_INITIAL_SCHEMA: &'static [&'static str] = &[ memo TEXT, commit_outs TEXT, burn_fee TEXT NOT NULL, -- use text to encode really big numbers - sunset_burn TEXT NOT NULL, -- use text to encode really big numbers (OBSOLETE; IGNORED) + sunset_burn TEXT NOT NULL, -- use text to encode really big numbers (OBSOLETE; RENAMED TO destroyed) input TEXT NOT NULL, apparent_sender TEXT NOT NULL, burn_parent_modulus INTEGER NOT NULL, @@ -606,6 +612,10 @@ const SORTITION_DB_SCHEMA_3: &'static [&'static str] = &[r#" FOREIGN KEY(block_commit_txid,block_commit_sortition_id) REFERENCES block_commits(txid,sortition_id) );"#]; +const SORTITION_DB_SCHEMA_4: &'static [&'static str] = &[r#" + ALTER TABLE block_commits RENAME COLUMN sunset_burn TO destroyed; + "#]; + // update this to add new indexes const LAST_SORTITION_DB_INDEX: &'static str = "index_parent_sortition_id"; @@ -729,6 +739,75 @@ fn get_adjusted_block_height(context: &C, block_height: u64 Some(block_height - first_block_height) } +/// Get the PoX spend cutoff for the current cycle, given the PoX cutoff MARF value and a handle to +/// the sortition DB and the associated context (i.e. from either a SortitionHandleConn or +/// SortitionHandleTx). +/// +/// The PoX cutoff takes effect in the first full reward cycle *after* Stacks 2.1 goes live, +/// so if we ask for the cutoff in the reward cycle just prior to it, then the the cutoff is +/// u64::MAX. It is not defined in reward cycles earlier than this. +fn inner_get_pox_cutoff( + conn: &DBConn, + context: &SortitionHandleContext, + pox_cutoff_opt: Option, +) -> Result, db_error> { + let chain_tip = context.chain_tip.clone(); // grr borrow checker + match pox_cutoff_opt { + None => { + // possibly in the penultimate Stacks 2.05 reward cycle + let tip = SortitionDB::get_block_snapshot(conn, &chain_tip)? + .ok_or(db_error::NotFoundError)?; + + let rc = Burnchain::static_block_height_to_reward_cycle( + tip.block_height, + context.first_block_height, + context.pox_constants.reward_cycle_length.into(), + ) + .expect("FATAL: tip block height is mined before the first block height"); + let rc_start = Burnchain::static_reward_cycle_to_block_height( + rc, + context.first_block_height, + context.pox_constants.reward_cycle_length.into(), + ); + + let epochs = SortitionDB::get_stacks_epochs(conn)?; + let cur_epoch_index = StacksEpoch::find_epoch(&epochs, tip.block_height).expect( + &format!("FATAL: no epoch found for height {}", tip.block_height), + ); + let rc_start_epoch_index = StacksEpoch::find_epoch(&epochs, rc_start).expect(&format!( + "Fatal: no epoch found for rc start height {}", + rc_start + )); + + if epochs[cur_epoch_index].epoch_id < StacksEpochId::Epoch21 { + // we're before epoch 2.1, so no cutoff defined. + Ok(None) + } else if epochs[cur_epoch_index].epoch_id > StacksEpochId::Epoch21 + || (StacksEpochId::Epoch21 == epochs[cur_epoch_index].epoch_id + && epochs[rc_start_epoch_index].epoch_id != epochs[cur_epoch_index].epoch_id) + { + // we're currently in or after 2.1, but the reward cycle did not start in 2.1. So, the + // cutoff is u64::MAX -- miners can spend as much as they want on PoX outputs + // until we've gone through a prepare phase in 2.1. + Ok(Some(u64::MAX)) + } else { + // we're in a 2.1 reward cycle that started in 2.1. If there's no PoX cutoff + // defined, then PoX must be disabled and thus there's no cutoff to report. + Ok(None) + } + } + Some(x) => { + // we have a PoX cutoff. If it's 0, then we're PoB and must report no cutoff. + let cutoff = db_keys::pox_cutoff_from_string(&x); + if cutoff > 0 { + Ok(Some(cutoff)) + } else { + Ok(None) + } + } + } +} + struct db_keys; impl db_keys { /// store an entry that maps from a PoX anchor's to @@ -808,6 +887,25 @@ impl db_keys { byte_buff.copy_from_slice(&bytes[0..2]); u16::from_le_bytes(byte_buff) } + + pub fn pox_cutoff() -> &'static str { + "sortition_db::pox_cutoff" + } + + pub fn pox_cutoff_to_string(cutoff: u64) -> String { + to_hex( + &u64::try_from(cutoff) + .expect("BUG: maximum cutoff size should be u64") + .to_le_bytes(), + ) + } + + pub fn pox_cutoff_from_string(cutoff: &str) -> u64 { + let bytes = hex_bytes(cutoff).expect("CORRUPTION: bad format written for PoX cutoff"); + let mut byte_buff = [0; 8]; + byte_buff.copy_from_slice(&bytes[0..8]); + u64::from_le_bytes(byte_buff) + } } impl<'a> SortitionHandleTx<'a> { @@ -842,7 +940,7 @@ impl<'a> SortitionHandleTx<'a> { chain_tip: &SortitionId, ) -> Result, db_error> { let sortition_identifier_key = db_keys::sortition_id_for_bhh(burn_header_hash); - let sortition_id = match self.get_indexed(&chain_tip, &sortition_identifier_key)? { + let sortition_id = match self.get_indexed(chain_tip, &sortition_identifier_key)? { None => return Ok(None), Some(x) => SortitionId::from_hex(&x).expect("FATAL: bad Sortition ID stored in DB"), }; @@ -850,6 +948,16 @@ impl<'a> SortitionHandleTx<'a> { SortitionDB::get_block_snapshot(self.tx(), &sortition_id) } + /// Get the PoX spend cutoff for the current cycle. + /// The PoX cutoff takes effect in the first full reward cycle *after* Stacks 2.1 goes live, + /// so if we ask for the cutoff in the reward cycle just prior to it, then the the cutoff is + /// u64::MAX. It is not defined in reward cycles earlier than this. + pub fn get_pox_cutoff(&mut self) -> Result, db_error> { + let context = self.context.clone(); // grr borrow checker + let pox_cutoff_opt = self.get_indexed(&context.chain_tip, db_keys::pox_cutoff())?; + inner_get_pox_cutoff(self, &context, pox_cutoff_opt) + } + /// Get a leader key at a specific location in the burn chain's fork history, given the /// matching block commit's fork index root (block_height and vtxindex are the leader's /// calculated location in this fork). @@ -1793,9 +1901,9 @@ impl<'a> SortitionHandleConn<'a> { get_block_commit_by_txid(self.conn(), txid) } - /// Return a vec of sortition winner's burn header hash and stacks header hash, ordered by - /// increasing block height in the range (block_height_begin, block_height_end] - fn get_sortition_winners_in_fork( + /// Return a vec of sortition winner's txids and block heights, + /// in order over the interval (block_height_begin, block_height_end] + pub fn get_sortition_winners_in_fork( &self, block_height_begin: u32, block_height_end: u32, @@ -1816,8 +1924,32 @@ impl<'a> SortitionHandleConn<'a> { Ok(result) } + /// Calculate the PoX payout cutoff. Any PoX payouts higher than this must burn the difference. + /// The cutoff is a function of the burnchain burns for the winning block-commits in the + /// prepare phase. + /// + /// The currently-used function is 4 * min(median(burns), mean(burns)) + pub fn calculate_pox_cutoff(mut burns: Vec) -> u64 { + if burns.len() < 2 { + // PoX definitely not engaged + return 0; + } + + burns.sort(); + + let mean_burn = burns.iter().fold(0, |acc, x| acc + x) / (burns.len() as u64); + let median_burn = if burns.len() % 2 == 0 { + (burns[burns.len() / 2 - 1] + burns[burns.len() / 2]) / 2 + } else { + burns[burns.len() - 1] / 2 + }; + + 4 * cmp::min(median_burn, mean_burn) + } + /// Return identifying information for a PoX anchor block for the reward cycle that - /// begins the block after `prepare_end_bhh`. + /// begins the block after `prepare_end_bhh`, as well as the PoX payout cutoff for this + /// cycle. /// 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 @@ -1826,20 +1958,42 @@ impl<'a> SortitionHandleConn<'a> { &self, prepare_end_bhh: &BurnchainHeaderHash, pox_consts: &PoxConstants, - ) -> Result, CoordinatorError> { + ) -> Result, CoordinatorError> { match self.get_chosen_pox_anchor_check_position(prepare_end_bhh, pox_consts, true) { - Ok(Ok((c_hash, bh_hash, _))) => Ok(Some((c_hash, bh_hash))), + Ok(Ok((c_hash, bh_hash, _, burns))) => Ok(Some(( + c_hash, + bh_hash, + SortitionHandleConn::calculate_pox_cutoff(burns), + ))), Ok(Err(_)) => Ok(None), Err(e) => Err(e), } } + /// Return identifying information for a PoX anchor block that would have been selected + /// `pox_consts.reward_cycle_length` sortitions ago, as of `prepare_end_bhh`. If + /// `check_position` is `true` and `prepare_end_bhh` does _not_ fall on the reward cycle + /// boundary, then this method returns `Err(CoordinatorError::NotPrepareEndBlock)`. + /// + /// Returns Ok(Ok(ch, bh, confs, burns)) if an anchor block was chosen -- i.e. confs >= F*w + /// * ch, bhh identify the Stacks block that is the anchor block + /// * confs is the number of confirmations + /// * burns is the list of burns spent by the anchor block's descendants (only winning + /// commits' burns are considered). Note that the *total burn* -- PoX cutoff burn + + /// send-to-burn-address burn -- are considered, *even before* Stacks 2.1. This is fine, + /// because this information is derived to calculate the next reward cycle's PoX cutoff, + /// and this isn't used by the sortition logic or block-commit logic until the proper time + /// anyway. + /// + /// Returns Ok(Err(confs)) if an anchor block was not chosen -- i.e. confs < F*w + /// Returns Err(..) if we could not calculate this result for some reason. pub fn get_chosen_pox_anchor_check_position( &self, prepare_end_bhh: &BurnchainHeaderHash, pox_consts: &PoxConstants, check_position: bool, - ) -> Result, CoordinatorError> { + ) -> Result), u32>, CoordinatorError> + { let prepare_end_sortid = self.get_sortition_id_for_bhh(prepare_end_bhh)? .ok_or_else(|| { @@ -1873,20 +2027,29 @@ impl<'a> SortitionHandleConn<'a> { let prepare_end = block_height; let prepare_begin = prepare_end.saturating_sub(pox_consts.prepare_length); - let mut candidate_anchors = HashMap::new(); - let mut memoized_candidates: HashMap<_, (Txid, u64)> = HashMap::new(); + let mut candidate_anchors: HashMap<_, (u32, Vec)> = HashMap::new(); + let mut memoized_candidates: HashMap<_, (Txid, u64, Vec)> = HashMap::new(); // iterate over every sortition winner in the prepare phase // looking for their highest ancestor _before_ prepare_begin. let winners = self.get_sortition_winners_in_fork(prepare_begin, prepare_end)?; for (winner_commit_txid, winner_block_height) in winners.into_iter() { - let mut cursor = (winner_commit_txid, winner_block_height); + let block_commit = self + .get_block_commit_by_txid(&winner_commit_txid)? + .expect("CORRUPTED: Failed to fetch block commit for known sortition winner"); + let mut cursor = ( + winner_commit_txid, + winner_block_height, + vec![block_commit.total_spend()], + ); let mut found_ancestor = true; while cursor.1 > (prepare_begin as u64) { // check if we've already discovered the candidate for this block if let Some(ancestor) = memoized_candidates.get(&cursor.1) { - cursor = ancestor.clone(); + let mut combined_burns = ancestor.2.clone(); + combined_burns.append(&mut cursor.2); + cursor = (ancestor.0, ancestor.1, combined_burns); } else { // get the block commit let block_commit = self.get_block_commit_by_txid(&cursor.0)?.expect( @@ -1912,7 +2075,11 @@ impl<'a> SortitionHandleConn<'a> { ); assert!(sn.sortition, "CORRUPTED: accepted block commit, but parent pointer not a sortition winner"); - cursor = (sn.winning_block_txid, sn.block_height); + cursor = ( + sn.winning_block_txid, + sn.block_height, + vec![block_commit.total_spend()], + ); } } if !found_ancestor { @@ -1922,20 +2089,25 @@ impl<'a> SortitionHandleConn<'a> { // highest ancestor of winner_stacks_bh whose sortition occurred before prepare_begin // the winner of that sortition is the PoX anchor block candidate that winner_stacks_bh is "voting for" let highest_ancestor = cursor.1; + let mut total_spends = cursor.2.clone(); memoized_candidates.insert(winner_block_height, cursor); - if let Some(x) = candidate_anchors.get_mut(&highest_ancestor) { - *x += 1; + if let Some((confs, burnt)) = candidate_anchors.get_mut(&highest_ancestor) { + *confs += 1; + burnt.clear(); + burnt.append(&mut total_spends); } else { - candidate_anchors.insert(highest_ancestor, 1u32); + candidate_anchors.insert(highest_ancestor, (1u32, total_spends)); } } // did any candidate receive >= F*w? let mut result = None; let mut max_confirmed_by = 0; - for (candidate, confirmed_by) in candidate_anchors.into_iter() { + let mut max_burnt = vec![]; + for (candidate, (confirmed_by, all_burns)) in candidate_anchors.into_iter() { if confirmed_by > max_confirmed_by { max_confirmed_by = confirmed_by; + max_burnt = all_burns; } if confirmed_by >= pox_consts.anchor_threshold { // find the sortition at height @@ -1947,7 +2119,8 @@ impl<'a> SortitionHandleConn<'a> { .replace(( sn.consensus_hash, sn.winning_stacks_block_hash, - confirmed_by + confirmed_by, + max_burnt.clone() )) .is_none(), "BUG: multiple anchor blocks received more confirmations than anchor_threshold" @@ -1965,11 +2138,20 @@ impl<'a> SortitionHandleConn<'a> { Ok(Err(max_confirmed_by)) } Some(response) => { - info!("Reward cycle #{} ({}): {:?} reached (F*w), expecting consensus over proof of transfer", reward_cycle_id, block_height, result); + info!("Reward cycle #{} ({}): {:?} reached (F*w), expecting consensus over proof of transfer", reward_cycle_id, block_height, &response); Ok(Ok(response)) } } } + + /// Get the PoX spend cutoff for the current cycle. + /// The PoX cutoff takes effect in the first full reward cycle *after* Stacks 2.1 goes live, + /// so if we ask for the cutoff in the reward cycle just prior to it, then the the cutoff is + /// u64::MAX. It is not defined in reward cycles earlier than this. + pub fn get_pox_cutoff(&self) -> Result, db_error> { + let pox_cutoff_opt = self.get_indexed(&self.context.chain_tip, db_keys::pox_cutoff())?; + inner_get_pox_cutoff(self, &self.context, pox_cutoff_opt) + } } // Connection methods @@ -2326,6 +2508,9 @@ impl SortitionDB { for row_text in SORTITION_DB_SCHEMA_3 { db_tx.execute_batch(row_text)?; } + for row_text in SORTITION_DB_SCHEMA_4 { + db_tx.execute_batch(row_text)?; + } SortitionDB::validate_and_insert_epochs(&db_tx, epochs_ref)?; @@ -2537,6 +2722,17 @@ impl SortitionDB { Ok(()) } + fn apply_schema_4(tx: &DBTx) -> Result<(), db_error> { + for sql_exec in SORTITION_DB_SCHEMA_4 { + tx.execute_batch(sql_exec)?; + } + tx.execute( + "INSERT OR REPLACE INTO db_config (version) VALUES (?1)", + &["4"], + )?; + Ok(()) + } + fn check_schema_version_or_error(&mut self) -> Result<(), db_error> { match SortitionDB::get_schema_version(self.conn()) { Ok(Some(version)) => { @@ -2571,6 +2767,10 @@ impl SortitionDB { let tx = self.tx_begin()?; SortitionDB::apply_schema_3(&tx.deref())?; tx.commit()?; + } else if version == "3" { + let tx = self.tx_begin()?; + SortitionDB::apply_schema_4(&tx.deref())?; + tx.commit()?; } else if version == expected_version { return Ok(()); } else { @@ -3346,6 +3546,9 @@ impl SortitionDB { conn: &Connection, block_snapshot: &BlockSnapshot, ) -> Result { + let cur_epoch = SortitionDB::get_stacks_epoch(conn, block_snapshot.block_height)? + .expect("FATAL: no epoch defined for snapshot"); + let user_burns = SortitionDB::get_user_burns_by_block(conn, &block_snapshot.sortition_id)?; let block_commits = SortitionDB::get_block_commits_by_block(conn, &block_snapshot.sortition_id)?; @@ -3358,7 +3561,7 @@ impl SortitionDB { } for i in 0..block_commits.len() { burn_total = burn_total - .checked_add(block_commits[i].burn_fee) + .checked_add(block_commits[i].sortition_spend(cur_epoch.epoch_id)) .expect("Way too many tokens burned"); } Ok(burn_total) @@ -3962,6 +4165,25 @@ impl<'a> SortitionHandleTx<'a> { assert!(parent_sortition_id != SortitionId([0x00; 32])); } } + if parent_sortition_id == SortitionId([0x00; 32]) + && (block_commit.parent_block_ptr != 0 || block_commit.parent_vtxindex != 0) + { + warn!( + "insert_block_commit: No block snapshot at height {} vtxindex {}", + block_commit.parent_block_ptr, block_commit.parent_vtxindex + ); + } + test_debug!( + "Parent sortition of block-commit {},{},{} is {} ({},{})", + &block_commit.txid, + block_commit.block_height, + block_commit.vtxindex, + &parent_sortition_id, + block_commit.parent_block_ptr, + block_commit.parent_vtxindex + ); + + let (burn_fee_str, destroyed_str) = block_commit.encode_spends_for_db(); let args: &[&dyn ToSql] = &[ &block_commit.txid, @@ -3975,16 +4197,16 @@ impl<'a> SortitionHandleTx<'a> { &block_commit.key_block_ptr, &block_commit.key_vtxindex, &to_hex(&block_commit.memo[..]), - &block_commit.burn_fee.to_string(), + &burn_fee_str, &tx_input_str, sort_id, &serde_json::to_value(&block_commit.commit_outs).unwrap(), - &0i64, + &destroyed_str, &apparent_sender_str, &block_commit.burn_parent_modulus, ]; - self.execute("INSERT INTO block_commits (txid, vtxindex, block_height, burn_header_hash, block_header_hash, new_seed, parent_block_ptr, parent_vtxindex, key_block_ptr, key_vtxindex, memo, burn_fee, input, sortition_id, commit_outs, sunset_burn, apparent_sender, burn_parent_modulus) \ + self.execute("INSERT INTO block_commits (txid, vtxindex, block_height, burn_header_hash, block_header_hash, new_seed, parent_block_ptr, parent_vtxindex, key_block_ptr, key_vtxindex, memo, burn_fee, input, sortition_id, commit_outs, destroyed, apparent_sender, burn_parent_modulus) \ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)", args)?; let parent_args: &[&dyn ToSql] = &[sort_id, &block_commit.txid, &parent_sortition_id]; @@ -4209,6 +4431,7 @@ impl<'a> SortitionHandleTx<'a> { } // if we have selected an anchor block, write that info if let Some(ref anchor_block) = reward_info.selected_anchor_block() { + test_debug!("Anchor block is {}", anchor_block); keys.push(db_keys::pox_anchor_to_prepare_end(anchor_block)); values.push(parent_snapshot.sortition_id.to_hex()); @@ -4220,7 +4443,9 @@ impl<'a> SortitionHandleTx<'a> { } // if we've selected an anchor _and_ know of the anchor, // write the reward set information - if let Some(mut reward_set) = reward_info.known_selected_anchor_block_owned() { + if let Some((mut reward_set, pox_cutoff)) = + reward_info.known_selected_anchor_block_owned() + { if reward_set.len() > 0 { // if we have a reward set, then we must also have produced a recipient // info for this block @@ -4244,9 +4469,15 @@ impl<'a> SortitionHandleTx<'a> { keys.push(db_keys::pox_reward_set_entry(ix as u16)); values.push(address.to_string()); } + + keys.push(db_keys::pox_cutoff().to_string()); + values.push(db_keys::pox_cutoff_to_string(pox_cutoff)); } else { keys.push(db_keys::pox_reward_set_size().to_string()); values.push(db_keys::reward_set_size_to_string(0)); + + keys.push(db_keys::pox_cutoff().to_string()); + values.push(db_keys::pox_cutoff_to_string(0)); } // in all cases, write the new PoX bit vector @@ -4535,12 +4766,14 @@ pub mod tests { tx.commit().unwrap(); } - pub fn test_append_snapshot_with_winner( + pub fn test_append_snapshot_with_winner_and_pox_info( db: &mut SortitionDB, next_hash: BurnchainHeaderHash, block_ops: &Vec, parent_sn: Option, winning_block_commit: Option, + reward_cycle_info: Option, + reward_set_info: Option, ) -> BlockSnapshot { let mut sn = match parent_sn { Some(sn) => sn, @@ -4554,18 +4787,31 @@ pub mod tests { sn.parent_sortition_id = sn.sortition_id.clone(); sn.burn_header_hash = next_hash; sn.block_height += 1; - sn.num_sortitions += 1; sn.sortition_id = SortitionId::stubbed(&sn.burn_header_hash); sn.consensus_hash = ConsensusHash(Hash160::from_data(&sn.consensus_hash.0).0); + sn.sortition_hash = SortitionHash(Sha512Trunc256Sum::from_data(&sn.sortition_hash.0).0); if let Some(cmt) = winning_block_commit { sn.sortition = true; sn.winning_stacks_block_hash = cmt.block_header_hash; sn.winning_block_txid = cmt.txid; + sn.num_sortitions += 1; + } else { + sn.sortition = false; + sn.winning_stacks_block_hash = BlockHeaderHash([0x00; 32]); + sn.winning_block_txid = Txid([0x00; 32]); } let index_root = tx - .append_chain_tip_snapshot(&sn_parent, &sn, block_ops, &vec![], None, None, None) + .append_chain_tip_snapshot( + &sn_parent, + &sn, + block_ops, + &vec![], + reward_cycle_info, + reward_set_info.as_ref(), + None, + ) .unwrap(); sn.index_root = index_root; @@ -4574,6 +4820,24 @@ pub mod tests { sn } + pub fn test_append_snapshot_with_winner( + db: &mut SortitionDB, + next_hash: BurnchainHeaderHash, + block_ops: &Vec, + parent_sn: Option, + winning_block_commit: Option, + ) -> BlockSnapshot { + test_append_snapshot_with_winner_and_pox_info( + db, + next_hash, + block_ops, + parent_sn, + winning_block_commit, + None, + None, + ) + } + pub fn test_append_snapshot( db: &mut SortitionDB, next_hash: BurnchainHeaderHash, @@ -4725,6 +4989,7 @@ pub mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -4870,7 +5135,6 @@ pub mod tests { sn.parent_sortition_id = sn_parent.sortition_id.clone(); sn.burn_header_hash = next_hash; sn.block_height += 1; - sn.num_sortitions += 1; sn.consensus_hash = ConsensusHash([0x23; 20]); let index_root = tx @@ -5546,6 +5810,7 @@ pub mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -5617,7 +5882,7 @@ pub mod tests { { let burn_amt = SortitionDB::get_block_burn_amount(db.conn(), &commit_snapshot).unwrap(); - assert_eq!(burn_amt, block_commit.burn_fee + user_burn.burn_fee); + assert_eq!(burn_amt, block_commit.total_spend() + user_burn.burn_fee); let no_burn_amt = SortitionDB::get_block_burn_amount(db.conn(), &key_snapshot).unwrap(); assert_eq!(no_burn_amt, 0); @@ -7674,6 +7939,7 @@ pub mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -7715,6 +7981,7 @@ pub mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -7756,6 +8023,7 @@ pub mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -7797,6 +8065,7 @@ pub mod tests { commit_outs: vec![], burn_fee: 1, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -8005,4 +8274,372 @@ pub mod tests { } } } + + fn test_make_block_commit_from_snapshot( + db: &SortitionDB, + parent_snapshot: &BlockSnapshot, + vtxindex: u32, + burn_fee: u64, + key: Option, + ) -> LeaderBlockCommitOp { + if parent_snapshot.sortition { + let parent_block_commit = + get_block_commit_by_txid(db.conn(), &parent_snapshot.winning_block_txid) + .unwrap() + .unwrap(); + let txid = { + let mut bytes = [0u8; 44]; + bytes[0..32].copy_from_slice(&parent_block_commit.txid.0); + bytes[32..40].copy_from_slice(&burn_fee.to_be_bytes()); + bytes[40..44].copy_from_slice(&vtxindex.to_be_bytes()); + Txid(Sha512Trunc256Sum::from_data(&bytes).0) + }; + + let block_commit = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash( + Sha512Trunc256Sum::from_data(&parent_block_commit.block_header_hash.0).0, + ), + new_seed: VRFSeed(Sha512Trunc256Sum::from_data(&parent_block_commit.new_seed.0).0), + parent_block_ptr: parent_block_commit.block_height as u32, + parent_vtxindex: parent_block_commit.vtxindex as u16, + key_block_ptr: parent_block_commit.key_block_ptr, + key_vtxindex: parent_block_commit.key_vtxindex, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: burn_fee, + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: txid, + vtxindex: vtxindex, + + // ignored? + burn_parent_modulus: ((parent_snapshot.block_height + 1) + % BURN_BLOCK_MINED_AT_MODULUS) as u8, + block_height: parent_snapshot.block_height + 1, + burn_header_hash: BurnchainHeaderHash([0x00; 32]), + }; + block_commit + } else { + let key = key.unwrap(); + let txid = { + let mut bytes = [0u8; 44]; + bytes[0..32].copy_from_slice(&key.txid.0); + bytes[32..40].copy_from_slice(&burn_fee.to_be_bytes()); + bytes[40..44].copy_from_slice(&vtxindex.to_be_bytes()); + Txid(Sha512Trunc256Sum::from_data(&bytes).0) + }; + + // make a genesis commit + let block_commit = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash( + Sha512Trunc256Sum::from_data(&parent_snapshot.burn_header_hash.0).0, + ), + new_seed: VRFSeed( + Sha512Trunc256Sum::from_data(&parent_snapshot.burn_header_hash.0).0, + ), + parent_block_ptr: 0, + parent_vtxindex: 0, + key_block_ptr: key.block_height as u32, + key_vtxindex: key.vtxindex as u16, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: burn_fee, + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: txid, + vtxindex: vtxindex, + + // ignored? + burn_parent_modulus: ((parent_snapshot.block_height + 1) + % BURN_BLOCK_MINED_AT_MODULUS) as u8, + block_height: parent_snapshot.block_height + 1, + burn_header_hash: BurnchainHeaderHash([0x00; 32]), + }; + block_commit + } + } + + #[test] + fn test_get_chosen_pox_anchor_check_position() { + let first_block_height = 100; + let first_burn_header_hash = BurnchainHeaderHash([0x01; 32]); + let mut db = SortitionDB::connect_test_with_epochs( + first_block_height, + &first_burn_header_hash, + StacksEpoch::unit_test_2_1(0), + ) + .unwrap(); + + let pox_consts = PoxConstants::new(20, 10, 6, 1, 1, 0); + + let leader_key = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash([0x22; 20]), + public_key: VRFPublicKey::from_bytes( + &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") + .unwrap(), + ) + .unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: StacksAddress::from_bitcoin_address( + &BitcoinAddress::from_scriptpubkey( + BitcoinNetworkType::Testnet, + &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap(), + ) + .unwrap(), + ), + txid: Txid::from_bytes_be( + &hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562") + .unwrap(), + ) + .unwrap(), + vtxindex: 2, + block_height: first_block_height + 1, + burn_header_hash: BurnchainHeaderHash([0x02; 32]), + }; + + let mut sn = test_append_snapshot_with_winner( + &mut db, + BurnchainHeaderHash([0x02; 32]), + &vec![BlockstackOperationType::LeaderKeyRegister( + leader_key.clone(), + )], + None, + None, + ); + + // make the first commit + let cmt = test_make_block_commit_from_snapshot(&db, &sn, 1, 1, Some(leader_key)); + sn = test_append_snapshot_with_winner( + &mut db, + BurnchainHeaderHash([0x03; 32]), + &vec![BlockstackOperationType::LeaderBlockCommit(cmt.clone())], + Some(sn), + Some(cmt), + ); + + // confirm an anchor block with two miners + let mut expected_anchor_block_commit = None; + for i in 4..22 { + let cmt1 = test_make_block_commit_from_snapshot(&db, &sn, 1, 10 * i + 1, None); + let cmt2 = test_make_block_commit_from_snapshot(&db, &sn, 2, 100 * i + 1, None); + + let winner = if i % 2 == 0 { + cmt1.clone() + } else { + cmt2.clone() + }; + if i == 11 { + expected_anchor_block_commit = Some(winner.clone()); + } + + sn = test_append_snapshot_with_winner( + &mut db, + BurnchainHeaderHash([i as u8; 32]), + &vec![ + BlockstackOperationType::LeaderBlockCommit(cmt1.clone()), + BlockstackOperationType::LeaderBlockCommit(cmt2.clone()), + ], + Some(sn), + Some(winner), + ); + } + + let expected_anchor_block_commit = expected_anchor_block_commit.unwrap(); + + let tip = SortitionDB::get_canonical_burn_chain_tip(db.conn()).unwrap(); + let index_handle = db.index_handle(&tip.sortition_id); + let (ch, bhh, confs, burns) = index_handle + .get_chosen_pox_anchor_check_position(&tip.burn_header_hash, &pox_consts, true) + .unwrap() + .unwrap(); + + assert_eq!(bhh, expected_anchor_block_commit.block_header_hash); + assert_eq!(confs, 10); + assert_eq!( + burns, + vec![121, 1301, 141, 1501, 161, 1701, 181, 1901, 201, 2101] + ); + } + + #[test] + fn test_get_pox_cutoff() { + let first_block_height = 100; + let first_burn_header_hash = BurnchainHeaderHash([0x01; 32]); + let mut db = SortitionDB::connect_test_with_epochs( + first_block_height, + &first_burn_header_hash, + StacksEpoch::all(0, 1, 115), // at block height 115 (the middle of a reward cycle), epoch 2.1 activates + ) + .unwrap(); + + let pox_consts = PoxConstants::new(20, 10, 6, 1, 1, 0); + + let leader_key = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash([0x22; 20]), + public_key: VRFPublicKey::from_bytes( + &hex_bytes("a366b51292bef4edd64063d9145c617fec373bceb0758e98cd72becd84d54c7a") + .unwrap(), + ) + .unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: StacksAddress::from_bitcoin_address( + &BitcoinAddress::from_scriptpubkey( + BitcoinNetworkType::Testnet, + &hex_bytes("76a9140be3e286a15ea85882761618e366586b5574100d88ac").unwrap(), + ) + .unwrap(), + ), + txid: Txid::from_bytes_be( + &hex_bytes("1bfa831b5fc56c858198acb8e77e5863c1e9d8ac26d49ddb914e24d8d4083562") + .unwrap(), + ) + .unwrap(), + vtxindex: 2, + block_height: first_block_height + 1, + burn_header_hash: BurnchainHeaderHash([0x02; 32]), + }; + + let mut sn = test_append_snapshot_with_winner( + &mut db, + BurnchainHeaderHash([0x02; 32]), + &vec![BlockstackOperationType::LeaderKeyRegister( + leader_key.clone(), + )], + None, + None, + ); + + // make the first commit + let cmt = test_make_block_commit_from_snapshot(&db, &sn, 1, 1, Some(leader_key)); + sn = test_append_snapshot_with_winner( + &mut db, + BurnchainHeaderHash([0x03; 32]), + &vec![BlockstackOperationType::LeaderBlockCommit(cmt.clone())], + Some(sn), + Some(cmt), + ); + + // build first reward cycle, and confirm an anchor block. + // The anchor block is mined at i = 10 + for i in 4..22 { + let cmt = test_make_block_commit_from_snapshot(&db, &sn, 1, 10 * i + 1, None); + sn = test_append_snapshot_with_winner( + &mut db, + BurnchainHeaderHash([i as u8; 32]), + &vec![BlockstackOperationType::LeaderBlockCommit(cmt.clone())], + Some(sn), + Some(cmt.clone()), + ); + + let epoch = SortitionDB::get_stacks_epoch(db.conn(), sn.block_height) + .unwrap() + .unwrap(); + + if sn.block_height < 115 { + // we don't yet have a pox cutoff, since we're in epoch 2.05 + assert_eq!(epoch.epoch_id, StacksEpochId::Epoch2_05); + + let mut sort_tx = db.tx_begin_at_tip(); + assert_eq!(sort_tx.get_pox_cutoff().unwrap(), None); + } else if sn.block_height < 120 { + // 2.1 activated in the middle of the reward cycle, so the pox cutoff defaults to + // u64::MAX + assert_eq!(epoch.epoch_id, StacksEpochId::Epoch21); + let mut sort_tx = db.tx_begin_at_tip(); + assert_eq!(sort_tx.get_pox_cutoff().unwrap(), Some(u64::MAX)); + } + } + + // build second reward cycle with the anchor block and a mocked reward set + let (anchor_ch, anchor_bhh) = { + let tip = SortitionDB::get_canonical_burn_chain_tip(db.conn()).unwrap(); + let index_handle = db.index_handle(&tip.sortition_id); + let (ch, bhh, _confs, burns) = index_handle + .get_chosen_pox_anchor_check_position(&tip.burn_header_hash, &pox_consts, true) + .unwrap() + .unwrap(); + + // NOTE: anchor block is at i = 10 + assert_eq!( + burns, + vec![121, 131, 141, 151, 161, 171, 181, 191, 201, 211] + ); + + let median_burn = (burns[4] + burns[5]) / 2; + let avg_burn = burns.iter().fold(0, |acc, x| acc + x) / 10; + + assert_eq!(median_burn, 166); + assert_eq!(avg_burn, 166); + + assert_eq!( + SortitionHandleConn::calculate_pox_cutoff(burns), + 4 * cmp::min(median_burn, avg_burn) + ); + + (ch, bhh) + }; + + let mock_reward_addresses = + vec![ + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(); + ((pox_consts.reward_cycle_length - pox_consts.prepare_length) * 2) as usize + ]; + let cmt = test_make_block_commit_from_snapshot(&db, &sn, 1, 10 * 23 + 1, None); + + sn = test_append_snapshot_with_winner_and_pox_info( + &mut db, + BurnchainHeaderHash([23u8; 32]), + &vec![BlockstackOperationType::LeaderBlockCommit(cmt.clone())], + Some(sn), + Some(cmt.clone()), + Some(RewardCycleInfo { + anchor_status: PoxAnchorBlockStatus::SelectedAndKnown( + anchor_bhh.clone(), + mock_reward_addresses, + ), + pox_cutoff: 10_000, // NOTE: this is different than calculate_pox_cutoff(); we're only testing that this value gets stored and is loadable + }), + Some(RewardSetInfo { + anchor_block: anchor_bhh.clone(), + recipients: vec![ + ( + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6") + .unwrap(), + 0, + ), + ( + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6") + .unwrap(), + 1, + ), + ], + }), + ); + + // this loads now + { + let mut sort_tx = db.tx_begin_at_tip(); + assert_eq!(sort_tx.get_pox_cutoff().unwrap(), Some(10_000)); + } + } } diff --git a/src/chainstate/burn/distribution.rs b/src/chainstate/burn/distribution.rs index 2ea8f91a3a..1613193d27 100644 --- a/src/chainstate/burn/distribution.rs +++ b/src/chainstate/burn/distribution.rs @@ -28,6 +28,7 @@ use crate::chainstate::burn::operations::{ LeaderKeyRegisterOp, UserBurnSupportOp, }; use crate::chainstate::stacks::StacksPublicKey; +use crate::core::StacksEpochId; use crate::core::MINING_COMMITMENT_WINDOW; use crate::monitoring; use stacks_common::address::AddressHashMode; @@ -82,10 +83,10 @@ impl LinkedCommitIdentifier { } } - fn burn_fee(&self) -> u64 { + fn burn_fee(&self, epoch_id: StacksEpochId) -> u64 { match self { LinkedCommitIdentifier::Missed(_) => 1, - LinkedCommitIdentifier::Valid(ref op) => op.burn_fee, + LinkedCommitIdentifier::Valid(ref op) => op.sortition_spend(epoch_id), } } @@ -155,6 +156,7 @@ impl BurnSamplePoint { /// `OP_RETURN` payload. The length of this vector must be equal to the length of the /// `block_commits` vector. `burn_blocks[i]` is `true` if the `ith` block-commit must be PoB. pub fn make_min_median_distribution( + epoch_id: StacksEpochId, mut block_commits: Vec>, mut missed_commits: Vec>, burn_blocks: Vec, @@ -252,7 +254,7 @@ impl BurnSamplePoint { .iter() .map(|commit| { if let Some(commit) = commit { - commit.op.burn_fee() as u128 + commit.op.burn_fee(epoch_id) as u128 } else { // use 1 as the linked commit min. this gives a miner a _small_ // chance of winning a block even if they haven't performed chained utxos yet @@ -280,7 +282,10 @@ impl BurnSamplePoint { } else { unreachable!("BUG: first linked commit should always be valid"); }; - assert_eq!(candidate.burn_fee as u128, most_recent_burn); + assert_eq!( + candidate.sortition_spend(epoch_id) as u128, + most_recent_burn + ); debug!("Burn sample"; "txid" => %candidate.txid.to_string(), @@ -331,7 +336,12 @@ impl BurnSamplePoint { _consumed_leader_keys: Vec, user_burns: Vec, ) -> Vec { - Self::make_min_median_distribution(vec![all_block_candidates], vec![], vec![true]) + Self::make_min_median_distribution( + StacksEpochId::Epoch21, + vec![all_block_candidates], + vec![], + vec![true], + ) } /// Calculate the ranges between 0 and 2**256 - 1 over which each point in the burn sample @@ -416,6 +426,7 @@ mod tests { use crate::chainstate::stacks::address::StacksAddressExtensions; use crate::chainstate::stacks::index::TrieHashExtension; use crate::chainstate::stacks::StacksPublicKey; + use crate::core::StacksEpochId; use crate::core::MINING_COMMITMENT_WINDOW; use stacks_common::address::AddressHashMode; use stacks_common::types::chainstate::StacksAddress; @@ -485,8 +496,9 @@ mod tests { } } - fn make_block_commit( + fn make_block_commit_with_destroyed( burn_fee: u64, + destroyed: u64, vrf_ident: u32, block_id: u64, txid_id: u64, @@ -516,6 +528,7 @@ mod tests { key_vtxindex: 0, memo: vec![], burn_fee, + destroyed: destroyed, input: (input_txid, 3), apparent_sender: BurnchainSigner::new_p2pkh(&StacksPublicKey::new()), commit_outs: vec![], @@ -531,6 +544,19 @@ mod tests { } } + fn make_block_commit( + burn_fee: u64, + vrf_ident: u32, + block_id: u64, + txid_id: u64, + input_tx: Option, + block_ht: u64, + ) -> LeaderBlockCommitOp { + make_block_commit_with_destroyed( + burn_fee, 0, vrf_ident, block_id, txid_id, input_tx, block_ht, + ) + } + #[test] fn make_mean_min_median() { // test case 1: @@ -582,6 +608,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch21, commits.clone(), vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], vec![false, false, false, false, false, false], @@ -646,6 +673,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch2_05, commits.clone(), vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], vec![false, false, false, false, false, false], @@ -666,6 +694,174 @@ mod tests { assert_eq!(result[1].user_burns.len(), 0); } + #[test] + fn make_mean_min_median_with_destroyed() { + // test case 1: + // miner 1: 3 4 5 4 5 4 + // ub : 1 0 0 0 0 0 + // miner 2: 1 3 3 3 3 3 + // ub : 1 0 0 0 0 0 + // 0 1 0 0 0 0 + // .. + + // user burns are ignored: + // + // miner 1 => min = 3, median = 4, last_burn = 4 + // miner 2 => min = 1, median = 3, last_burn = 3 + + let commits = vec![ + vec![ + make_block_commit_with_destroyed(2, 1, 1, 1, 1, None, 1), + make_block_commit_with_destroyed(1, 0, 2, 2, 2, None, 1), + ], + vec![ + make_block_commit_with_destroyed(2, 2, 3, 3, 3, Some(1), 2), + make_block_commit_with_destroyed(2, 1, 4, 4, 4, Some(2), 2), + ], + vec![ + make_block_commit_with_destroyed(2, 3, 5, 5, 5, Some(3), 3), + make_block_commit_with_destroyed(2, 1, 6, 6, 6, Some(4), 3), + ], + vec![ + make_block_commit_with_destroyed(2, 2, 7, 7, 7, Some(5), 4), + make_block_commit_with_destroyed(2, 1, 8, 8, 8, Some(6), 4), + ], + vec![ + make_block_commit_with_destroyed(2, 3, 9, 9, 9, Some(7), 5), + make_block_commit_with_destroyed(2, 1, 10, 10, 10, Some(8), 5), + ], + vec![ + make_block_commit_with_destroyed(2, 2, 11, 11, 11, Some(9), 6), + make_block_commit_with_destroyed(2, 1, 12, 12, 12, Some(10), 6), + ], + ]; + let user_burns = vec![ + vec![make_user_burn(1, 1, 1, 1, 1), make_user_burn(1, 2, 2, 2, 1)], + vec![make_user_burn(1, 4, 4, 4, 2)], + vec![make_user_burn(1, 6, 6, 6, 3)], + vec![make_user_burn(1, 8, 8, 8, 4)], + vec![make_user_burn(1, 10, 10, 10, 5)], + vec![make_user_burn(1, 12, 12, 12, 6)], + ]; + + let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch21, + commits.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], + vec![false, false, false, false, false, false], + ); + + assert_eq!(result.len(), 2, "Should be two miners"); + + result.sort_by_key(|sample| sample.candidate.txid); + + assert_eq!(result[0].burns, 4); + assert_eq!(result[1].burns, 3); + + // make sure that we're associating with the last commit in the window. + assert_eq!(result[0].candidate.txid, commits[5][0].txid); + assert_eq!(result[1].candidate.txid, commits[5][1].txid); + + assert_eq!(result[0].user_burns.len(), 0); + assert_eq!(result[1].user_burns.len(), 0); + + // prior to 2.1, only the burn_fee value will be considered + let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch2_05, + commits.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], + vec![false, false, false, false, false, false], + ); + + assert_eq!(result.len(), 2, "Should be two miners"); + + result.sort_by_key(|sample| sample.candidate.txid); + + // destroyed tokens not counted + assert_eq!(result[0].burns, 2); + assert_eq!(result[1].burns, 2); + + // test case 2: + // miner 1: 4 4 5 4 5 3 + // miner 2: 4 4 4 4 4 1 + // ub : 0 0 0 0 0 2 + // *split* + + // miner 1 => min = 3, median = 4, last_burn = 3 + // miner 2 => min = 1, median = 4, last_burn = 1 + + let commits = vec![ + vec![ + make_block_commit_with_destroyed(2, 2, 1, 1, 1, None, 1), + make_block_commit_with_destroyed(2, 2, 2, 2, 2, None, 1), + ], + vec![ + make_block_commit_with_destroyed(2, 2, 3, 3, 3, Some(1), 2), + make_block_commit_with_destroyed(2, 2, 4, 4, 4, Some(2), 2), + ], + vec![ + make_block_commit_with_destroyed(2, 3, 5, 5, 5, Some(3), 3), + make_block_commit_with_destroyed(2, 2, 6, 6, 6, Some(4), 3), + ], + vec![ + make_block_commit_with_destroyed(2, 2, 7, 7, 7, Some(5), 4), + make_block_commit_with_destroyed(2, 2, 8, 8, 8, Some(6), 4), + ], + vec![ + make_block_commit_with_destroyed(2, 3, 9, 9, 9, Some(7), 5), + make_block_commit_with_destroyed(2, 2, 10, 10, 10, Some(8), 5), + ], + vec![ + make_block_commit_with_destroyed(2, 1, 11, 11, 11, Some(9), 6), + make_block_commit_with_destroyed(1, 0, 11, 11, 12, Some(10), 6), + ], + ]; + let user_burns = vec![ + vec![], + vec![], + vec![], + vec![], + vec![], + vec![make_user_burn(2, 11, 11, 1, 6)], + ]; + + let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch21, + commits.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], + vec![false, false, false, false, false, false], + ); + + assert_eq!(result.len(), 2, "Should be two miners"); + + result.sort_by_key(|sample| sample.candidate.txid); + + assert_eq!(result[0].burns, 3); + assert_eq!(result[1].burns, 1); + + // make sure that we're associating with the last commit in the window. + assert_eq!(result[0].candidate.txid, commits[5][0].txid); + assert_eq!(result[1].candidate.txid, commits[5][1].txid); + + assert_eq!(result[0].user_burns.len(), 0); + assert_eq!(result[1].user_burns.len(), 0); + + // prior to 2.1, only the burn_fee value will be considered + let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch2_05, + commits.clone(), + vec![vec![]; (MINING_COMMITMENT_WINDOW - 1) as usize], + vec![false, false, false, false, false, false], + ); + + assert_eq!(result.len(), 2, "Should be two miners"); + + result.sort_by_key(|sample| sample.candidate.txid); + + assert_eq!(result[0].burns, 2); + assert_eq!(result[1].burns, 1); + } + #[test] fn missed_block_commits() { // test case 1: @@ -705,6 +901,7 @@ mod tests { ]; let mut result = BurnSamplePoint::make_min_median_distribution( + StacksEpochId::Epoch21, commits.clone(), missed_commits.clone(), vec![false, false, false, false, false, false], @@ -1035,6 +1232,7 @@ mod tests { memo: vec![0x80], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1079,6 +1277,7 @@ mod tests { memo: vec![0x80], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1123,6 +1322,7 @@ mod tests { memo: vec![0x80], burn_fee: 23456, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1178,7 +1378,7 @@ mod tests { block_commits: vec![block_commit_1.clone()], user_burns: vec![], res: vec![BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256::max(), candidate: block_commit_1.clone(), @@ -1191,7 +1391,7 @@ mod tests { user_burns: vec![], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1203,7 +1403,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1222,7 +1422,7 @@ mod tests { user_burns: vec![user_burn_noblock.clone()], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1234,7 +1434,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1253,7 +1453,7 @@ mod tests { user_burns: vec![user_burn_nokey.clone()], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1265,7 +1465,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1288,7 +1488,7 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1300,7 +1500,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1324,7 +1524,7 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1336,7 +1536,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1362,7 +1562,7 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0xffffffffffffffff, @@ -1374,7 +1574,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0xffffffffffffffff, 0xffffffffffffffff, @@ -1408,7 +1608,7 @@ mod tests { ], res: vec![ BurnSamplePoint { - burns: block_commit_1.burn_fee.into(), + burns: block_commit_1.total_spend().into(), range_start: Uint256::zero(), range_end: Uint256([ 0x3ed94d3cb0a84709, @@ -1420,7 +1620,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: block_commit_2.burn_fee.into(), + burns: block_commit_2.total_spend().into(), range_start: Uint256([ 0x3ed94d3cb0a84709, 0x0963dded799a7c1a, @@ -1437,7 +1637,7 @@ mod tests { user_burns: vec![], }, BurnSamplePoint { - burns: (block_commit_3.burn_fee).into(), + burns: (block_commit_3.total_spend()).into(), range_start: Uint256([ 0x7db29a7961508e12, 0x12c7bbdaf334f834, diff --git a/src/chainstate/burn/operations/leader_block_commit.rs b/src/chainstate/burn/operations/leader_block_commit.rs index 6ab580a35d..c9aa0c74c6 100644 --- a/src/chainstate/burn/operations/leader_block_commit.rs +++ b/src/chainstate/burn/operations/leader_block_commit.rs @@ -85,6 +85,7 @@ impl LeaderBlockCommitOp { parent_vtxindex: 0, memo: vec![0x00], burn_fee: burn_fee, + destroyed: 0, input: input.clone(), block_header_hash: block_header_hash.clone(), commit_outs: vec![], @@ -117,6 +118,7 @@ impl LeaderBlockCommitOp { parent_vtxindex: parent.vtxindex as u16, memo: vec![], burn_fee: burn_fee, + destroyed: 0, input: input.clone(), block_header_hash: block_header_hash.clone(), commit_outs: vec![], @@ -290,14 +292,14 @@ impl LeaderBlockCommitOp { return Err(op_error::ParseError); } - let (commit_outs, burn_fee) = if burnchain.is_in_prepare_phase(block_height) { + let (commit_outs, burn_fee, destroyed) = if burnchain.is_in_prepare_phase(block_height) { // check if we're in a prepare phase // should be only one burn output if !outputs[0].address.is_burn() { return Err(op_error::BlockCommitBadOutputs); } let BurnchainRecipient { address, amount } = outputs.remove(0); - (vec![address], amount) + (vec![address], amount, 0) } else { let mut commit_outs = vec![]; let mut pox_fee = None; @@ -335,7 +337,7 @@ impl LeaderBlockCommitOp { return Err(op_error::ParseError); } - (commit_outs, burn_fee) + (commit_outs, burn_fee, tx.get_burn_amount()) }; let input = tx @@ -359,6 +361,7 @@ impl LeaderBlockCommitOp { commit_outs, burn_fee, + destroyed, input, apparent_sender, @@ -389,6 +392,33 @@ impl LeaderBlockCommitOp { pub fn is_first_block(&self) -> bool { self.parent_block_ptr == 0 && self.parent_vtxindex == 0 } + + /// Method to get the total number of burnchain tokens destroyed, so we make *absolutely + /// certain* to count both the `destroyed` and `burn_fee` quantities. + pub fn total_spend(&self) -> u64 { + self.burn_fee + .checked_add(self.destroyed) + .expect("FATAL: too many tokens spent") + } + + /// Epoch-specific measurement of token spend. In epochs prior to 2.1, we only count the + /// burn_fee. In epochs 2.1 and later, we count the total spend + pub fn sortition_spend(&self, epoch_id: StacksEpochId) -> u64 { + match epoch_id { + // in epoch 2.1 and later, we count both the PoX spend and the amount destroyed via + // the OP_RETURN. Before that, we only counted the burn fee. + StacksEpochId::Epoch21 => self.total_spend(), + _ => self.burn_fee, + } + } + + /// Convert the burn_fee and destroyed members to strings for storing in a DB + pub fn encode_spends_for_db(&self) -> (String, String) { + ( + format!("{}", &self.burn_fee), + format!("{}", &self.destroyed), + ) + } } impl StacksMessageCodec for LeaderBlockCommitOp { @@ -476,7 +506,7 @@ impl LeaderBlockCommitOp { ) -> Result<(), op_error> { let parent_block_height = self.parent_block_ptr as u64; - ///////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////e///////////////////////////////// // This tx must have the expected commit or burn outputs: // * if there is a known anchor block for the current reward cycle, and this // block commit descends from that block, and this block commit is not in the @@ -522,8 +552,10 @@ impl LeaderBlockCommitOp { } } else { let expect_pox_descendant = if self.all_outputs_burn() { + // reward-phase with PoB (does not descend from anchor block) false } else { + // reward-phase with PoX (must descend from anchor block) let mut check_recipients: Vec<_> = reward_set_info .recipients .iter() @@ -560,6 +592,7 @@ impl LeaderBlockCommitOp { return Err(op_error::BlockCommitBadOutputs); } } + true }; @@ -568,6 +601,7 @@ impl LeaderBlockCommitOp { error!("Failed to check whether parent (height={}) is descendent of anchor block={}: {}", parent_block_height, &reward_set_info.anchor_block, e); op_error::BlockCommitAnchorCheck})?; + if descended_from_anchor != expect_pox_descendant { if descended_from_anchor { warn!("Invalid block commit: descended from PoX anchor, but used burn outputs"); @@ -577,6 +611,40 @@ impl LeaderBlockCommitOp { } return Err(op_error::BlockCommitBadOutputs); } + + if descended_from_anchor { + // if this is a PoX anchor block descendant, and we're in Stacks 2.1 or later, + // then we need to verify that the total PoX payout does not exceed the PoX + // cutoff for this reward cycle. Any additional expenditure must be destroyed. + let epoch = + SortitionDB::get_stacks_epoch(tx, self.block_height)?.expect(&format!( + "FATAL: impossible block height: no epoch defined for {}", + self.block_height + )); + match epoch.epoch_id { + StacksEpochId::Epoch21 => { + let pox_cutoff = tx.get_pox_cutoff()?.expect( + "FATAL: In PoX in Epoch 2.1 but do not have a known PoX cutoff", + ); + + if self.burn_fee > pox_cutoff { + warn!("Invalid block commit: PoX cutoff is {}, but sent {} to PoX addresses", pox_cutoff, self.burn_fee); + return Err(op_error::BlockCommitPoxOverpay); + } + + if self.total_spend() > self.burn_fee { + // must have sent `pox_cutoff` tokens + if self.burn_fee != pox_cutoff { + warn!("Invalid block commit: total spend exceeds PoX cutoff ({}) but PoX payout ({}) was incomplete", pox_cutoff, self.burn_fee); + return Err(op_error::BlockCommitPoxUnderpay); + } + } + } + _ => { + // noop + } + } + } } } } else { @@ -617,7 +685,7 @@ impl LeaderBlockCommitOp { let tx_tip = tx.context.chain_tip.clone(); ///////////////////////////////////////////////////////////////////////////////////// - // There must be a burn + // There must be a burn/spend ///////////////////////////////////////////////////////////////////////////////////// let apparent_sender_address = self @@ -870,6 +938,9 @@ mod tests { use crate::types::chainstate::StacksAddress; use crate::types::chainstate::{BlockHeaderHash, SortitionId, VRFSeed}; + use crate::chainstate::coordinator::PoxAnchorBlockStatus; + use crate::chainstate::coordinator::RewardCycleInfo; + use super::*; use clarity::vm::costs::ExecutionCost; @@ -1253,6 +1324,7 @@ mod tests { ], burn_fee: 24690, + destroyed: 0, input: (Txid([0x11; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![ @@ -1384,6 +1456,10 @@ mod tests { "0000000000000000000000000000000000000000000000000000000000001260", ) .unwrap(); + let block_127_hash = BurnchainHeaderHash::from_hex( + "0000000000000000000000000000000000000000000000000000000000001270", + ) + .unwrap(); let block_header_hashes = [ block_122_hash.clone(), @@ -1391,10 +1467,13 @@ mod tests { block_124_hash.clone(), block_125_hash.clone(), // prepare phase block_126_hash.clone(), // prepare phase + block_127_hash.clone(), ]; + let pox_consts = PoxConstants::new(5, 2, 2, 25, 5, u32::max_value()); + let burnchain = Burnchain { - pox_constants: PoxConstants::new(6, 2, 2, 25, 5, u32::max_value()), + pox_constants: pox_consts.clone(), peer_version: 0x012345678, network_id: 0x9abcdef0, chain_name: "bitcoin".to_string(), @@ -1408,6 +1487,35 @@ mod tests { first_block_hash: first_burn_hash.clone(), }; + let leader_key_0 = LeaderKeyRegisterOp { + consensus_hash: ConsensusHash::from_bytes( + &hex_bytes("1111111111111111111111111111111111111111").unwrap(), + ) + .unwrap(), + public_key: VRFPublicKey::from_bytes( + &hex_bytes("cc66b0e89e4bdd79d5d4926a260af06036fcebd9f518892a242d418cb5562347") + .unwrap(), + ) + .unwrap(), + memo: vec![01, 02, 03, 04, 05], + address: StacksAddress::from_bitcoin_address( + &BitcoinAddress::from_scriptpubkey( + BitcoinNetworkType::Testnet, + &hex_bytes("76a914306231b2782b5f80d944bf69f9d46a1453a0a0eb88ac").unwrap(), + ) + .unwrap(), + ), + + txid: Txid::from_bytes_be( + &hex_bytes("f3b73257538366cbf11d9b23d69fc9710a015d73ab79ddc2582682c4b5dfb4e9") + .unwrap(), + ) + .unwrap(), + vtxindex: 2, + block_height: 122, + burn_header_hash: block_122_hash.clone(), + }; + let leader_key_1 = LeaderKeyRegisterOp { consensus_hash: ConsensusHash::from_bytes( &hex_bytes("2222222222222222222222222222222222222222").unwrap(), @@ -1466,10 +1574,94 @@ mod tests { burn_header_hash: block_124_hash.clone(), }; + // consumes leader_key_0; will be the anchor block + let block_commit_0 = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("1111111111111111111111111111111111111111111111111111111111111111") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("4444444444444444444444444444444444444444444444444444444444444444") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: 0, + parent_vtxindex: 0, + key_block_ptr: 122, + key_vtxindex: 2, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: 12345, + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("70b9a33eff6b30954caac52b862dc5bafc5f7108d3509e7bb9180d79f6bea160") + .unwrap(), + ) + .unwrap(), + vtxindex: 444, + block_height: 124, + burn_parent_modulus: (123 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_123_hash.clone(), + }; + + // consumes leader_key_0, but is not the anchor block + let block_commit_0_not_anchor = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("1111111111111111111111111111111111111111111111111111111111111112") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("4444444444444444444444444444444444444444444444444444444444444445") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: 0, + parent_vtxindex: 0, + key_block_ptr: 122, + key_vtxindex: 2, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: 12345, + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("80b9a33eff6b30954caac52b862dc5bafc5f7108d3509e7bb9180d79f6bea160") + .unwrap(), + ) + .unwrap(), + vtxindex: 200, + block_height: 123, + burn_parent_modulus: (122 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_123_hash.clone(), + }; + // consumes leader_key_1 let block_commit_1 = LeaderBlockCommitOp { block_header_hash: BlockHeaderHash::from_bytes( - &hex_bytes("2222222222222222222222222222222222222222222222222222222222222222") + &hex_bytes("2222222222222222222222222222222222222222222222222222222222222221") .unwrap(), ) .unwrap(), @@ -1486,6 +1678,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1507,6 +1700,274 @@ mod tests { burn_header_hash: block_125_hash.clone(), }; + // consumes leader_key_1 + let block_commit_1_not_anchor = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("2222222222222222222222222222222222222222222222222222222222222211") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("3333333333333333333333333333333333333333333333333333333333333322") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: block_commit_0_not_anchor.block_height.try_into().unwrap(), + parent_vtxindex: block_commit_0_not_anchor.vtxindex.try_into().unwrap(), + key_block_ptr: 124, + key_vtxindex: 456, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: 12345, + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27ce") + .unwrap(), + ) + .unwrap(), + vtxindex: 444, + block_height: 126, + burn_parent_modulus: (125 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_126_hash.clone(), + }; + + // consumes leader_key_1 but in second reward cycle + let block_commit_2 = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("4444444444444444444444444444444444444444444444444444444444444444") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("5555555555555555555555555555555555555555555555555555555555555555") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: block_commit_0.block_height.try_into().unwrap(), + parent_vtxindex: block_commit_0.vtxindex.try_into().unwrap(), + key_block_ptr: 124, + key_vtxindex: 456, + memo: vec![0x80], + commit_outs: vec![ + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + ], + + burn_fee: 10_000, // maximum PoX payout allowed in this cycle + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d0") + .unwrap(), + ) + .unwrap(), + vtxindex: 444, + block_height: 127, + burn_parent_modulus: (126 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_127_hash.clone(), + }; + + // consumes leader_key_1 but in second reward cycle + let block_commit_2_destroyed = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("5555555555555555555555555555555555555555555555555555555555555555") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("6666666666666666666666666666666666666666666666666666666666666666") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: block_commit_0.block_height.try_into().unwrap(), + parent_vtxindex: block_commit_0.vtxindex.try_into().unwrap(), + key_block_ptr: 124, + key_vtxindex: 456, + memo: vec![0x80], + commit_outs: vec![ + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + ], + + burn_fee: 10_000, // maximum PoX payout allowed in this cycle + destroyed: 5_000, // total_spend() should be 15_000 + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d1") + .unwrap(), + ) + .unwrap(), + vtxindex: 445, + block_height: 127, + burn_parent_modulus: (126 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_127_hash.clone(), + }; + + // consumes leader_key_1 but in second reward cycle. + // This is valid because it does PoB during PoX -- there's nothing to overpay or overspend. + let block_commit_2_pob_destroyed = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("6666666666666666666666666666666666666666666666666666666666666666") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("7777777777777777777777777777777777777777777777777777777777777777") + .unwrap(), + ) + .unwrap(), + // does not descend from anchor block + parent_block_ptr: block_commit_1_not_anchor.block_height.try_into().unwrap(), + parent_vtxindex: block_commit_1_not_anchor.vtxindex.try_into().unwrap(), + key_block_ptr: 124, + key_vtxindex: 456, + memo: vec![0x80], + commit_outs: vec![], + + burn_fee: 15_000, // exceeds maximum PoX payout allowed in this cycle, but is PoB + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d2") + .unwrap(), + ) + .unwrap(), + vtxindex: 445, + block_height: 127, + burn_parent_modulus: (126 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_127_hash.clone(), + }; + + // consumes leader_key_1 but in second reward cycle + // this is invalid because it overpays PoX outputs + let block_commit_2_pox_not_destroyed = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("7777777777777777777777777777777777777777777777777777777777777777") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("8888888888888888888888888888888888888888888888888888888888888888") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: block_commit_0.block_height.try_into().unwrap(), + parent_vtxindex: block_commit_0.block_height.try_into().unwrap(), + key_block_ptr: 124, + key_vtxindex: 456, + memo: vec![0x80], + commit_outs: vec![ + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + ], + + burn_fee: 15_000, // exceeds maximum PoX payout allowed in this cycle, so will fail + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d3") + .unwrap(), + ) + .unwrap(), + vtxindex: 445, + block_height: 127, + burn_parent_modulus: (126 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_127_hash.clone(), + }; + + // consumes leader_key_1 but in second reward cycle + // this is invalid because it underpays PoX outputs + let block_commit_2_pox_underpaid = LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash::from_bytes( + &hex_bytes("8888888888888888888888888888888888888888888888888888888888888888") + .unwrap(), + ) + .unwrap(), + new_seed: VRFSeed::from_bytes( + &hex_bytes("9999999999999999999999999999999999999999999999999999999999999999") + .unwrap(), + ) + .unwrap(), + parent_block_ptr: block_commit_0.block_height.try_into().unwrap(), + parent_vtxindex: block_commit_0.block_height.try_into().unwrap(), + key_block_ptr: 124, + key_vtxindex: 456, + memo: vec![0x80], + commit_outs: vec![ + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6").unwrap(), + ], + + burn_fee: 9_999, // does not meet maximum PoX output + destroyed: 1, + input: (Txid([0; 32]), 0), + apparent_sender: BurnchainSigner { + public_keys: vec![StacksPublicKey::from_hex( + "02d8015134d9db8178ac93acbc43170a2f20febba5087a5b0437058765ad5133d0", + ) + .unwrap()], + num_sigs: 1, + hash_mode: AddressHashMode::SerializeP2PKH, + }, + + txid: Txid::from_bytes_be( + &hex_bytes("3c07a0a93360bc85047bbaadd49e30c8af770f73a37e10fec400174d2e5f27d4") + .unwrap(), + ) + .unwrap(), + vtxindex: 445, + block_height: 127, + burn_parent_modulus: (126 % BURN_BLOCK_MINED_AT_MODULUS) as u8, + burn_header_hash: block_127_hash.clone(), + }; + let mut db = SortitionDB::connect_test_with_epochs( first_block_height, &first_burn_hash, @@ -1515,11 +1976,16 @@ mod tests { .unwrap(); let block_ops = vec![ // 122 - vec![], + vec![BlockstackOperationType::LeaderKeyRegister( + leader_key_0.clone(), + )], // 123 - vec![], + vec![BlockstackOperationType::LeaderBlockCommit( + block_commit_0_not_anchor.clone(), + )], // 124 vec![ + BlockstackOperationType::LeaderBlockCommit(block_commit_0.clone()), BlockstackOperationType::LeaderKeyRegister(leader_key_1.clone()), BlockstackOperationType::LeaderKeyRegister(leader_key_2.clone()), ], @@ -1528,12 +1994,42 @@ mod tests { block_commit_1.clone(), )], // 126 - vec![], + vec![BlockstackOperationType::LeaderBlockCommit( + block_commit_1_not_anchor.clone(), + )], + // 127 + vec![BlockstackOperationType::LeaderBlockCommit( + block_commit_2.clone(), + )], ]; + let mut test_reward_set_info = None; let tip_index_root = { let mut prev_snapshot = SortitionDB::get_first_block_snapshot(db.conn()).unwrap(); for i in 0..block_header_hashes.len() { + let (sortition, winning_block_txid, winning_stacks_block_hash, num_sortitions) = { + let mut sortition = false; + let mut winning_block_txid = Txid([0x00; 32]); + let mut winning_stacks_block_hash = BlockHeaderHash([0x00; 32]); + let mut num_sortitions = prev_snapshot.num_sortitions; + for j in 0..block_ops[i].len() { + if let BlockstackOperationType::LeaderBlockCommit(ref op) = block_ops[i][j] + { + sortition = true; + winning_block_txid = op.txid.clone(); + winning_stacks_block_hash = op.block_header_hash.clone(); + num_sortitions += 1; + break; + } + } + ( + sortition, + winning_block_txid, + winning_stacks_block_hash, + num_sortitions, + ) + }; + let mut snapshot_row = BlockSnapshot { accumulated_coinbase_ustx: 0, pox_valid: true, @@ -1572,18 +2068,13 @@ mod tests { ]) .unwrap(), total_burn: i as u64, - sortition: true, - sortition_hash: SortitionHash::initial(), - winning_block_txid: Txid::from_hex( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), - winning_stacks_block_hash: BlockHeaderHash::from_hex( - "0000000000000000000000000000000000000000000000000000000000000000", - ) - .unwrap(), + sortition: sortition, + sortition_hash: SortitionHash(Sha512Trunc256Sum::from_data(&i.to_be_bytes()).0), + winning_block_txid: winning_block_txid, + winning_stacks_block_hash: winning_stacks_block_hash, + // overwritten index_root: TrieHash::from_empty_data(), - num_sortitions: (i + 1) as u64, + num_sortitions: num_sortitions, stacks_block_accepted: false, stacks_block_height: 0, arrival_index: 0, @@ -1591,16 +2082,64 @@ mod tests { canonical_stacks_tip_hash: BlockHeaderHash([0u8; 32]), canonical_stacks_tip_consensus_hash: ConsensusHash([0u8; 20]), }; + test_debug!( + "Sortition ID of height {} is {}, parent is {}", + snapshot_row.block_height, + &snapshot_row.sortition_id, + &snapshot_row.parent_sortition_id + ); let mut tx = SortitionHandleTx::begin(&mut db, &prev_snapshot.sortition_id).unwrap(); + + let (reward_cycle_info, reward_set_info) = if i == block_header_hashes.len() - 1 { + // this is the first snapshot in the second reward phase, so add PoX data + let mock_reward_addresses = + vec![ + StacksAddress::from_string("STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6") + .unwrap(); + ((pox_consts.reward_cycle_length - pox_consts.prepare_length) * 2) + as usize + ]; + ( + Some(RewardCycleInfo { + anchor_status: PoxAnchorBlockStatus::SelectedAndKnown( + block_commit_0.block_header_hash.clone(), + mock_reward_addresses, + ), + pox_cutoff: 10_000, + }), + Some(RewardSetInfo { + anchor_block: block_commit_0.block_header_hash.clone(), + recipients: vec![ + ( + StacksAddress::from_string( + "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6", + ) + .unwrap(), + 0, + ), + ( + StacksAddress::from_string( + "STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6", + ) + .unwrap(), + 1, + ), + ], + }), + ) + } else { + (None, None) + }; + let next_index_root = tx .append_chain_tip_snapshot( &prev_snapshot, &snapshot_row, &block_ops[i], &vec![], - None, - None, + reward_cycle_info, + reward_set_info.as_ref(), None, ) .unwrap(); @@ -1609,6 +2148,9 @@ mod tests { tx.commit().unwrap(); prev_snapshot = snapshot_row; + if test_reward_set_info.is_none() { + test_reward_set_info = reward_set_info; + } } prev_snapshot.index_root.clone() @@ -1640,6 +2182,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1689,6 +2232,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1738,6 +2282,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1787,6 +2332,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1848,6 +2394,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -1884,6 +2431,22 @@ mod tests { intended_sortition: SortitionId(first_burn_hash.0.clone()), })), }, + CheckFixture { + op: block_commit_2_destroyed, + res: Ok(()), + }, + CheckFixture { + op: block_commit_2_pob_destroyed, + res: Ok(()), + }, + CheckFixture { + op: block_commit_2_pox_not_destroyed, + res: Err(op_error::BlockCommitPoxOverpay), + }, + CheckFixture { + op: block_commit_2_pox_underpaid, + res: Err(op_error::BlockCommitPoxUnderpay), + }, ]; for (ix, fixture) in fixtures.iter().enumerate() { @@ -1902,7 +2465,18 @@ mod tests { .unwrap(); assert_eq!( format!("{:?}", &fixture.res), - format!("{:?}", &fixture.op.check(&burnchain, &mut ic, None)) + format!( + "{:?}", + &fixture.op.check( + &burnchain, + &mut ic, + if fixture.op.block_height >= 127 { + test_reward_set_info.as_ref() + } else { + None + } + ) + ) ); } } @@ -2037,6 +2611,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2188,6 +2763,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2237,6 +2813,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2286,6 +2863,7 @@ mod tests { memo: vec![0x80], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2335,6 +2913,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2384,6 +2963,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2433,6 +3013,7 @@ mod tests { commit_outs: vec![], burn_fee: 0, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2482,6 +3063,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2531,6 +3113,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2580,6 +3163,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2720,6 +3304,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2748,6 +3333,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2776,6 +3362,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2804,6 +3391,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( @@ -2832,6 +3420,7 @@ mod tests { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( diff --git a/src/chainstate/burn/operations/mod.rs b/src/chainstate/burn/operations/mod.rs index 2e0a82b36f..ac3f0c2c9e 100644 --- a/src/chainstate/burn/operations/mod.rs +++ b/src/chainstate/burn/operations/mod.rs @@ -74,6 +74,8 @@ pub enum Error { BlockCommitAnchorCheck, BlockCommitBadModulus, BlockCommitBadEpoch, + BlockCommitPoxOverpay, + BlockCommitPoxUnderpay, MissedBlockCommit(MissedBlockCommit), // all the things that can go wrong with leader key register @@ -120,6 +122,12 @@ impl fmt::Display for Error { Error::BlockCommitBadEpoch => { write!(f, "Block commit has an invalid epoch") } + Error::BlockCommitPoxOverpay => { + write!(f, "Block commit overpaid PoX addresses") + } + Error::BlockCommitPoxUnderpay => { + write!(f, "Block commit did not pay the full PoX amount") + } Error::MissedBlockCommit(_) => write!( f, "Block commit included in a burn block that was not intended" @@ -218,6 +226,8 @@ pub struct LeaderBlockCommitOp { /// how many burn tokens (e.g. satoshis) were committed to produce this block pub burn_fee: u64, + /// number of burnchain tokens destroyed. + pub destroyed: u64, /// the input transaction, used in mining commitment smoothing pub input: (Txid, u32), diff --git a/src/chainstate/coordinator/mod.rs b/src/chainstate/coordinator/mod.rs index cb952ec3b0..53aa0b6f7e 100644 --- a/src/chainstate/coordinator/mod.rs +++ b/src/chainstate/coordinator/mod.rs @@ -79,6 +79,7 @@ pub enum PoxAnchorBlockStatus { #[derive(Debug, PartialEq)] pub struct RewardCycleInfo { pub anchor_status: PoxAnchorBlockStatus, + pub pox_cutoff: u64, } impl RewardCycleInfo { @@ -104,11 +105,20 @@ impl RewardCycleInfo { NotSelected => None, } } - pub fn known_selected_anchor_block_owned(self) -> Option> { + pub fn known_selected_anchor_block_owned(self) -> Option<(Vec, u64)> { use self::PoxAnchorBlockStatus::*; match self.anchor_status { SelectedAndUnknown(_) => None, - SelectedAndKnown(_, reward_set) => Some(reward_set), + SelectedAndKnown(_, reward_set) => Some((reward_set, self.pox_cutoff)), + NotSelected => None, + } + } + pub fn get_pox_cutoff(&self) -> Option { + use self::PoxAnchorBlockStatus::*; + match self.anchor_status { + SelectedAndUnknown(_) => Some(self.pox_cutoff), + SelectedAndKnown(_, _) => Some(self.pox_cutoff), + // n/a. NotSelected => None, } } @@ -452,7 +462,7 @@ pub fn get_reward_cycle_info( let ic = sort_db.index_handle(sortition_tip); ic.get_chosen_pox_anchor(&parent_bhh, &burnchain.pox_constants) }?; - if let Some((consensus_hash, stacks_block_hash)) = reward_cycle_info { + if let Some((consensus_hash, stacks_block_hash, pox_cutoff)) = reward_cycle_info { info!("Anchor block selected: {}", stacks_block_hash); let anchor_block_known = StacksChainState::is_stacks_block_processed( &chain_state.db(), @@ -472,10 +482,14 @@ pub fn get_reward_cycle_info( } else { PoxAnchorBlockStatus::SelectedAndUnknown(stacks_block_hash) }; - Ok(Some(RewardCycleInfo { anchor_status })) + Ok(Some(RewardCycleInfo { + anchor_status, + pox_cutoff, + })) } else { Ok(Some(RewardCycleInfo { anchor_status: PoxAnchorBlockStatus::NotSelected, + pox_cutoff: 0, })) } } else { @@ -496,7 +510,7 @@ fn calculate_paid_rewards(ops: &[BlockstackOperationType]) -> PaidRewards { if commit.commit_outs.len() == 0 { continue; } - let amt_per_address = commit.burn_fee / (commit.commit_outs.len() as u64); + let amt_per_address = commit.total_spend() / (commit.commit_outs.len() as u64); for addr in commit.commit_outs.iter() { if addr.is_burn() { burn_amt += amt_per_address; diff --git a/src/chainstate/coordinator/tests.rs b/src/chainstate/coordinator/tests.rs index 2865067c1e..b444cbcf27 100644 --- a/src/chainstate/coordinator/tests.rs +++ b/src/chainstate/coordinator/tests.rs @@ -510,6 +510,7 @@ fn make_genesis_block_with_recipients( let commit_op = LeaderBlockCommitOp { block_header_hash: block.block_hash(), burn_fee: my_burn, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { num_sigs: 1, @@ -691,6 +692,7 @@ fn make_stacks_block_with_input( let commit_op = LeaderBlockCommitOp { block_header_hash: block.block_hash(), burn_fee: my_burn, + destroyed: 0, input, apparent_sender: BurnchainSigner { num_sigs: 1, diff --git a/src/chainstate/stacks/block.rs b/src/chainstate/stacks/block.rs index 576f544f8c..33ceba6c09 100644 --- a/src/chainstate/stacks/block.rs +++ b/src/chainstate/stacks/block.rs @@ -1330,6 +1330,7 @@ mod test { commit_outs: vec![], burn_fee: 12345, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: BurnchainSigner { public_keys: vec![StacksPublicKey::from_hex( diff --git a/src/chainstate/stacks/boot/mod.rs b/src/chainstate/stacks/boot/mod.rs index 7cad205f98..c11763f4ca 100644 --- a/src/chainstate/stacks/boot/mod.rs +++ b/src/chainstate/stacks/boot/mod.rs @@ -1866,7 +1866,7 @@ pub mod test { if let BlockstackOperationType::LeaderBlockCommit(ref opdata) = &op { eprintln!("prepare phase {}: {:?}", burn_height, opdata); assert!(opdata.all_outputs_burn()); - assert!(opdata.burn_fee > 0); + assert!(opdata.total_spend() > 0); if tenure_id > 1 && cur_reward_cycle > lockup_reward_cycle { prepared = true; @@ -1886,7 +1886,7 @@ pub mod test { assert!(opdata.all_outputs_burn()); } - assert!(opdata.burn_fee > 0); + assert!(opdata.total_spend() > 0); } } } diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index fcbe151685..362f5a5009 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -3789,7 +3789,14 @@ impl StacksChainState { SortitionDB::get_block_burn_amount(db_handle, &penultimate_sortition_snapshot) .expect("FATAL: have block commit but no total burns in its sortition"); - Ok(Some((block_commit.burn_fee, sortition_burns))) + let epoch = + SortitionDB::get_stacks_epoch(db_handle, penultimate_sortition_snapshot.block_height)? + .expect("FATAL: do not have epoch at penultimate sortition"); + + Ok(Some(( + block_commit.sortition_spend(epoch.epoch_id), + sortition_burns, + ))) } /// Pre-process and store an anchored block to staging, queuing it up for diff --git a/src/main.rs b/src/main.rs index 70a43584e2..453c3f04d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,6 +52,7 @@ use blockstack_lib::burnchains::bitcoin::BitcoinNetworkType; use blockstack_lib::burnchains::db::BurnchainDB; use blockstack_lib::burnchains::Address; use blockstack_lib::burnchains::Burnchain; +use blockstack_lib::burnchains::BurnchainParameters; use blockstack_lib::burnchains::Txid; use blockstack_lib::chainstate::burn::ConsensusHash; use blockstack_lib::chainstate::stacks::db::blocks::DummyEventDispatcher; @@ -684,7 +685,7 @@ check if the associated microblocks can be downloaded .expect("Failed to compute PoX cycle"); match result { - Ok((_, _, confirmed_by)) => results.push((eval_height, true, confirmed_by)), + Ok((_, _, confirmed_by, ..)) => results.push((eval_height, true, confirmed_by)), Err(confirmed_by) => results.push((eval_height, false, confirmed_by)), }; } @@ -697,6 +698,113 @@ check if the associated microblocks can be downloaded process::exit(0); } + if argv[1] == "reward-cycle-spends" { + if argv.len() < 4 { + eprintln!( + "Usage: {} reward-cycle-spends ", + argv[0] + ); + process::exit(1); + } + let sort_db = SortitionDB::open(&argv[2], false, PoxConstants::mainnet_default()) + .expect(&format!("Failed to open {}", argv[2])); + let cycle: u64 = argv[3].parse().expect("Failed to parse "); + + let burnchain_params = BurnchainParameters::bitcoin_mainnet(); + let pox_consts = PoxConstants::mainnet_default(); + let start_height = burnchain_params.first_block_height + + cycle * (pox_consts.reward_cycle_length as u64) + + 1; + let end_height = burnchain_params.first_block_height + + (cycle + 1) * (pox_consts.reward_cycle_length as u64) + + 1; + let prepare_start_height = end_height - (pox_consts.prepare_length as u64); + + let chain_tip = SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()) + .expect("Failed to get sortition chain tip"); + + if chain_tip.block_height < end_height { + eprintln!( + "Burnchain tip is not yet at end-of-cycle height {} (only at {})", + &end_height, &chain_tip.block_height + ); + process::exit(1); + } + + let sort_conn = sort_db.index_handle(&chain_tip.sortition_id); + + let reward_phase_winners = sort_conn + .get_sortition_winners_in_fork( + (start_height - 1) as u32, + (prepare_start_height - 1) as u32, + ) + .unwrap(); + + let mut reward_phase_payouts = vec![]; + let mut cur_height = start_height; + + for (winner_txid, winner_height) in reward_phase_winners.into_iter() { + while cur_height < winner_height { + // no sortition here + reward_phase_payouts.push(0); + cur_height += 1; + } + let block_commit = sort_conn + .get_block_commit_by_txid(&winner_txid) + .unwrap() + .unwrap(); + reward_phase_payouts.push(block_commit.total_spend()); + cur_height += 1; + } + + assert_eq!( + reward_phase_payouts.len() as u64, + (pox_consts.reward_cycle_length - pox_consts.prepare_length) as u64 + ); + + let reward_cycle_tip = + SortitionDB::get_ancestor_snapshot(&sort_conn, end_height - 1, &chain_tip.sortition_id) + .expect("Failed to get chain tip to evaluate at") + .expect("Failed to get chain tip to evaluate at"); + + let pox_result = sort_conn + .get_chosen_pox_anchor_check_position( + &reward_cycle_tip.burn_header_hash, + &pox_consts, + true, + ) + .expect("Failed to compute PoX cycle"); + + let reward_cycle_json = match pox_result { + Ok((ch, bh, confirmed_by, burns)) => { + json!({ + "reward_phase": reward_phase_payouts, + "prepare_phase": { + "decision": "PoX", + "anchor_block": { + "block_hash": format!("{}", &bh), + "consensus_hash": format!("{}", &ch), + "confirmations": confirmed_by, + }, + "burns": burns + } + }) + } + Err(confirmed_by) => { + json!({ + "reward_phase": reward_phase_payouts, + "prepare_phase": { + "decision": "PoB", + "max_confirmations": confirmed_by + } + }) + } + }; + + println!("{}", reward_cycle_json.to_string()); + process::exit(0); + } + if argv[1] == "try-mine" { if argv.len() < 3 { eprintln!( diff --git a/src/net/rpc.rs b/src/net/rpc.rs index 0fca5e664c..8ece30cac7 100644 --- a/src/net/rpc.rs +++ b/src/net/rpc.rs @@ -264,10 +264,17 @@ impl RPCPoxInfoData { chainstate: &mut StacksChainState, tip: &StacksBlockId, burnchain: &Burnchain, + burn_block_height: u64, ) -> Result { let mainnet = chainstate.mainnet; let chain_id = chainstate.chain_id; - let contract_identifier = boot_code_id("pox", mainnet); + let contract_identifier = boot_code_id( + burnchain + .pox_constants + .active_pox_contract(burn_block_height), + mainnet, + ); + let function = "get-pox-info"; let cost_track = LimitedCostTracker::new_free(); let sender = PrincipalData::Standard(StandardPrincipalData::transient()); @@ -672,11 +679,12 @@ impl ConversationHttp { tip: &StacksBlockId, burnchain: &Burnchain, canonical_stacks_tip_height: u64, + burn_block_height: u64, ) -> Result<(), net_error> { let response_metadata = HttpResponseMetadata::from_http_request_type(req, Some(canonical_stacks_tip_height)); - match RPCPoxInfoData::from_db(sortdb, chainstate, tip, burnchain) { + match RPCPoxInfoData::from_db(sortdb, chainstate, tip, burnchain, burn_block_height) { Ok(pi) => { let response = HttpResponseType::PoxInfo(response_metadata, pi); response.send(http, fd) @@ -2262,6 +2270,7 @@ impl ConversationHttp { &tip, &network.burnchain, network.burnchain_tip.canonical_stacks_tip_height, + network.burnchain_tip.block_height, )?; } None @@ -4137,11 +4146,13 @@ mod test { &tip.anchored_block_hash, ) }; + let burn_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); let pox_info = RPCPoxInfoData::from_db( &mut sortdb, chainstate, &stacks_block_id, &peer_client.config.burnchain, + burn_tip.block_height, ) .unwrap(); *pox_server_info.borrow_mut() = Some(pox_info); @@ -4194,11 +4205,13 @@ mod test { .unwrap() .unconfirmed_chain_tip .clone(); + let burn_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap(); let pox_info = RPCPoxInfoData::from_db( &mut sortdb, chainstate, &stacks_block_id, &peer_client.config.burnchain, + burn_tip.block_height, ) .unwrap(); *pox_server_info.borrow_mut() = Some(pox_info); diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 765fcfc1c3..f444455096 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -56,6 +56,8 @@ use stacks_common::deps_common::bitcoin::network::encodable::ConsensusEncodable; use stacks_common::deps_common::bitcoin::network::serialize::RawEncoder; use stacks_common::deps_common::bitcoin::util::hash::Sha256dHash; +use stacks_common::util::hash::to_hex; + use stacks::monitoring::{increment_btc_blocks_received_counter, increment_btc_ops_sent_counter}; #[cfg(test)] @@ -96,6 +98,7 @@ impl OngoingBlockCommit { #[derive(Clone)] struct LeaderBlockCommitFees { + destroyed_fee: u64, fee_rate: u64, sortition_fee: u64, outputs_len: u64, @@ -123,6 +126,12 @@ impl LeaderBlockCommitFees { payload: &LeaderBlockCommitOp, config: &Config, ) -> LeaderBlockCommitFees { + let destroyed_fee = if payload.destroyed > 0 { + cmp::max(payload.destroyed, DUST_UTXO_LIMIT) + } else { + 0 + }; + let number_of_transfers = payload.commit_outs.len() as u64; let value_per_transfer = payload.burn_fee / number_of_transfers; let sortition_fee = value_per_transfer * number_of_transfers; @@ -131,6 +140,7 @@ impl LeaderBlockCommitFees { let default_tx_size = config.burnchain.block_commit_tx_estimated_size; LeaderBlockCommitFees { + destroyed_fee, fee_rate, sortition_fee, outputs_len: number_of_transfers, @@ -154,7 +164,7 @@ impl LeaderBlockCommitFees { } pub fn estimated_amount_required(&self) -> u64 { - self.estimated_miner_fee() + self.rbf_fee() + self.sortition_fee + self.estimated_miner_fee() + self.rbf_fee() + self.destroyed_fee + self.sortition_fee } pub fn total_spent(&self) -> u64 { @@ -166,7 +176,7 @@ impl LeaderBlockCommitFees { } pub fn total_spent_in_outputs(&self) -> u64 { - self.sortition_fee + self.destroyed_fee + self.sortition_fee } pub fn min_tx_size(&self) -> u64 { @@ -607,6 +617,7 @@ impl BitcoinRegtestController { .expect("Public key incorrect"); let filter_addresses = vec![address.to_b58()]; + debug!("Get UTXOs for {}", &address); let mut utxos = loop { let result = BitcoinRPCRequest::list_unspent( &self.config, @@ -960,7 +971,7 @@ impl BitcoinRegtestController { }; let consensus_output = TxOut { - value: 0, + value: estimated_fees.destroyed_fee, script_pubkey: Builder::new() .push_opcode(opcodes::All::OP_RETURN) .push_slice(&op_bytes) @@ -1003,6 +1014,10 @@ impl BitcoinRegtestController { txids, }; + debug!( + "Miner node: Submitting tx {}", + &to_hex(&serialized_tx.bytes) + ); info!( "Miner node: submitting leader_block_commit (txid: {}, rbf: {}, total spent: {}, size: {}, fee_rate: {})", txid.to_hex(), @@ -1145,9 +1160,11 @@ impl BitcoinRegtestController { ) -> Option<(Transaction, UTXOSet)> { let utxos = if let Some(utxos) = utxos_to_include { // in RBF, you have to consume the same UTXOs + debug!("Using utxos_to_include: {:?}", &utxos); utxos } else { // Fetch some UTXOs + debug!("Fetching UTXOs for {}", &public_key.to_hex()); let utxos = match self.get_utxos(&public_key, total_required, utxos_to_exclude, block_height) { Some(utxos) => utxos, diff --git a/testnet/stacks-node/src/burnchains/mocknet_controller.rs b/testnet/stacks-node/src/burnchains/mocknet_controller.rs index 7dc0e5c2e3..d14e113df4 100644 --- a/testnet/stacks-node/src/burnchains/mocknet_controller.rs +++ b/testnet/stacks-node/src/burnchains/mocknet_controller.rs @@ -192,6 +192,7 @@ impl BurnchainController for MocknetController { key_vtxindex: payload.key_vtxindex, memo: payload.memo, burn_fee: payload.burn_fee, + destroyed: 0, apparent_sender: payload.apparent_sender, input: payload.input, commit_outs: payload.commit_outs, diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index 836d2149a2..98dffae04c 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -399,6 +399,7 @@ fn inner_generate_block_commit_op( parent_winning_vtx: u16, vrf_seed: VRFSeed, commit_outs: Vec, + destroyed: u64, current_burn_height: u64, ) -> BlockstackOperationType { let (parent_block_ptr, parent_vtxindex) = (parent_burnchain_height, parent_winning_vtx); @@ -408,6 +409,7 @@ fn inner_generate_block_commit_op( BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { block_header_hash, burn_fee, + destroyed, input: (Txid([0; 32]), 0), apparent_sender: sender, key_block_ptr: key.block_height as u32, @@ -2153,11 +2155,25 @@ impl StacksNode { vec![StacksAddress::burn_address(config.is_mainnet())] }; + let pox_cutoff = { + let reader_handle = burn_db.index_handle(&burn_block.sortition_id); + reader_handle + .get_pox_cutoff() + .expect("FATAL: failed to query sortition DB for PoX cutoff") + .unwrap_or(u64::MAX) + }; + + let (pox_out, destroyed) = if pox_cutoff < burn_fee_cap { + (pox_cutoff, burn_fee_cap - pox_cutoff) + } else { + (burn_fee_cap, 0) + }; + // let's commit let op = inner_generate_block_commit_op( keychain.get_burnchain_signer(), anchored_block.block_hash(), - burn_fee_cap, + pox_out, ®istered_key, parent_block_burn_height .try_into() @@ -2165,6 +2181,7 @@ impl StacksNode { parent_winning_vtxindex, VRFSeed::from_proof(&vrf_proof), commit_outs, + destroyed, burn_block.block_height, ); diff --git a/testnet/stacks-node/src/node.rs b/testnet/stacks-node/src/node.rs index f2258043bd..e013a30787 100644 --- a/testnet/stacks-node/src/node.rs +++ b/testnet/stacks-node/src/node.rs @@ -1013,6 +1013,7 @@ impl Node { BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { block_header_hash, burn_fee, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: self.keychain.get_burnchain_signer(), key_block_ptr: key.block_height as u32, diff --git a/testnet/stacks-node/src/tests/epoch_205.rs b/testnet/stacks-node/src/tests/epoch_205.rs index b111413141..bff14a1551 100644 --- a/testnet/stacks-node/src/tests/epoch_205.rs +++ b/testnet/stacks-node/src/tests/epoch_205.rs @@ -603,6 +603,7 @@ fn transition_empty_blocks() { let op = BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { block_header_hash: BlockHeaderHash([0xff; 32]), burn_fee: burn_fee_cap, + destroyed: 0, input: (Txid([0; 32]), 0), apparent_sender: keychain.get_burnchain_signer(), key_block_ptr, diff --git a/testnet/stacks-node/src/tests/epoch_21.rs b/testnet/stacks-node/src/tests/epoch_21.rs index e0e5a56b2d..55ad02ca01 100644 --- a/testnet/stacks-node/src/tests/epoch_21.rs +++ b/testnet/stacks-node/src/tests/epoch_21.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::collections::HashSet; use std::env; use std::thread; @@ -19,21 +20,36 @@ use crate::tests::neon_integrations::*; use crate::tests::*; use crate::BitcoinRegtestController; use crate::BurnchainController; +use crate::Keychain; + use stacks::core; use stacks::chainstate::burn::db::sortdb::SortitionDB; use stacks::chainstate::burn::distribution::BurnSamplePoint; +use stacks::chainstate::burn::operations::leader_block_commit::BURN_BLOCK_MINED_AT_MODULUS; +use stacks::chainstate::burn::operations::BlockstackOperationType; +use stacks::chainstate::burn::operations::LeaderBlockCommitOp; use stacks::burnchains::PoxConstants; +use stacks::burnchains::Txid; +use crate::stacks_common::types::chainstate::BlockHeaderHash; +use crate::stacks_common::types::chainstate::VRFSeed; use crate::stacks_common::types::Address; +use crate::stacks_common::util::hash::bytes_to_hex; use crate::stacks_common::util::hash::hex_bytes; use stacks_common::types::chainstate::BurnchainHeaderHash; +use stacks_common::util::hash::Hash160; use stacks_common::util::secp256k1::Secp256k1PublicKey; use stacks::chainstate::coordinator::comm::CoordinatorChannels; +use stacks::core::STACKS_EPOCH_2_1_MARKER; + +use clarity::vm::ClarityVersion; +use stacks::clarity_cli::vm_execute as execute; + fn advance_to_2_1( mut initial_balances: Vec, ) -> ( @@ -143,8 +159,6 @@ fn advance_to_2_1( // these should all succeed across the epoch 2.1 boundary for _i in 0..5 { - // also, make *huge* block-commits with invalid marker bytes once we reach the new - // epoch, and verify that it fails. let tip_info = get_chain_info(&conf); // this block is the epoch transition? @@ -456,3 +470,250 @@ fn transition_adds_burn_block_height() { test_observer::clear(); coord_channel.stop_chains_coordinator(); } + +#[test] +#[ignore] +fn transition_caps_discount_mining_upside() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let epoch_2_05 = 210; + let epoch_2_1 = 215; + + let spender_sk = StacksPrivateKey::new(); + let spender_addr = PrincipalData::from(to_addr(&spender_sk)); + let spender_addr_c32 = StacksAddress::from(to_addr(&spender_sk)); + + let pox_pubkey = Secp256k1PublicKey::from_hex( + "02f006a09b59979e2cb8449f58076152af6b124aa29b948a3714b8d5f15aa94ede", + ) + .unwrap(); + let pox_pubkey_hash160 = Hash160::from_node_public_key(&pox_pubkey); + let pox_pubkey_hash = bytes_to_hex(&pox_pubkey_hash160.to_bytes().to_vec()); + + test_observer::spawn(); + + let (mut conf, miner_account) = neon_integration_test_conf(); + let keychain = Keychain::default(conf.node.seed.clone()); + + conf.initial_balances = vec![InitialBalance { + address: spender_addr.clone(), + amount: 200_000_000_000_000, + }]; + conf.events_observers.push(EventObserverConfig { + endpoint: format!("localhost:{}", test_observer::EVENT_OBSERVER_PORT), + events_keys: vec![EventKeyType::AnyEvent], + }); + + let mut epochs = core::STACKS_EPOCHS_REGTEST.to_vec(); + epochs[1].end_height = epoch_2_05; + epochs[2].start_height = epoch_2_05; + epochs[2].end_height = epoch_2_1; + epochs[3].start_height = epoch_2_1; + + conf.burnchain.epochs = Some(epochs); + + let mut burnchain_config = Burnchain::regtest(&conf.get_burn_db_path()); + + let reward_cycle_len = 20; + let prepare_phase_len = 10; + let pox_constants = PoxConstants::new( + reward_cycle_len, + prepare_phase_len, + 4 * prepare_phase_len / 5, + 5, + 1, + 230, + ); + burnchain_config.pox_constants = pox_constants.clone(); + + let mut btcd_controller = BitcoinCoreController::new(conf.clone()); + btcd_controller + .start_bitcoind() + .map_err(|_e| ()) + .expect("Failed starting bitcoind"); + + let mut btc_regtest_controller = BitcoinRegtestController::with_burnchain( + conf.clone(), + None, + Some(burnchain_config.clone()), + None, + ); + let http_origin = format!("http://{}", &conf.node.rpc_bind); + + // give one coinbase to the miner, and then burn the rest + btc_regtest_controller.bootstrap_chain(1); + + let mining_pubkey = btc_regtest_controller.get_mining_pubkey().unwrap(); + btc_regtest_controller.set_mining_pubkey( + "03dc62fe0b8964d01fc9ca9a5eec0e22e557a12cc656919e648f04e0b26fea5faa".to_string(), + ); + + // bitcoin chain starts at epoch 2.05 boundary, minus 5 blocks to go + btc_regtest_controller.bootstrap_chain(epoch_2_05 - 6); + + // only one UTXO for our mining pubkey + let utxos = btc_regtest_controller + .get_all_utxos(&Secp256k1PublicKey::from_hex(&mining_pubkey).unwrap()); + assert_eq!(utxos.len(), 1); + + eprintln!("Chain bootstrapped..."); + + 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(); + + let runloop_burnchain = burnchain_config.clone(); + thread::spawn(move || run_loop.start(Some(runloop_burnchain), 0)); + + // give the run loop some time to start up! + wait_for_runloop(&blocks_processed); + + // first block wakes up the run loop + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + let tip_info = get_chain_info(&conf); + assert_eq!(tip_info.burn_block_height, epoch_2_05 - 4); + + // first block will hold our VRF registration + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + let key_block_ptr = tip_info.burn_block_height as u32; + let key_vtxindex = 1; // nothing else here but the coinbase + + // second block will be the first mined Stacks block + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + + // cross the epoch 2.05 boundary + for _i in 0..3 { + debug!("Burnchain block height is {}", tip_info.burn_block_height); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + } + + // cross the epoch 2.1 boundary + for _i in 0..5 { + debug!("Burnchain block height is {}", tip_info.burn_block_height); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + } + + // wait until the next reward cycle starts + let mut tip_info = get_chain_info(&conf); + while tip_info.burn_block_height < 221 { + debug!("Burnchain block height is {}", tip_info.burn_block_height); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + tip_info = get_chain_info(&conf); + } + + // stack all the tokens + let tx = make_contract_call( + &spender_sk, + 0, + 300, + &StacksAddress::from_string("ST000000000000000000002AMW42H").unwrap(), + "pox-2", + "stack-stx", + &[ + Value::UInt(200_000_000_000_000 - 1_000), + execute( + &format!("{{ hashbytes: 0x{}, version: 0x00 }}", pox_pubkey_hash), + ClarityVersion::Clarity2, + ) + .unwrap() + .unwrap(), + Value::UInt((tip_info.burn_block_height + 3).into()), + Value::UInt(12), + ], + ); + + submit_tx(&http_origin, &tx); + + // wait till next reward phase + while tip_info.burn_block_height < 241 { + debug!("Burnchain block height is {}", tip_info.burn_block_height); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + tip_info = get_chain_info(&conf); + } + + // verify stacking worked + let pox_info = get_pox_info(&http_origin); + + assert_eq!( + pox_info.current_cycle.stacked_ustx, + 200_000_000_000_000 - 1_000 + ); + assert_eq!(pox_info.current_cycle.is_pox_active, true); + + // try to send a PoX payout that over-spends + assert!(!burnchain_config.is_in_prepare_phase(tip_info.burn_block_height + 1)); + + let mut bitcoin_controller = BitcoinRegtestController::new_dummy(conf.clone()); + + // allow using 0-conf utxos + bitcoin_controller.set_allow_rbf(false); + let mut bad_commits = HashSet::new(); + + for i in 0..10 { + let burn_fee_cap = 100000000; // 1 BTC + let commit_outs = vec![ + StacksAddress { + version: C32_ADDRESS_VERSION_TESTNET_SINGLESIG, + bytes: pox_pubkey_hash160.clone(), + }, + StacksAddress { + version: C32_ADDRESS_VERSION_TESTNET_SINGLESIG, + bytes: pox_pubkey_hash160.clone(), + }, + ]; + + // let's commit an oversized commit + let burn_parent_modulus = (tip_info.burn_block_height % BURN_BLOCK_MINED_AT_MODULUS) as u8; + let block_header_hash = BlockHeaderHash([0xff - (i as u8); 32]); + + let op = BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp { + block_header_hash: block_header_hash.clone(), + burn_fee: burn_fee_cap, + destroyed: 0, + input: (Txid([0; 32]), 0), + apparent_sender: keychain.get_burnchain_signer(), + key_block_ptr, + key_vtxindex, + memo: vec![STACKS_EPOCH_2_1_MARKER], + new_seed: VRFSeed([0x11; 32]), + parent_block_ptr: tip_info.burn_block_height.try_into().unwrap(), + parent_vtxindex: 1, + // to be filled in + vtxindex: 0, + txid: Txid([0u8; 32]), + block_height: 0, + burn_header_hash: BurnchainHeaderHash::zero(), + burn_parent_modulus, + commit_outs, + }); + + let mut op_signer = keychain.generate_op_signer(); + let res = bitcoin_controller.submit_operation(op, &mut op_signer, 1); + assert!(res, "Failed to submit block-commit"); + + bad_commits.insert(block_header_hash); + next_block_and_wait(&mut btc_regtest_controller, &blocks_processed); + } + + // commit not even accepted + // (NOTE: there's no directly way to confirm here that it was rejected due to + // BlockCommitPoxOverpay) + + let sortdb = btc_regtest_controller.sortdb_mut(); + let all_snapshots = sortdb.get_all_snapshots().unwrap(); + + for i in (all_snapshots.len() - 2)..all_snapshots.len() { + let commits = + SortitionDB::get_block_commits_by_block(sortdb.conn(), &all_snapshots[i].sortition_id) + .unwrap(); + assert_eq!(commits.len(), 1); + assert!(!bad_commits.contains(&commits[0].block_header_hash)); + } + + test_observer::clear(); + channel.stop_chains_coordinator(); +} diff --git a/testnet/stacks-node/src/tests/neon_integrations.rs b/testnet/stacks-node/src/tests/neon_integrations.rs index 1b02b37583..3a30c22578 100644 --- a/testnet/stacks-node/src/tests/neon_integrations.rs +++ b/testnet/stacks-node/src/tests/neon_integrations.rs @@ -753,7 +753,7 @@ pub fn get_account(http_origin: &str, account: &F) -> Acco } } -fn get_pox_info(http_origin: &str) -> RPCPoxInfoData { +pub fn get_pox_info(http_origin: &str) -> RPCPoxInfoData { let client = reqwest::blocking::Client::new(); let path = format!("{}/v2/pox", http_origin); client @@ -1841,7 +1841,7 @@ fn microblock_integration_test() { // this microblock should correspond to `second_microblock` let microblock = microblock_events.pop().unwrap(); let transactions = microblock.get("transactions").unwrap().as_array().unwrap(); - assert_eq!(transactions.len(), 1); + assert!(transactions.len() >= 1); let tx_sequence = transactions[0] .get("microblock_sequence") .unwrap() @@ -1883,7 +1883,7 @@ fn microblock_integration_test() { // this microblock should correspond to the first microblock that was posted let microblock = microblock_events.pop().unwrap(); let transactions = microblock.get("transactions").unwrap().as_array().unwrap(); - assert_eq!(transactions.len(), 1); + assert!(transactions.len() >= 1); let tx_sequence = transactions[0] .get("microblock_sequence") .unwrap()