Skip to content

Commit 307d088

Browse files
authored
Merge pull request #6478 from obycode/feat/block-time
[Clarity-4] Implement `block-time`
2 parents b1332a7 + f970554 commit 307d088

File tree

19 files changed

+767
-120
lines changed

19 files changed

+767
-120
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1616
- Creates epoch 3.3 and costs-4 in preparation for a hardfork to activate Clarity 4
1717
- Adds support for new Clarity 4 builtins (not activated until epoch 3.3):
1818
- `contract-hash?`
19+
- `block-time`
1920
- `to-ascii?`
2021
- 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.
2122

clarity-types/src/errors/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ pub enum RuntimeErrorType {
9999
// pox-locking errors
100100
DefunctPoxContract,
101101
PoxAlreadyLocked,
102+
103+
BlockTimeNotAvailable,
102104
}
103105

104106
#[derive(Debug, PartialEq)]

clarity/src/vm/analysis/arithmetic_checker/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,8 @@ impl ArithmeticOnlyChecker<'_> {
142142
{
143143
match native_var {
144144
ContractCaller | TxSender | TotalLiquidMicroSTX | BlockHeight | BurnBlockHeight
145-
| Regtest | TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight => {
146-
Err(Error::VariableForbidden(native_var))
147-
}
145+
| Regtest | TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight
146+
| BlockTime => Err(Error::VariableForbidden(native_var)),
148147
NativeNone | NativeTrue | NativeFalse => Ok(()),
149148
}
150149
} else {

clarity/src/vm/analysis/type_checker/v2_05/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,9 +330,9 @@ fn type_reserved_variable(variable_name: &str) -> Result<Option<TypeSignature>,
330330
NativeFalse => TypeSignature::BoolType,
331331
TotalLiquidMicroSTX => TypeSignature::UIntType,
332332
Regtest => TypeSignature::BoolType,
333-
TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight => {
333+
TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight | BlockTime => {
334334
return Err(CheckErrors::Expects(
335-
"tx-sponsor, mainnet, chain-id, stacks-block-height, and tenure-height should not reach here in 2.05".into(),
335+
"tx-sponsor, mainnet, chain-id, stacks-block-height, tenure-height, and block-time should not reach here in 2.05".into(),
336336
)
337337
.into())
338338
}

clarity/src/vm/analysis/type_checker/v2_1/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,7 @@ fn type_reserved_variable(
10221022
Regtest => TypeSignature::BoolType,
10231023
Mainnet => TypeSignature::BoolType,
10241024
ChainId => TypeSignature::UIntType,
1025+
BlockTime => TypeSignature::UIntType,
10251026
};
10261027
Ok(Some(var_type))
10271028
} else {

clarity/src/vm/database/clarity_db.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ use crate::vm::types::{
4949

5050
pub const STORE_CONTRACT_SRC_INTERFACE: bool = true;
5151
const TENURE_HEIGHT_KEY: &str = "_stx-data::tenure_height";
52+
const CLARITY_STORAGE_BLOCK_TIME_KEY: &str = "_stx-data::clarity_storage::block_time";
5253

5354
pub type StacksEpoch = GenericStacksEpoch<ExecutionCost>;
5455

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

889+
/// Setup block metadata at the beginning of a block
890+
/// This stores block-specific data that can be accessed during Clarity execution
891+
pub fn setup_block_metadata(&mut self, block_time: Option<u64>) -> Result<()> {
892+
let epoch = self.get_clarity_epoch_version()?;
893+
if epoch.uses_marfed_block_time() {
894+
let block_time = block_time.ok_or_else(|| {
895+
InterpreterError::Expect(
896+
"FATAL: Marfed block time not provided to Clarity DB setup".into(),
897+
)
898+
})?;
899+
self.put_data(CLARITY_STORAGE_BLOCK_TIME_KEY, &block_time)?;
900+
}
901+
Ok(())
902+
}
903+
904+
pub fn get_current_block_time(&mut self) -> Result<u64> {
905+
match self.get_data(CLARITY_STORAGE_BLOCK_TIME_KEY)? {
906+
Some(value) => Ok(value),
907+
None => Err(RuntimeErrorType::BlockTimeNotAvailable.into()),
908+
}
909+
}
910+
888911
/// Returns the _current_ total liquid ustx
889912
pub fn get_total_liquid_ustx(&mut self) -> Result<u128> {
890913
let epoch = self.get_clarity_epoch_version()?;

clarity/src/vm/docs/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ At the start of epoch 3.0, `tenure-height` will return the same value as `block-
144144
"(< tenure-height u140000) ;; returns true if the current tenure-height has passed 140,000 blocks.",
145145
};
146146

147+
const BLOCK_TIME_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI {
148+
name: "block-time",
149+
snippet: "block-time",
150+
output_type: "uint",
151+
description: "Returns the Unix timestamp (in seconds) of the current Stacks block. Introduced
152+
in Clarity 4. Provides access to the timestamp of the current block, which is
153+
not available with `get-stacks-block-info?`.",
154+
example: "(>= block-time u1755820800) ;; returns true if current block timestamp is at or after 2025-07-22.",
155+
};
156+
147157
const TX_SENDER_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI {
148158
name: "tx-sender",
149159
snippet: "tx-sender",
@@ -2680,6 +2690,7 @@ pub fn make_keyword_reference(variable: &NativeVariables) -> Option<KeywordAPI>
26802690
NativeVariables::Mainnet => MAINNET_KEYWORD.clone(),
26812691
NativeVariables::ChainId => CHAINID_KEYWORD.clone(),
26822692
NativeVariables::TxSponsor => TX_SPONSOR_KEYWORD.clone(),
2693+
NativeVariables::BlockTime => BLOCK_TIME_KEYWORD.clone(),
26832694
};
26842695
Some(KeywordAPI {
26852696
name: keyword.name,

clarity/src/vm/tests/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ impl OwnedEnvironment<'_, '_> {
4949
.unwrap();
5050
self.context.database.commit().unwrap();
5151
}
52+
53+
pub fn setup_block_metadata(&mut self, block_time: u64) {
54+
self.context.database.begin();
55+
self.context
56+
.database
57+
.setup_block_metadata(Some(block_time))
58+
.unwrap();
59+
self.context.database.commit().unwrap();
60+
}
5261
}
5362

5463
macro_rules! epochs_template {
@@ -191,6 +200,11 @@ impl MemoryEnvironmentGenerator {
191200
db.set_tenure_height(1).unwrap();
192201
db.commit().unwrap();
193202
}
203+
if epoch.uses_marfed_block_time() {
204+
db.begin();
205+
db.setup_block_metadata(Some(1)).unwrap();
206+
db.commit().unwrap();
207+
}
194208
let mut owned_env = OwnedEnvironment::new(db, epoch);
195209
// start an initial transaction.
196210
owned_env.begin();
@@ -210,6 +224,11 @@ impl TopLevelMemoryEnvironmentGenerator {
210224
db.set_tenure_height(1).unwrap();
211225
db.commit().unwrap();
212226
}
227+
if epoch.uses_marfed_block_time() {
228+
db.begin();
229+
db.setup_block_metadata(Some(1)).unwrap();
230+
db.commit().unwrap();
231+
}
213232
OwnedEnvironment::new(db, epoch)
214233
}
215234
}

clarity/src/vm/tests/variables.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,3 +1081,114 @@ fn reuse_tenure_height(
10811081
Value::Bool(true),
10821082
);
10831083
}
1084+
1085+
#[apply(test_clarity_versions)]
1086+
fn test_block_time(
1087+
version: ClarityVersion,
1088+
epoch: StacksEpochId,
1089+
mut tl_env_factory: TopLevelMemoryEnvironmentGenerator,
1090+
) {
1091+
let contract = "(define-read-only (test-func) block-time)";
1092+
1093+
let placeholder_context =
1094+
ContractContext::new(QualifiedContractIdentifier::transient(), version);
1095+
1096+
let mut owned_env = tl_env_factory.get_env(epoch);
1097+
let contract_identifier = QualifiedContractIdentifier::local("test-contract").unwrap();
1098+
1099+
let mut exprs = parse(&contract_identifier, contract, version, epoch).unwrap();
1100+
let mut marf = MemoryBackingStore::new();
1101+
let mut db = marf.as_analysis_db();
1102+
let analysis = db.execute(|db| {
1103+
type_check_version(&contract_identifier, &mut exprs, db, true, epoch, version)
1104+
});
1105+
1106+
// block-time should only be available in Clarity 4
1107+
if version < ClarityVersion::Clarity4 {
1108+
let err = analysis.unwrap_err();
1109+
assert_eq!(
1110+
CheckErrors::UndefinedVariable("block-time".to_string()),
1111+
*err.err
1112+
);
1113+
} else {
1114+
assert!(analysis.is_ok());
1115+
}
1116+
1117+
// Initialize the contract
1118+
// Note that we're ignoring the analysis failure here so that we can test
1119+
// the runtime behavior. In earlier versions, if this case somehow gets past the
1120+
// analysis, it should fail at runtime.
1121+
let result = owned_env.initialize_versioned_contract(
1122+
contract_identifier.clone(),
1123+
version,
1124+
contract,
1125+
None,
1126+
ASTRules::PrecheckSize,
1127+
);
1128+
1129+
let mut env = owned_env.get_exec_environment(None, None, &placeholder_context);
1130+
1131+
// Call the function
1132+
let eval_result = env.eval_read_only(&contract_identifier, "(test-func)");
1133+
1134+
// In versions before Clarity 4, this should trigger a runtime error
1135+
if version < ClarityVersion::Clarity4 {
1136+
let err = eval_result.unwrap_err();
1137+
assert_eq!(
1138+
Error::Unchecked(CheckErrors::UndefinedVariable("block-time".to_string(),)),
1139+
err
1140+
);
1141+
} else {
1142+
// Always 1 in the testing environment
1143+
assert_eq!(Ok(Value::UInt(1)), eval_result);
1144+
}
1145+
}
1146+
1147+
#[test]
1148+
fn test_block_time_in_expressions() {
1149+
let version = ClarityVersion::Clarity4;
1150+
let epoch = StacksEpochId::Epoch33;
1151+
let mut tl_env_factory = tl_env_factory();
1152+
1153+
let contract = r#"
1154+
(define-read-only (time-comparison (threshold uint))
1155+
(>= block-time threshold))
1156+
(define-read-only (time-arithmetic)
1157+
(+ block-time u100))
1158+
(define-read-only (time-in-response)
1159+
(ok block-time))
1160+
"#;
1161+
1162+
let placeholder_context =
1163+
ContractContext::new(QualifiedContractIdentifier::transient(), version);
1164+
1165+
let mut owned_env = tl_env_factory.get_env(epoch);
1166+
let contract_identifier = QualifiedContractIdentifier::local("test-contract").unwrap();
1167+
1168+
// Initialize the contract
1169+
let result = owned_env.initialize_versioned_contract(
1170+
contract_identifier.clone(),
1171+
version,
1172+
contract,
1173+
None,
1174+
ASTRules::PrecheckSize,
1175+
);
1176+
assert!(result.is_ok());
1177+
1178+
let mut env = owned_env.get_exec_environment(None, None, &placeholder_context);
1179+
1180+
// Test comparison: 1 >= 0 should be true
1181+
let eval_result = env.eval_read_only(&contract_identifier, "(time-comparison u0)");
1182+
info!("time-comparison result: {:?}", eval_result);
1183+
assert_eq!(Ok(Value::Bool(true)), eval_result);
1184+
1185+
// Test arithmetic: 1 + 100 = 101
1186+
let eval_result = env.eval_read_only(&contract_identifier, "(time-arithmetic)");
1187+
info!("time-arithmetic result: {:?}", eval_result);
1188+
assert_eq!(Ok(Value::UInt(101)), eval_result);
1189+
1190+
// Test in response: (ok 1)
1191+
let eval_result = env.eval_read_only(&contract_identifier, "(time-in-response)");
1192+
info!("time-in-response result: {:?}", eval_result);
1193+
assert_eq!(Ok(Value::okay(Value::UInt(1)).unwrap()), eval_result);
1194+
}

clarity/src/vm/variables.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ define_versioned_named_enum_with_max!(NativeVariables(ClarityVersion) {
3939
ChainId("chain-id", ClarityVersion::Clarity2, None),
4040
StacksBlockHeight("stacks-block-height", ClarityVersion::Clarity3, None),
4141
TenureHeight("tenure-height", ClarityVersion::Clarity3, None),
42+
BlockTime("block-time", ClarityVersion::Clarity4, None)
4243
});
4344

4445
pub fn is_reserved_name(name: &str, version: &ClarityVersion) -> bool {
@@ -133,6 +134,11 @@ pub fn lookup_reserved_variable(
133134
let tenure_height = env.global_context.database.get_tenure_height()?;
134135
Ok(Some(Value::UInt(tenure_height as u128)))
135136
}
137+
NativeVariables::BlockTime => {
138+
runtime_cost(ClarityCostFunction::FetchVar, env, 1)?;
139+
let block_time = env.global_context.database.get_current_block_time()?;
140+
Ok(Some(Value::UInt(u128::from(block_time))))
141+
}
136142
}
137143
} else {
138144
Ok(None)

0 commit comments

Comments
 (0)