Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
- Creates epoch 3.3 and costs-4 in preparation for a hardfork to activate Clarity 4
- Adds support for new Clarity 4 builtins (not activated until epoch 3.3):
- `contract-hash?`
- `block-time`
- `to-ascii?`
- Added `contract_cost_limit_percentage` to the miner config file — sets the percentage of a block’s execution cost at which, if a large non-boot contract call would cause a BlockTooBigError, the miner will stop adding further non-boot contract calls and only include STX transfers and boot contract calls for the remainder of the block.

Expand Down
2 changes: 2 additions & 0 deletions clarity-types/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ pub enum RuntimeErrorType {
// pox-locking errors
DefunctPoxContract,
PoxAlreadyLocked,

BlockTimeNotAvailable,
}

#[derive(Debug, PartialEq)]
Expand Down
5 changes: 2 additions & 3 deletions clarity/src/vm/analysis/arithmetic_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,8 @@ impl ArithmeticOnlyChecker<'_> {
{
match native_var {
ContractCaller | TxSender | TotalLiquidMicroSTX | BlockHeight | BurnBlockHeight
| Regtest | TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight => {
Err(Error::VariableForbidden(native_var))
}
| Regtest | TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight
| BlockTime => Err(Error::VariableForbidden(native_var)),
NativeNone | NativeTrue | NativeFalse => Ok(()),
}
} else {
Expand Down
4 changes: 2 additions & 2 deletions clarity/src/vm/analysis/type_checker/v2_05/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,9 @@ fn type_reserved_variable(variable_name: &str) -> Result<Option<TypeSignature>,
NativeFalse => TypeSignature::BoolType,
TotalLiquidMicroSTX => TypeSignature::UIntType,
Regtest => TypeSignature::BoolType,
TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight => {
TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight | BlockTime => {
return Err(CheckErrors::Expects(
"tx-sponsor, mainnet, chain-id, stacks-block-height, and tenure-height should not reach here in 2.05".into(),
"tx-sponsor, mainnet, chain-id, stacks-block-height, tenure-height, and block-time should not reach here in 2.05".into(),
)
.into())
}
Expand Down
1 change: 1 addition & 0 deletions clarity/src/vm/analysis/type_checker/v2_1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,7 @@ fn type_reserved_variable(
Regtest => TypeSignature::BoolType,
Mainnet => TypeSignature::BoolType,
ChainId => TypeSignature::UIntType,
BlockTime => TypeSignature::UIntType,
};
Ok(Some(var_type))
} else {
Expand Down
23 changes: 23 additions & 0 deletions clarity/src/vm/database/clarity_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use crate::vm::types::{

pub const STORE_CONTRACT_SRC_INTERFACE: bool = true;
const TENURE_HEIGHT_KEY: &str = "_stx-data::tenure_height";
const CLARITY_STORAGE_BLOCK_TIME_KEY: &str = "_stx-data::clarity_storage::block_time";

pub type StacksEpoch = GenericStacksEpoch<ExecutionCost>;

Expand Down Expand Up @@ -885,6 +886,28 @@ impl<'a> ClarityDatabase<'a> {
self.put_data(Self::clarity_state_epoch_key(), &(epoch as u32))
}

/// Setup block metadata at the beginning of a block
/// This stores block-specific data that can be accessed during Clarity execution
pub fn setup_block_metadata(&mut self, block_time: Option<u64>) -> Result<()> {
let epoch = self.get_clarity_epoch_version()?;
if epoch.uses_marfed_block_time() {
let block_time = block_time.ok_or_else(|| {
InterpreterError::Expect(
"FATAL: Marfed block time not provided to Clarity DB setup".into(),
)
})?;
self.put_data(CLARITY_STORAGE_BLOCK_TIME_KEY, &block_time)?;
}
Ok(())
}

pub fn get_current_block_time(&mut self) -> Result<u64> {
match self.get_data(CLARITY_STORAGE_BLOCK_TIME_KEY)? {
Some(value) => Ok(value),
None => Err(RuntimeErrorType::BlockTimeNotAvailable.into()),
}
}

/// Returns the _current_ total liquid ustx
pub fn get_total_liquid_ustx(&mut self) -> Result<u128> {
let epoch = self.get_clarity_epoch_version()?;
Expand Down
11 changes: 11 additions & 0 deletions clarity/src/vm/docs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ At the start of epoch 3.0, `tenure-height` will return the same value as `block-
"(< tenure-height u140000) ;; returns true if the current tenure-height has passed 140,000 blocks.",
};

const BLOCK_TIME_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI {
name: "block-time",
snippet: "block-time",
output_type: "uint",
description: "Returns the Unix timestamp (in seconds) of the current Stacks block. Introduced
in Clarity 4. Provides access to the timestamp of the current block, which is
not available with `get-stacks-block-info?`.",
example: "(>= block-time u1755820800) ;; returns true if current block timestamp is at or after 2025-07-22.",
};

const TX_SENDER_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI {
name: "tx-sender",
snippet: "tx-sender",
Expand Down Expand Up @@ -2680,6 +2690,7 @@ pub fn make_keyword_reference(variable: &NativeVariables) -> Option<KeywordAPI>
NativeVariables::Mainnet => MAINNET_KEYWORD.clone(),
NativeVariables::ChainId => CHAINID_KEYWORD.clone(),
NativeVariables::TxSponsor => TX_SPONSOR_KEYWORD.clone(),
NativeVariables::BlockTime => BLOCK_TIME_KEYWORD.clone(),
};
Some(KeywordAPI {
name: keyword.name,
Expand Down
19 changes: 19 additions & 0 deletions clarity/src/vm/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ impl OwnedEnvironment<'_, '_> {
.unwrap();
self.context.database.commit().unwrap();
}

pub fn setup_block_metadata(&mut self, block_time: u64) {
self.context.database.begin();
self.context
.database
.setup_block_metadata(Some(block_time))
.unwrap();
self.context.database.commit().unwrap();
}
}

macro_rules! epochs_template {
Expand Down Expand Up @@ -191,6 +200,11 @@ impl MemoryEnvironmentGenerator {
db.set_tenure_height(1).unwrap();
db.commit().unwrap();
}
if epoch.uses_marfed_block_time() {
db.begin();
db.setup_block_metadata(Some(1)).unwrap();
db.commit().unwrap();
}
let mut owned_env = OwnedEnvironment::new(db, epoch);
// start an initial transaction.
owned_env.begin();
Expand All @@ -210,6 +224,11 @@ impl TopLevelMemoryEnvironmentGenerator {
db.set_tenure_height(1).unwrap();
db.commit().unwrap();
}
if epoch.uses_marfed_block_time() {
db.begin();
db.setup_block_metadata(Some(1)).unwrap();
db.commit().unwrap();
}
OwnedEnvironment::new(db, epoch)
}
}
Expand Down
111 changes: 111 additions & 0 deletions clarity/src/vm/tests/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1081,3 +1081,114 @@ fn reuse_tenure_height(
Value::Bool(true),
);
}

#[apply(test_clarity_versions)]
fn test_block_time(
version: ClarityVersion,
epoch: StacksEpochId,
mut tl_env_factory: TopLevelMemoryEnvironmentGenerator,
) {
let contract = "(define-read-only (test-func) block-time)";

let placeholder_context =
ContractContext::new(QualifiedContractIdentifier::transient(), version);

let mut owned_env = tl_env_factory.get_env(epoch);
let contract_identifier = QualifiedContractIdentifier::local("test-contract").unwrap();

let mut exprs = parse(&contract_identifier, contract, version, epoch).unwrap();
let mut marf = MemoryBackingStore::new();
let mut db = marf.as_analysis_db();
let analysis = db.execute(|db| {
type_check_version(&contract_identifier, &mut exprs, db, true, epoch, version)
});

// block-time should only be available in Clarity 4
if version < ClarityVersion::Clarity4 {
let err = analysis.unwrap_err();
assert_eq!(
CheckErrors::UndefinedVariable("block-time".to_string()),
*err.err
);
} else {
assert!(analysis.is_ok());
}

// Initialize the contract
// Note that we're ignoring the analysis failure here so that we can test
// the runtime behavior. In earlier versions, if this case somehow gets past the
// analysis, it should fail at runtime.
let result = owned_env.initialize_versioned_contract(
contract_identifier.clone(),
version,
contract,
None,
ASTRules::PrecheckSize,
);

let mut env = owned_env.get_exec_environment(None, None, &placeholder_context);

// Call the function
let eval_result = env.eval_read_only(&contract_identifier, "(test-func)");

// In versions before Clarity 4, this should trigger a runtime error
if version < ClarityVersion::Clarity4 {
let err = eval_result.unwrap_err();
assert_eq!(
Error::Unchecked(CheckErrors::UndefinedVariable("block-time".to_string(),)),
err
);
} else {
// Always 1 in the testing environment
assert_eq!(Ok(Value::UInt(1)), eval_result);
}
}

#[test]
fn test_block_time_in_expressions() {
let version = ClarityVersion::Clarity4;
let epoch = StacksEpochId::Epoch33;
let mut tl_env_factory = tl_env_factory();

let contract = r#"
(define-read-only (time-comparison (threshold uint))
(>= block-time threshold))
(define-read-only (time-arithmetic)
(+ block-time u100))
(define-read-only (time-in-response)
(ok block-time))
"#;

let placeholder_context =
ContractContext::new(QualifiedContractIdentifier::transient(), version);

let mut owned_env = tl_env_factory.get_env(epoch);
let contract_identifier = QualifiedContractIdentifier::local("test-contract").unwrap();

// Initialize the contract
let result = owned_env.initialize_versioned_contract(
contract_identifier.clone(),
version,
contract,
None,
ASTRules::PrecheckSize,
);
assert!(result.is_ok());

let mut env = owned_env.get_exec_environment(None, None, &placeholder_context);

// Test comparison: 1 >= 0 should be true
let eval_result = env.eval_read_only(&contract_identifier, "(time-comparison u0)");
info!("time-comparison result: {:?}", eval_result);
assert_eq!(Ok(Value::Bool(true)), eval_result);

// Test arithmetic: 1 + 100 = 101
let eval_result = env.eval_read_only(&contract_identifier, "(time-arithmetic)");
info!("time-arithmetic result: {:?}", eval_result);
assert_eq!(Ok(Value::UInt(101)), eval_result);

// Test in response: (ok 1)
let eval_result = env.eval_read_only(&contract_identifier, "(time-in-response)");
info!("time-in-response result: {:?}", eval_result);
assert_eq!(Ok(Value::okay(Value::UInt(1)).unwrap()), eval_result);
}
6 changes: 6 additions & 0 deletions clarity/src/vm/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ define_versioned_named_enum_with_max!(NativeVariables(ClarityVersion) {
ChainId("chain-id", ClarityVersion::Clarity2, None),
StacksBlockHeight("stacks-block-height", ClarityVersion::Clarity3, None),
TenureHeight("tenure-height", ClarityVersion::Clarity3, None),
BlockTime("block-time", ClarityVersion::Clarity4, None)
});

pub fn is_reserved_name(name: &str, version: &ClarityVersion) -> bool {
Expand Down Expand Up @@ -133,6 +134,11 @@ pub fn lookup_reserved_variable(
let tenure_height = env.global_context.database.get_tenure_height()?;
Ok(Some(Value::UInt(tenure_height as u128)))
}
NativeVariables::BlockTime => {
runtime_cost(ClarityCostFunction::FetchVar, env, 1)?;
let block_time = env.global_context.database.get_current_block_time()?;
Ok(Some(Value::UInt(u128::from(block_time))))
}
}
} else {
Ok(None)
Expand Down
17 changes: 17 additions & 0 deletions stacks-common/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,23 @@ impl StacksEpochId {
StacksEpochId::Epoch32 | StacksEpochId::Epoch33 => true,
}
}

pub fn uses_marfed_block_time(&self) -> bool {
match self {
StacksEpochId::Epoch10
| StacksEpochId::Epoch20
| StacksEpochId::Epoch2_05
| StacksEpochId::Epoch21
| StacksEpochId::Epoch22
| StacksEpochId::Epoch23
| StacksEpochId::Epoch24
| StacksEpochId::Epoch25
| StacksEpochId::Epoch30
| StacksEpochId::Epoch31
| StacksEpochId::Epoch32 => false,
StacksEpochId::Epoch33 => true,
}
}
}

impl std::fmt::Display for StacksEpochId {
Expand Down
Loading