Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1eb48de
Add support for pre naka blocks
jacinta-stacks Oct 27, 2025
59c48ea
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Oct 27, 2025
0d2e5fa
Set lower block limits
jacinta-stacks Oct 27, 2025
9f8c670
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Oct 28, 2025
ed30abc
Generate Epoch List on the Fly
jacinta-stacks Oct 28, 2025
eafad79
Fix Clarity versions per Epoch and update contract_call_consensus_tes…
jacinta-stacks Oct 28, 2025
3650058
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Oct 28, 2025
7500516
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Oct 30, 2025
13659d1
CRC: check for epoch at deployment time to enable better clarity vers…
jacinta-stacks Oct 30, 2025
d7c728a
CRC: rename some functions and improve their scope
jacinta-stacks Oct 30, 2025
84306cd
CRC: remove coinbase txs from epoch receipt for pre nakamoto tenures
jacinta-stacks Oct 30, 2025
c37946e
CRC: fix duplicate calls and cleanup ContractConsensusTest struct
jacinta-stacks Nov 1, 2025
76edefd
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Nov 1, 2025
74e94fc
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Nov 3, 2025
14e13e2
Automatically create StacksEpochId::ALL from varients to prevent manu…
jacinta-stacks Nov 3, 2025
0e71860
Move functions around for easier understanding
jacinta-stacks Nov 3, 2025
6069c36
Split ConsensusTest's chain logic out to underlying ConsensusChain an…
jacinta-stacks Nov 3, 2025
0a44f6c
Create network_epoch helper function
jacinta-stacks Nov 3, 2025
779315b
Fmt
jacinta-stacks Nov 3, 2025
a7c544d
CRC: use the prepare phase start to determine if Epoch 2.5 or Epoch 3…
jacinta-stacks Nov 3, 2025
3ea9d35
CRC: use saturating sub in load_nakamoto_reward_set to ensure 0th cyc…
jacinta-stacks Nov 3, 2025
381398d
Move asserts about nakamoto boot state to a helper function that can …
jacinta-stacks Nov 3, 2025
e4e5ab6
Cannot use prepare phase length to determine reward set calculation
jacinta-stacks Nov 3, 2025
7fec308
CRC: cleanup comments and move epoch assertions into build_nakamoto_c…
jacinta-stacks Nov 4, 2025
6da8514
Cleanup assert_valid_epoch_3_0_activation and rename to validate_naka…
jacinta-stacks Nov 4, 2025
395fe95
Fix tests that have reward cycle starting on boundary. Still need to …
jacinta-stacks Nov 5, 2025
1ea1f63
Merge branch 'develop' of https://github.com/stacks-network/stacks-co…
jacinta-stacks Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion stacks-common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,14 @@ impl StacksEpochId {
StacksEpochId::Epoch33
}

pub const ALL_GTE_30: &'static [StacksEpochId] = &[
pub const ALL_GTE_20: &'static [StacksEpochId] = &[
StacksEpochId::Epoch20,
StacksEpochId::Epoch2_05,
StacksEpochId::Epoch21,
StacksEpochId::Epoch22,
StacksEpochId::Epoch23,
StacksEpochId::Epoch24,
StacksEpochId::Epoch25,
StacksEpochId::Epoch30,
StacksEpochId::Epoch31,
StacksEpochId::Epoch32,
Expand Down
10 changes: 10 additions & 0 deletions stackslib/src/chainstate/coordinator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,16 @@ impl<
Ok(None)
}

/// A helper function for exposing the private process_new_pox_anchor_test function
#[cfg(test)]
pub fn process_new_pox_anchor_test(
&mut self,
block_id: BlockHeaderHash,
already_processed_burn_blocks: &mut HashSet<BurnchainHeaderHash>,
) -> Result<Option<BlockHeaderHash>, Error> {
self.process_new_pox_anchor(block_id, already_processed_burn_blocks)
}

/// Process a new PoX anchor block, possibly resulting in the PoX history being unwound and
/// replayed through a different sequence of consensus hashes. If the new anchor block causes
/// the node to reach a prepare-phase that elects a network-affirmed anchor block that we don't
Expand Down
41 changes: 20 additions & 21 deletions stackslib/src/chainstate/nakamoto/coordinator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,28 +363,27 @@ pub fn load_nakamoto_reward_set<U: RewardSetProvider>(
provider: &U,
) -> Result<Option<(RewardCycleInfo, StacksHeaderInfo)>, Error> {
let cycle_start_height = burnchain.nakamoto_first_block_of_cycle(reward_cycle);

let epoch_at_height = SortitionDB::get_stacks_epoch(sort_db.conn(), cycle_start_height)?
.unwrap_or_else(|| {
panic!(
"FATAL: no epoch defined for burn height {}",
cycle_start_height
)
});

// Find the first Stacks block in this reward cycle's preceding prepare phase.
// This block will have invoked `.signers.stackerdb-set-signer-slots()` with the reward set.
// Note that we may not have processed it yet. But, if we do find it, then it's
// unique (and since Nakamoto Stacks blocks are processed in order, the anchor block
// cannot change later).
let first_epoch30_reward_cycle = burnchain
.block_height_to_reward_cycle(epoch_at_height.start_height)
.expect("FATAL: no reward cycle for epoch 3.0 start height");

if !epoch_at_height
.epoch_id
.uses_nakamoto_reward_set(reward_cycle, first_epoch30_reward_cycle)
{
.unwrap_or_else(|| panic!("FATAL: no epoch defined for burn height {cycle_start_height}"));
let is_pre_naka_epoch = if epoch_at_height.epoch_id < StacksEpochId::Epoch30 {
true
} else {
let epoch_30 =
SortitionDB::get_stacks_epoch_by_epoch_id(sort_db.conn(), &StacksEpochId::Epoch30)?
.unwrap_or_else(|| panic!("FATAL: no Nakamoto epoch defined"));
// Find the first Stacks block in this reward cycle's preceding prepare phase.
// This block will have invoked `.signers.stackerdb-set-signer-slots()` with the reward set.
// Note that we may not have processed it yet. But, if we do find it, then it's
// unique (and since Nakamoto Stacks blocks are processed in order, the anchor block
// cannot change later).
let first_epoch30_reward_cycle = burnchain
.block_height_to_reward_cycle(epoch_30.start_height)
.expect("FATAL: no reward cycle for epoch 3.0 start height");
!epoch_at_height
.epoch_id
.uses_nakamoto_reward_set(reward_cycle, first_epoch30_reward_cycle)
};
if is_pre_naka_epoch {
Comment on lines +367 to +386

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure that this check could just be simplified to something like:

// if this reward cycle started before 3.0, we need to lookup the
// reward set using 2.x rules.
if epoch_at_height.epoch_id < StacksEpochId::Epoch3_0 {

Copy link
Contributor Author

@jacinta-stacks jacinta-stacks Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not use the pre-naka reward set calculation in the FIRST epoch 3.0 reward set? Specifically see this comment:

    /// 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
            | StacksEpochId::Epoch31
            | StacksEpochId::Epoch32
            | StacksEpochId::Epoch33 => cur_reward_cycle > first_epoch30_reward_cycle,
        }
    }

Copy link

@aaronb-stacks aaronb-stacks Nov 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep -- but that would be the case because the epoch_at_height variable corresponds to the active epoch of the reward cycle's start block: in the first cycle of Epoch 3.0's case, that block is in 2.5.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is necessarily true though no? I mean the epoch at height 3.0 activation...is epoch 3.0. However...its prepare phase actually happens in Epoch 2.5 meaning by epoch 2.5 reward set calculation standards. So that means it would need to use this specialty function, no?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh -- yes, this is kind of weird.

What matters here is the active epoch during the reward cycle calculations. These actually occur at different times in 2.x and 3.x. In 2.x rules, the reward cycle is calculated at the reward cycle start height. In 3.x rules, its calculated during the prepare phase. This one of the reasons that 3.0 shouldn't start exactly on a reward cycle boundary.

The reason that checking the start height of the reward cycle "works" if the 3.0 epoch doesn't start at a reward cycle boundary is basically the following:

          | --------------------------------|--------------------|--------------------->
reward cycle N starts                   Epoch 3.0       reward cycle N+1 starts
                                          starts

Because Epoch 3.0 doesn't start at the reward cycle boundary, it's first cycle (N) has a start height which is in Epoch 2.5. That's why 2.x rules need to be used to calculate that reward cycle.

Now, if Epoch 3.0 actually starts on a reward cycle boundary, that's when things go topsy-turvy. But I think that should just be forbidden? Or alternatively, rather than using the cycle start height to check for the epoch, we could use the prepare-phase start height of the cycle (i.e., cycle start height - prepare length)

Copy link
Contributor Author

@jacinta-stacks jacinta-stacks Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very hard for me to grok this but I think I understand XD I am not sure where to enforce Epoch 3.0 not starting on a boundary. I enforce it in my ConsensusTest and I mean I suppose since Epoch 3.0 has already started no place in mainnet to enforce. Looking into PoxConstants definition and where it is used with Epoch list to see if I can enforce that Epoch 3.0 never starts on a reward cycle boundary in all tests cause I assume this could cause confusion to a test writer. EDIT: done prepare phase change in a7c544d

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm this broke some unit tests. Taking a look now.

Copy link
Contributor Author

@jacinta-stacks jacinta-stacks Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted. So this broke a few tests because a few of them seem to activate Epoch 3.0 and Epoch 2.5 in the same reward cycle.

For e.g. test pox_treatment

reward_cycle_length = 5
prepare_length = 3

Epoch 2.5 activates at 0 and ends at 39
Epoch 3.0 activates at 39 and ends at MAX.

Therefore we have one nakamoto tenure at block height 39 which uses the pre computed reward set for reward cycle 3. But then a new nakmaoto tenure starts at 40 which is now in reward cycle 4. But it no longer is the first Epoch 3.0 reward cycle BUT its prepare phase does occur in Epoch 2.5...so it goes to find the preprocessed reward set using Epoch 2.5 rules for reward cycle 4. However, it never processed this using Epoch 2.5 rules because it already had a reward cycle with Epoch 3.0 activated therefore it uses Epoch 3.0 rules. It therefore fails to find the pre processed reward set and should really be using Epoch 3.0 rules.

// in epoch 2.5, and in the first reward cycle of epoch 3.0, the reward set can *only* be found in the sortition DB.
// The nakamoto chain-processing rules aren't active yet, so we can't look for the reward
// cycle info in the nakamoto chain state.
Expand Down
69 changes: 69 additions & 0 deletions stackslib/src/chainstate/nakamoto/tests/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,75 @@ impl TestStacksNode {
Ok((block, size, cost))
}

/// Insert a staging pre-Nakamoto block and microblocks
/// then process them as the next ready block
/// NOTE: Will panic if called with unprocessed staging
/// blocks already in the queue.
pub fn process_pre_nakamoto_next_ready_block<'a>(
stacks_node: &mut TestStacksNode,
sortdb: &mut SortitionDB,
miner: &mut TestMiner,
tenure_id_consensus_hash: &ConsensusHash,
coord: &mut ChainsCoordinator<
'a,
TestEventObserver,
(),
OnChainRewardSetProvider<'a, TestEventObserver>,
(),
(),
BitcoinIndexer,
>,
block: &StacksBlock,
microblocks: &[StacksMicroblock],
) -> Result<Option<StacksEpochReceipt>, ChainstateError> {
// First append the block to the staging blocks
{
let ic = sortdb.index_conn();
let tip = SortitionDB::get_canonical_burn_chain_tip(&ic).unwrap();
stacks_node
.chainstate
.preprocess_stacks_epoch(&ic, &tip, block, microblocks)
.unwrap();
}

let canonical_sortition_tip = coord.canonical_sortition_tip.clone().expect(
"FAIL: processing a new Stacks block, but don't have a canonical sortition tip",
);
let mut sort_tx = sortdb.tx_begin_at_tip();
let res = stacks_node
.chainstate
.process_next_staging_block(&mut sort_tx, coord.dispatcher)
.map(|(epoch_receipt, _)| epoch_receipt)?;
sort_tx.commit()?;
if let Some(block_receipt) = res.as_ref() {
let in_sortition_set = coord
.sortition_db
.is_stacks_block_in_sortition_set(
&canonical_sortition_tip,
&block_receipt.header.anchored_header.block_hash(),
)
.unwrap();
if in_sortition_set {
let block_hash = block_receipt.header.anchored_header.block_hash();
// Was this block sufficiently confirmed by the prepare phase that it was a PoX
// anchor block? And if we're in epoch 2.1, does it match the heaviest-confirmed
// block-commit in the burnchain DB, and is it affirmed by the majority of the
// network?
if let Some(pox_anchor) = coord
.sortition_db
.is_stacks_block_pox_anchor(&block_hash, &canonical_sortition_tip)
.unwrap()
{
debug!("Discovered PoX anchor block {block_hash} off of canonical sortition tip {canonical_sortition_tip}");
coord
.process_new_pox_anchor_test(pox_anchor, &mut HashSet::new())
.unwrap();
}
}
}
Ok(res)
}

/// Insert a staging Nakamoto block as a pushed block and
/// then process it as the next ready block
/// NOTE: Will panic if called with unprocessed staging
Expand Down
Loading
Loading