diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index 5595905a4..d4c5bafe9 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -148,7 +148,7 @@ impl<'a> ArithmeticOnlyChecker<'a> { { match native_var { ContractCaller | TxSender | TotalLiquidMicroSTX | BlockHeight | BurnBlockHeight - | Regtest | TxSponsor | Mainnet | ChainId => { + | Regtest | TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight => { Err(Error::VariableForbidden(native_var)) } NativeNone | NativeTrue | NativeFalse => Ok(()), diff --git a/clarity/src/vm/analysis/type_checker/v2_05/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/mod.rs index d66cad5d4..2b913a3ac 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/mod.rs @@ -323,9 +323,9 @@ fn type_reserved_variable(variable_name: &str) -> CheckResult TypeSignature::BoolType, TotalLiquidMicroSTX => TypeSignature::UIntType, Regtest => TypeSignature::BoolType, - TxSponsor | Mainnet | ChainId => { + TxSponsor | Mainnet | ChainId | StacksBlockHeight | TenureHeight => { return Err(CheckErrors::Expects( - "tx-sponsor, mainnet, and chain-id should not reach here in 2.05".into(), + "tx-sponsor, mainnet, chain-id, stacks-block-height, and tenure-height should not reach here in 2.05".into(), ) .into()) } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/contexts.rs b/clarity/src/vm/analysis/type_checker/v2_1/contexts.rs index 8cbed1a41..cc72fb91d 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/contexts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/contexts.rs @@ -42,7 +42,7 @@ impl TraitContext { pub fn new(clarity_version: ClarityVersion) -> TraitContext { match clarity_version { ClarityVersion::Clarity1 => Self::Clarity1(HashMap::new()), - ClarityVersion::Clarity2 => Self::Clarity2 { + ClarityVersion::Clarity2 | ClarityVersion::Clarity3 => Self::Clarity2 { defined: HashSet::new(), all: HashMap::new(), }, diff --git a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs index b61d3bb6e..e8300c0f1 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs @@ -858,6 +858,8 @@ fn type_reserved_variable( .map_err(|_| CheckErrors::Expects("Bad construction".into()))?, ContractCaller => TypeSignature::PrincipalType, BlockHeight => TypeSignature::UIntType, + StacksBlockHeight => TypeSignature::UIntType, + TenureHeight => TypeSignature::UIntType, BurnBlockHeight => TypeSignature::UIntType, NativeNone => TypeSignature::new_option(no_type()) .map_err(|_| CheckErrors::Expects("Bad construction".into()))?, diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs index 99942ba42..09389337e 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs @@ -2740,7 +2740,9 @@ fn clarity_trait_experiments_downcast_literal_2( }) .unwrap_err(); match version { - ClarityVersion::Clarity2 => assert!(err.starts_with("ExpectedCallableType(PrincipalType)")), + ClarityVersion::Clarity2 | ClarityVersion::Clarity3 => { + assert!(err.starts_with("ExpectedCallableType(PrincipalType)")) + } ClarityVersion::Clarity1 => { assert!(err.starts_with("TraitReferenceUnknown(\"principal-value\")")) } @@ -2935,7 +2937,9 @@ fn clarity_trait_experiments_trait_cast_incompatible( assert!(err.starts_with("TypeError(CallableType(Trait(TraitIdentifier")) } } - ClarityVersion::Clarity2 => assert!(err.starts_with("IncompatibleTrait")), + ClarityVersion::Clarity2 | ClarityVersion::Clarity3 => { + assert!(err.starts_with("IncompatibleTrait")) + } } } diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 0f8be80aa..940b2f2f6 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -126,18 +126,16 @@ to the same contract principal.", example: "(print contract-caller) ;; Will print out a Stacks address of the transaction sender", }; -const STACKS_BLOCK_HEIGHT: KeywordAPI = KeywordAPI { +const STACKS_BLOCK_HEIGHT_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { name: "stacks-block-height", snippet: "stacks-block-height", output_type: "uint", description: "Returns the current block height of the Stacks blockchain.", example: "(<= stacks-block-height u500000) ;; returns true if the current block-height has not passed 500,000 blocks.", - min_version: ClarityVersion::Clarity3, - max_version: None, }; -const TENURE_HEIGHT_KEYWORD: KeywordAPI = KeywordAPI { +const TENURE_HEIGHT_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { name: "tenure-height", snippet: "tenure-height", output_type: "uint", @@ -145,8 +143,6 @@ const TENURE_HEIGHT_KEYWORD: KeywordAPI = KeywordAPI { At the start of epoch 3.0, `tenure-height` will return the same value as `block-height`, then it will continue to increase as each tenures passes.", example: "(< tenure-height u140000) ;; returns true if the current tenure-height has passed 140,000 blocks.", - min_version: ClarityVersion::Clarity3, - max_version: None, }; const TX_SENDER_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { @@ -2568,13 +2564,15 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { } fn make_keyword_reference(variable: &NativeVariables) -> Option { - let simple_api = match variable { + let keyword = match variable { NativeVariables::TxSender => TX_SENDER_KEYWORD.clone(), NativeVariables::ContractCaller => CONTRACT_CALLER_KEYWORD.clone(), NativeVariables::NativeNone => NONE_KEYWORD.clone(), NativeVariables::NativeTrue => TRUE_KEYWORD.clone(), NativeVariables::NativeFalse => FALSE_KEYWORD.clone(), NativeVariables::BlockHeight => BLOCK_HEIGHT.clone(), + NativeVariables::StacksBlockHeight => STACKS_BLOCK_HEIGHT_KEYWORD.clone(), + NativeVariables::TenureHeight => TENURE_HEIGHT_KEYWORD.clone(), NativeVariables::BurnBlockHeight => BURN_BLOCK_HEIGHT.clone(), NativeVariables::TotalLiquidMicroSTX => TOTAL_LIQUID_USTX_KEYWORD.clone(), NativeVariables::Regtest => REGTEST_KEYWORD.clone(), @@ -2583,11 +2581,11 @@ fn make_keyword_reference(variable: &NativeVariables) -> Option { NativeVariables::TxSponsor => TX_SPONSOR_KEYWORD.clone(), }; Some(KeywordAPI { - name: simple_api.name, - snippet: simple_api.snippet, - output_type: simple_api.output_type, - description: simple_api.description, - example: simple_api.example, + name: keyword.name, + snippet: keyword.snippet, + output_type: keyword.output_type, + description: keyword.description, + example: keyword.example, min_version: variable.get_min_version(), max_version: variable.get_max_version(), }) diff --git a/clarity/src/vm/tests/mod.rs b/clarity/src/vm/tests/mod.rs index c73db0cda..19c5e5af1 100644 --- a/clarity/src/vm/tests/mod.rs +++ b/clarity/src/vm/tests/mod.rs @@ -33,6 +33,7 @@ mod sequences; #[cfg(test)] mod simple_apply_eval; mod traits; +mod variables; macro_rules! epochs_template { ($($epoch:ident,)*) => { @@ -50,7 +51,6 @@ macro_rules! epochs_template { match epoch { // don't test Epoch-1.0 StacksEpochId::Epoch10 => (), - StacksEpochId::Epoch30 => (), // this will lead to a compile time failure if an epoch is left out // of the epochs_template! macro list $(StacksEpochId::$epoch)|* => (), @@ -76,10 +76,16 @@ macro_rules! clarity_template { match (epoch, clarity) { // don't test Epoch-1.0 (StacksEpochId::Epoch10, _) => (), - (StacksEpochId::Epoch30, _) => (), // don't test these pairs, because they aren't supported: (StacksEpochId::Epoch20, ClarityVersion::Clarity2) => (), (StacksEpochId::Epoch2_05, ClarityVersion::Clarity2) => (), + (StacksEpochId::Epoch20, ClarityVersion::Clarity3) => (), + (StacksEpochId::Epoch2_05, ClarityVersion::Clarity3) => (), + (StacksEpochId::Epoch21, ClarityVersion::Clarity3) => (), + (StacksEpochId::Epoch22, ClarityVersion::Clarity3) => (), + (StacksEpochId::Epoch23, ClarityVersion::Clarity3) => (), + (StacksEpochId::Epoch24, ClarityVersion::Clarity3) => (), + (StacksEpochId::Epoch25, ClarityVersion::Clarity3) => (), // this will lead to a compile time failure if a pair is left out // of the clarity_template! macro list $((StacksEpochId::$epoch, ClarityVersion::$clarity))|* => (), @@ -103,6 +109,7 @@ epochs_template! { Epoch23, Epoch24, Epoch25, + Epoch30, } clarity_template! { @@ -118,6 +125,9 @@ clarity_template! { (Epoch24, Clarity2), (Epoch25, Clarity1), (Epoch25, Clarity2), + (Epoch30, Clarity1), + (Epoch30, Clarity2), + (Epoch30, Clarity3), } #[cfg(test)] diff --git a/clarity/src/vm/tests/variables.rs b/clarity/src/vm/tests/variables.rs new file mode 100644 index 000000000..4c940830e --- /dev/null +++ b/clarity/src/vm/tests/variables.rs @@ -0,0 +1,147 @@ +// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation +// Copyright (C) 2020-2024 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 . + +#[cfg(any(test, feature = "testing"))] +use rstest::rstest; +use stacks_common::types::StacksEpochId; + +#[cfg(test)] +use crate::vm::analysis::type_checker::v2_1::tests::contracts::type_check_version; +use crate::vm::analysis::{run_analysis, CheckError}; +use crate::vm::ast::{parse, ASTRules}; +use crate::vm::database::MemoryBackingStore; +use crate::vm::errors::{CheckErrors, Error}; +use crate::vm::tests::{test_clarity_versions, tl_env_factory, TopLevelMemoryEnvironmentGenerator}; +use crate::vm::types::{QualifiedContractIdentifier, Value}; +use crate::vm::{ClarityVersion, ContractContext}; + +#[apply(test_clarity_versions)] +fn test_block_height( + version: ClarityVersion, + epoch: StacksEpochId, + mut tl_env_factory: TopLevelMemoryEnvironmentGenerator, +) { + let contract = "(define-read-only (test-func) block-height)"; + + let mut 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) + }); + if version >= ClarityVersion::Clarity3 { + let err = analysis.unwrap_err(); + assert_eq!( + CheckErrors::UndefinedVariable("block-height".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 Clarity 3, 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, &mut placeholder_context); + + // Call the function + let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + // In Clarity 3, this should trigger a runtime error + if version >= ClarityVersion::Clarity3 { + let err = eval_result.unwrap_err(); + assert_eq!( + Error::Unchecked(CheckErrors::UndefinedVariable("block-height".to_string(),)), + err + ); + } else { + assert_eq!(Ok(Value::UInt(1)), eval_result); + } +} + +#[apply(test_clarity_versions)] +fn test_stacks_block_height( + version: ClarityVersion, + epoch: StacksEpochId, + mut tl_env_factory: TopLevelMemoryEnvironmentGenerator, +) { + let contract = "(define-read-only (test-func) stacks-block-height)"; + + let mut 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) + }); + if version < ClarityVersion::Clarity3 { + let err = analysis.unwrap_err(); + assert_eq!( + CheckErrors::UndefinedVariable("stacks-block-height".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 Clarity 3, 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, &mut placeholder_context); + + // Call the function + let eval_result = env.eval_read_only(&contract_identifier, "(test-func)"); + // In Clarity 3, this should trigger a runtime error + if version < ClarityVersion::Clarity3 { + let err = eval_result.unwrap_err(); + assert_eq!( + Error::Unchecked(CheckErrors::UndefinedVariable( + "stacks-block-height".to_string(), + )), + err + ); + } else { + assert_eq!(Ok(Value::UInt(1)), eval_result); + } +} diff --git a/clarity/src/vm/variables.rs b/clarity/src/vm/variables.rs index 28ea90a04..36dbe5199 100644 --- a/clarity/src/vm/variables.rs +++ b/clarity/src/vm/variables.rs @@ -97,6 +97,7 @@ pub fn lookup_reserved_variable( Ok(Some(sponsor)) } NativeVariables::BlockHeight => { + // FIXME: this needs to be updated to epoch 3.0 vs epoch 2.x runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; let block_height = env.global_context.database.get_current_block_height(); Ok(Some(Value::UInt(block_height as u128))) @@ -136,6 +137,7 @@ pub fn lookup_reserved_variable( } NativeVariables::TenureHeight => { runtime_cost(ClarityCostFunction::FetchVar, env, 1)?; + // FIXME: this is a placeholder and needs to be implemented correctly let burn_block_height = env .global_context .database