Skip to content

Commit

Permalink
Merge pull request #4840 from stacks-network/fix/4826
Browse files Browse the repository at this point in the history
Fix/4826
  • Loading branch information
jcnelson authored Jun 5, 2024
2 parents 690701f + 50ea4dc commit fe8ba12
Show file tree
Hide file tree
Showing 26 changed files with 675 additions and 423 deletions.
22 changes: 22 additions & 0 deletions stacks-common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,28 @@ impl StacksEpochId {
StacksEpochId::Epoch30 => MINING_COMMITMENT_FREQUENCY_NAKAMOTO,
}
}

/// Does this epoch use the nakamoto reward set, or the epoch2 reward set?
/// We use the epoch2 reward set in all pre-3.0 epochs.
/// We also use the epoch2 reward set in the first 3.0 reward cycle.
/// After that, we use the nakamoto reward set.
pub fn uses_nakamoto_reward_set(
&self,
cur_reward_cycle: u64,
first_epoch30_reward_cycle: u64,
) -> bool {
match self {
StacksEpochId::Epoch10
| StacksEpochId::Epoch20
| StacksEpochId::Epoch2_05
| StacksEpochId::Epoch21
| StacksEpochId::Epoch22
| StacksEpochId::Epoch23
| StacksEpochId::Epoch24
| StacksEpochId::Epoch25 => false,
StacksEpochId::Epoch30 => cur_reward_cycle > first_epoch30_reward_cycle,
}
}
}

impl std::fmt::Display for StacksEpochId {
Expand Down
16 changes: 9 additions & 7 deletions stackslib/src/burnchains/burnchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,16 +551,18 @@ impl Burnchain {
.reward_cycle_to_block_height(self.first_block_height, reward_cycle)
}

pub fn next_reward_cycle(&self, block_height: u64) -> Option<u64> {
/// Compute the reward cycle ID of the PoX reward set which is active as of this burn_height.
/// The reward set is calculated at reward cycle index 1, so if this block height is at or after
/// reward cycle index 1, then this behaves like `block_height_to_reward_cycle()`. However,
/// if it's reward cycle index is 0, then it belongs to the previous reward cycle.
pub fn pox_reward_cycle(&self, block_height: u64) -> Option<u64> {
let cycle = self.block_height_to_reward_cycle(block_height)?;
let effective_height = block_height.checked_sub(self.first_block_height)?;
let next_bump = if effective_height % u64::from(self.pox_constants.reward_cycle_length) == 0
{
0
if effective_height % u64::from(self.pox_constants.reward_cycle_length) == 0 {
Some(cycle.saturating_sub(1))
} else {
1
};
Some(cycle + next_bump)
Some(cycle)
}
}

pub fn block_height_to_reward_cycle(&self, block_height: u64) -> Option<u64> {
Expand Down
231 changes: 73 additions & 158 deletions stackslib/src/chainstate/burn/db/sortdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ use stacks_common::util::hash::{hex_bytes, to_hex, Hash160, Sha512Trunc256Sum};
use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PublicKey};
use stacks_common::util::vrf::*;
use stacks_common::util::{get_epoch_time_secs, log};
use wsts::common::Signature as WSTSSignature;
use wsts::curve::point::{Compressed, Point};

use crate::burnchains::affirmation::{AffirmationMap, AffirmationMapEntry};
use crate::burnchains::bitcoin::BitcoinNetworkType;
Expand Down Expand Up @@ -1860,80 +1858,6 @@ impl<'a> SortitionHandleConn<'a> {
SortitionHandleConn::open_reader(connection, &sn.sortition_id)
}

/// Does the sortition db expect to receive blocks
/// signed by this signer set?
///
/// This only works if `consensus_hash` is within two reward cycles (4200 blocks) of the
/// sortition pointed to by this handle's sortiton tip. If it isn't, then this
/// method returns Ok(false). This is to prevent a DDoS vector whereby compromised stale
/// Signer keys can be used to blast out lots of Nakamoto blocks that will be accepted
/// but never processed. So, `consensus_hash` can be in the same reward cycle as
/// `self.context.chain_tip`, or the previous, but no earlier.
pub fn expects_signer_signature(
&self,
consensus_hash: &ConsensusHash,
signer_signature: &WSTSSignature,
message: &[u8],
aggregate_public_key: &Point,
) -> Result<bool, db_error> {
let sn = SortitionDB::get_block_snapshot(self, &self.context.chain_tip)?
.ok_or(db_error::NotFoundError)
.map_err(|e| {
warn!("No sortition for tip: {:?}", &self.context.chain_tip);
e
})?;

let ch_sn = SortitionDB::get_block_snapshot_consensus(self, consensus_hash)?
.ok_or(db_error::NotFoundError)
.map_err(|e| {
warn!("No sortition for consensus hash: {:?}", consensus_hash);
e
})?;

if ch_sn.block_height
+ u64::from(self.context.pox_constants.reward_cycle_length)
+ u64::from(self.context.pox_constants.prepare_length)
< sn.block_height
{
// too far in the past
debug!("Block with consensus hash {} is too far in the past", consensus_hash;
"consensus_hash" => %consensus_hash,
"block_height" => ch_sn.block_height,
"tip_block_height" => sn.block_height
);
return Ok(false);
}

// this given consensus hash must be an ancestor of our chain tip
let ch_at = self
.get_consensus_at(ch_sn.block_height)?
.ok_or(db_error::NotFoundError)
.map_err(|e| {
warn!("No ancestor consensus hash";
"tip" => %self.context.chain_tip,
"consensus_hash" => %consensus_hash,
"consensus_hash height" => %ch_sn.block_height
);
e
})?;

if ch_at != ch_sn.consensus_hash {
// not an ancestor
warn!("Consensus hash is not an ancestor of the sortition tip";
"tip" => %self.context.chain_tip,
"consensus_hash" => %consensus_hash
);
return Err(db_error::NotFoundError);
}

// is this consensus hash in this fork?
if SortitionDB::get_burnchain_header_hash_by_consensus(self, consensus_hash)?.is_none() {
return Ok(false);
}

Ok(signer_signature.verify(aggregate_public_key, message))
}

pub fn get_reward_set_size_at(&self, sortition_id: &SortitionId) -> Result<u16, db_error> {
self.get_indexed(sortition_id, &db_keys::pox_reward_set_size())
.map(|x| {
Expand Down Expand Up @@ -1984,32 +1908,6 @@ impl<'a> SortitionHandleConn<'a> {
Ok(anchor_block_txid)
}

/// Get the last processed reward cycle.
/// Since we always process a RewardSetInfo at the start of a reward cycle (anchor block or
/// no), this is simply the same as asking which reward cycle this SortitionHandleConn's
/// sortition tip is in.
pub fn get_last_processed_reward_cycle(&self) -> Result<u64, db_error> {
let sn = SortitionDB::get_block_snapshot(self, &self.context.chain_tip)?
.ok_or(db_error::NotFoundError)?;
let rc = self
.context
.pox_constants
.block_height_to_reward_cycle(self.context.first_block_height, sn.block_height)
.expect("FATAL: sortition from before system start");
let rc_start_block = self
.context
.pox_constants
.reward_cycle_to_block_height(self.context.first_block_height, rc);
let last_rc = if sn.block_height >= rc_start_block {
rc
} else {
// NOTE: the reward cycle is "processed" at reward cycle index 1, not index 0
rc.saturating_sub(1)
};

Ok(last_rc)
}

pub fn get_reward_cycle_unlocks(
&mut self,
cycle: u64,
Expand Down Expand Up @@ -3314,11 +3212,18 @@ impl SortitionDB {
) -> Result<(), db_error> {
let pox_constants = self.pox_constants.clone();
for rc in 0..=(canonical_tip.block_height / u64::from(pox_constants.reward_cycle_length)) {
if pox_constants.reward_cycle_to_block_height(self.first_block_height, rc)
> canonical_tip.block_height
{
let rc_start = pox_constants.reward_cycle_to_block_height(self.first_block_height, rc);
if rc_start > canonical_tip.block_height {
break;
}
let epoch_at_height = SortitionDB::get_stacks_epoch(self.conn(), rc_start)?
.unwrap_or_else(|| panic!("FATAL: no epoch defined for burn height {}", rc_start))
.epoch_id;

if epoch_at_height >= StacksEpochId::Epoch30 {
break;
}

info!("Regenerating reward set for cycle {}", &rc);
migrator.regenerate_reward_cycle_info(self, rc)?;
}
Expand Down Expand Up @@ -3535,21 +3440,45 @@ impl SortitionDB {
}

/// Store a pre-processed reward set.
/// `sortition_id` is the first sortition ID of the prepare phase
/// `sortition_id` is the first sortition ID of the prepare phase.
/// No-op if the reward set has a selected-and-unknown anchor block.
pub fn store_preprocessed_reward_set(
sort_tx: &mut DBTx,
sortition_id: &SortitionId,
rc_info: &RewardCycleInfo,
) -> Result<(), db_error> {
if !rc_info.is_reward_info_known() {
return Ok(());
}
let sql = "REPLACE INTO preprocessed_reward_sets (sortition_id,reward_set) VALUES (?1,?2)";
let rc_json = serde_json::to_string(rc_info).map_err(db_error::SerializationError)?;
let args: &[&dyn ToSql] = &[sortition_id, &rc_json];
let args = rusqlite::params![sortition_id, &rc_json];
sort_tx.execute(sql, args)?;
Ok(())
}

/// Get the prepare phase end sortition ID of a reward cycle. This is the last prepare
/// phase sortition for the prepare phase that began this reward cycle (i.e. the returned
/// sortition will be in the preceding reward cycle)
/// Wrapper around SortitionDBConn::get_prepare_phase_end_sortition_id_for_reward_ccyle()
pub fn get_prepare_phase_end_sortition_id_for_reward_cycle(
&self,
tip: &SortitionId,
reward_cycle_id: u64,
) -> Result<SortitionId, db_error> {
self.index_conn()
.get_prepare_phase_end_sortition_id_for_reward_cycle(
&self.pox_constants,
self.first_block_height,
tip,
reward_cycle_id,
)
}

/// Get the prepare phase start sortition ID of a reward cycle. This is the first prepare
/// phase sortition for the prepare phase that began this reward cycle (i.e. the returned
/// sortition will be in the preceding reward cycle)
/// Wrapper around SortitionDBConn::get_prepare_phase_start_sortition_id_for_reward_cycle().
/// See that method for details.
pub fn get_prepare_phase_start_sortition_id_for_reward_cycle(
&self,
tip: &SortitionId,
Expand All @@ -3564,8 +3493,11 @@ impl SortitionDB {
)
}

/// Figure out the reward cycle for `tip` and lookup the preprocessed
/// reward set (if it exists) for the active reward cycle during `tip`.
/// Returns the reward cycle info on success.
/// Returns Error on DB errors, as well as if the reward set is not yet processed.
/// Wrapper around SortitionDBConn::get_preprocessed_reward_set_for_reward_cycle().
/// See that method for details.
pub fn get_preprocessed_reward_set_for_reward_cycle(
&self,
tip: &SortitionId,
Expand All @@ -3580,8 +3512,11 @@ impl SortitionDB {
)
}

/// Figure out the reward cycle for `tip` and lookup the preprocessed
/// reward set (if it exists) for the active reward cycle during `tip`.
/// Returns the reward cycle info on success.
/// Returns Error on DB errors, as well as if the reward set is not yet processed.
/// Wrapper around SortitionDBConn::get_preprocessed_reward_set_of().
/// See that method for details.
pub fn get_preprocessed_reward_set_of(
&self,
tip: &SortitionId,
Expand Down Expand Up @@ -3859,12 +3794,8 @@ impl<'a> SortitionDBConn<'a> {
db_error::NotFoundError
})?;

// NOTE: the .saturating_sub(1) is necessary because the reward set is calculated in epoch
// 2.5 and lower at reward cycle index 1, not 0. This correction ensures that the last
// block is checked against the signers who were active just before the new reward set is
// calculated.
let reward_cycle_id = pox_constants
.block_height_to_reward_cycle(first_block_height, tip_sn.block_height.saturating_sub(1))
.block_height_to_reward_cycle(first_block_height, tip_sn.block_height)
.expect("FATAL: stored snapshot with block height < first_block_height");

self.get_preprocessed_reward_set_for_reward_cycle(
Expand All @@ -3876,6 +3807,33 @@ impl<'a> SortitionDBConn<'a> {
.and_then(|(reward_cycle_info, _anchor_sortition_id)| Ok(reward_cycle_info))
}

/// Get the prepare phase end sortition ID of a reward cycle. This is the last prepare
/// phase sortition for the prepare phase that began this reward cycle (i.e. the returned
/// sortition will be in the preceding reward cycle)
pub fn get_prepare_phase_end_sortition_id_for_reward_cycle(
&self,
pox_constants: &PoxConstants,
first_block_height: u64,
tip: &SortitionId,
reward_cycle_id: u64,
) -> Result<SortitionId, db_error> {
let prepare_phase_end = pox_constants
.reward_cycle_to_block_height(first_block_height, reward_cycle_id)
.saturating_sub(1);

let last_sortition =
get_ancestor_sort_id(self, prepare_phase_end, tip)?.ok_or_else(|| {
error!(
"Could not find prepare phase end ancestor while fetching reward set";
"tip_sortition_id" => %tip,
"reward_cycle_id" => reward_cycle_id,
"prepare_phase_end_height" => prepare_phase_end
);
db_error::NotFoundError
})?;
Ok(last_sortition)
}

/// Get the prepare phase start sortition ID of a reward cycle. This is the first prepare
/// phase sortition for the prepare phase that began this reward cycle (i.e. the returned
/// sortition will be in the preceding reward cycle)
Expand Down Expand Up @@ -6101,16 +6059,6 @@ impl<'a> SortitionHandleTx<'a> {
keys.push(db_keys::pox_affirmation_map().to_string());
values.push(cur_affirmation_map.encode());

if cfg!(test) {
// last reward cycle.
// NOTE: We keep this only for testing, since this is what the original (but
// unmigratable code) did, and we need to verify that the compatibility fix to
// SortitionDB::get_last_processed_reward_cycle() is semantically compatible
// with querying this key.
keys.push(db_keys::last_reward_cycle_key().to_string());
values.push(db_keys::last_reward_cycle_to_string(_reward_cycle));
}

pox_payout_addrs
} else {
// if this snapshot consumed some reward set entries AND
Expand Down Expand Up @@ -6193,15 +6141,6 @@ impl<'a> SortitionHandleTx<'a> {
keys.push(db_keys::pox_last_selected_anchor_txid().to_string());
values.push("".to_string());

if cfg!(test) {
// NOTE: We keep this only for testing, since this is what the original (but
// unmigratable code) did, and we need to verify that the compatibility fix to
// SortitionDB::get_last_processed_reward_cycle() is semantically compatible
// with querying this key.
keys.push(db_keys::last_reward_cycle_key().to_string());
values.push(db_keys::last_reward_cycle_to_string(0));
}

// no payouts
vec![]
};
Expand Down Expand Up @@ -6543,30 +6482,6 @@ pub mod tests {
use crate::core::{StacksEpochExtension, *};
use crate::util_lib::db::Error as db_error;

impl<'a> SortitionHandleConn<'a> {
/// At one point in the development lifecycle, this code depended on a MARF key/value
/// pair to map the sortition tip to the last-processed reward cycle number. This data would
/// not have been present in epoch 2.4 chainstate and earlier, but would have been present in
/// epoch 2.5 and later, since at the time it was expected that all nodes would perform a
/// genesis sync when booting into epoch 2.5. However, that requirement changed at the last
/// minute, so this code was reworked to avoid the need for the MARF key. But to ensure that
/// this method is semantically consistent with the old code (which the Nakamoto chains
/// coordinator depends on), this code will test that the new reward cycle calculation matches
/// the old reward cycle calculation.
#[cfg(test)]
pub fn legacy_get_last_processed_reward_cycle(&self) -> Result<u64, db_error> {
// verify that this is semantically compatible with the older behavior, which shipped
// for epoch 2.5 but needed to be removed at the last minute in order to support a
// migration path from 2.4 chainstate to 2.5/3.0 chainstate.
let encoded_rc = self
.get_indexed(&self.context.chain_tip, &db_keys::last_reward_cycle_key())?
.expect("FATAL: no last-processed reward cycle");

let expected_rc = db_keys::last_reward_cycle_from_string(&encoded_rc);
Ok(expected_rc)
}
}

impl<'a> SortitionHandleTx<'a> {
/// Update the canonical Stacks tip (testing only)
pub fn test_update_canonical_stacks_tip(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ impl LeaderBlockCommitOp {
check_recipients.sort();
let mut commit_outs = self.commit_outs.clone();
commit_outs.sort();
for (expected_commit, found_commit) in
for (found_commit, expected_commit) in
commit_outs.iter().zip(check_recipients)
{
if expected_commit.to_burnchain_repr()
Expand Down
Loading

0 comments on commit fe8ba12

Please sign in to comment.