Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions clarity-types/src/errors/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,9 @@ pub enum RuntimeCheckErrorKind {
InvalidCharactersDetected,
/// String contains invalid UTF-8 encoding.
InvalidUTF8Encoding,

/// `at-block` attempted to query a block outside the supported lookback window.
AtBlockOutOfLookbackWindow,
}

#[derive(Debug, PartialEq)]
Expand Down
53 changes: 52 additions & 1 deletion clarity/src/vm/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ use clarity_types::errors::{ParseError, ParseErrorKind};
use clarity_types::representations::ClarityName;
use serde::Serialize;
use serde_json::json;
use stacks_common::types::StacksEpochId;
use stacks_common::types::chainstate::StacksBlockId;
use stacks_common::types::{StacksEpochId, block_height_to_reward_cycle};

use super::EvalHook;
use crate::vm::ast::ContractAST;
Expand Down Expand Up @@ -1262,12 +1262,63 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
}
}

fn check_at_block_lookback_window(
&self,
block_id: &StacksBlockId,
) -> Result<(), VmExecutionError> {
let Some(lookback_cycles) = self
.global_context
.epoch_id
.at_block_lookback_reward_cycles()
else {
return Ok(());
};

let Some(target_burn_height) = self
.global_context
.database
.get_burnchain_block_height(block_id)
else {
// Preserve legacy behavior for unknown block ids.
return Ok(());
};

let current_burn_height = self
.global_context
.database
.get_tip_burnchain_block_height()?;
let first_burn_height = self.global_context.database.get_burn_start_height();
let reward_cycle_length = self.global_context.database.get_pox_reward_cycle_length();

let target_cycle = block_height_to_reward_cycle(
u64::from(target_burn_height),
u64::from(first_burn_height),
u64::from(reward_cycle_length),
)
.ok_or_else(|| VmInternalError::Expect("Invalid PoX reward cycle configuration".into()))?;
let current_cycle = block_height_to_reward_cycle(
u64::from(current_burn_height),
u64::from(first_burn_height),
u64::from(reward_cycle_length),
)
.ok_or_else(|| VmInternalError::Expect("Invalid PoX reward cycle configuration".into()))?;

let cycle_delta = current_cycle.saturating_sub(target_cycle);
if cycle_delta >= u64::from(lookback_cycles) {
return Err(RuntimeCheckErrorKind::AtBlockOutOfLookbackWindow.into());
}

Ok(())
}

pub fn evaluate_at_block(
&mut self,
bhh: StacksBlockId,
closure: &SymbolicExpression,
local: &LocalContext,
) -> Result<Value, VmExecutionError> {
self.check_at_block_lookback_window(&bhh)?;

self.global_context.begin_read_only();

let result = self
Expand Down
25 changes: 22 additions & 3 deletions clarity/src/vm/database/clarity_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,15 @@ impl BurnStateDB for NullBurnStateDB {
}

fn get_pox_prepare_length(&self) -> u32 {
panic!("NullBurnStateDB should not return PoX info");
1
}

fn get_pox_reward_cycle_length(&self) -> u32 {
panic!("NullBurnStateDB should not return PoX info");
1
}

fn get_pox_rejection_fraction(&self) -> u64 {
panic!("NullBurnStateDB should not return PoX info");
1
}
fn get_pox_payout_addrs(
&self,
Expand Down Expand Up @@ -1032,6 +1032,25 @@ impl ClarityDatabase<'_> {
self.store.get_current_block_height()
}

/// Return the first burnchain height tracked by this chain.
pub fn get_burn_start_height(&self) -> u32 {
self.burn_state_db.get_burn_start_height()
}

/// Return the configured PoX reward cycle length in burnchain blocks.
pub fn get_pox_reward_cycle_length(&self) -> u32 {
self.burn_state_db.get_pox_reward_cycle_length()
}

/// Get the current burnchain tip height.
pub fn get_tip_burnchain_block_height(&self) -> Result<u32, VmExecutionError> {
self.burn_state_db
.get_tip_burn_block_height()
.ok_or_else(|| {
VmInternalError::Expect("Failed to get burnchain tip height.".into()).into()
})
}

/// Return the height for PoX v1 -> v2 auto unlocks
/// from the burn state db
pub fn get_v1_unlock_height(&self) -> u32 {
Expand Down
18 changes: 18 additions & 0 deletions stacks-common/src/libcommon.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
// Copyright (C) 2026 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

#![allow(unused_macros)]
#![allow(dead_code)]
#![allow(non_camel_case_types)]
Expand Down Expand Up @@ -104,6 +119,9 @@ pub mod consts {

/// number of uSTX per STX
pub const MICROSTACKS_PER_STACKS: u32 = 1_000_000;

/// Maximum number of reward cycles to look back for `at-block` queries.
pub const AT_BLOCK_MAX_LOOKBACK_REWARD_CYCLES: u32 = 6;
}

pub mod versions {
Expand Down
34 changes: 29 additions & 5 deletions stacks-common/src/types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020-2024 Stacks Open Internet Foundation
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -30,10 +30,11 @@ use crate::address::{
C32_ADDRESS_VERSION_TESTNET_MULTISIG, C32_ADDRESS_VERSION_TESTNET_SINGLESIG,
};
use crate::consts::{
MICROSTACKS_PER_STACKS, PEER_VERSION_EPOCH_1_0, PEER_VERSION_EPOCH_2_0,
PEER_VERSION_EPOCH_2_05, PEER_VERSION_EPOCH_2_1, PEER_VERSION_EPOCH_2_2,
PEER_VERSION_EPOCH_2_3, PEER_VERSION_EPOCH_2_4, PEER_VERSION_EPOCH_2_5, PEER_VERSION_EPOCH_3_0,
PEER_VERSION_EPOCH_3_1, PEER_VERSION_EPOCH_3_2, PEER_VERSION_EPOCH_3_3, PEER_VERSION_EPOCH_3_4,
AT_BLOCK_MAX_LOOKBACK_REWARD_CYCLES, MICROSTACKS_PER_STACKS, PEER_VERSION_EPOCH_1_0,
PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05, PEER_VERSION_EPOCH_2_1,
PEER_VERSION_EPOCH_2_2, PEER_VERSION_EPOCH_2_3, PEER_VERSION_EPOCH_2_4, PEER_VERSION_EPOCH_2_5,
PEER_VERSION_EPOCH_3_0, PEER_VERSION_EPOCH_3_1, PEER_VERSION_EPOCH_3_2, PEER_VERSION_EPOCH_3_3,
PEER_VERSION_EPOCH_3_4,
};
use crate::types::chainstate::{StacksAddress, StacksPublicKey};
use crate::util::hash::Hash160;
Expand Down Expand Up @@ -457,6 +458,19 @@ impl SIP031EmissionInterval {
}
}

/// Returns the reward cycle at `block_ht` for a chain that begins at `first_block_ht`
/// with fixed reward cycles of length `reward_cycle_len`.
pub fn block_height_to_reward_cycle(
block_ht: u64,
first_block_ht: u64,
reward_cycle_len: u64,
) -> Option<u64> {
if reward_cycle_len == 0 || block_ht < first_block_ht {
return None;
}
Some((block_ht - first_block_ht) / reward_cycle_len)
}

impl StacksEpochId {
/// Highest epoch enabled in release builds.
/// Keep this in sync with `versions.toml` and `PEER_NETWORK_EPOCH`
Expand Down Expand Up @@ -673,6 +687,16 @@ impl StacksEpochId {
self >= &StacksEpochId::Epoch30
}

/// Return the lookback window (in reward cycles) for bounded `at-block`,
/// or `None` if bounded `at-block` is not active in this epoch.
pub fn at_block_lookback_reward_cycles(&self) -> Option<u32> {
if self >= &StacksEpochId::Epoch34 {
Some(AT_BLOCK_MAX_LOOKBACK_REWARD_CYCLES)
} else {
None
}
}

/// 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.
Expand Down
11 changes: 6 additions & 5 deletions stackslib/src/burnchains/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
// Copyright (C) 2020 Stacks Open Internet Foundation
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -661,10 +661,11 @@ impl PoxConstants {
first_block_ht: u64,
reward_cycle_len: u64,
) -> Option<u64> {
if block_ht < first_block_ht {
return None;
}
Some((block_ht - first_block_ht) / (reward_cycle_len))
stacks_common::types::block_height_to_reward_cycle(
block_ht,
first_block_ht,
reward_cycle_len,
)
}
}

Expand Down
Loading