From 336547093137bd5c19bce9b59b2e25897dff9f6d Mon Sep 17 00:00:00 2001 From: Hugo C <911307+hugocaillard@users.noreply.github.com> Date: Thu, 27 Jun 2024 17:22:39 +0200 Subject: [PATCH] feat: update clarity and support clarity 3 (#1484) * feat: support clarity 3 * tests: add clarity 2 tests * test: fix test * tests: add some clarit3 support tests * refactor: simplify interpreter tests * refactor: move session test to the interpreter * refactor: remove dead code --- .cargo/{config => config.toml} | 0 Cargo.lock | 12 +- .../clarinet-cli/src/generate/project.rs | 1 - .../clarity-repl/src/analysis/ast_visitor.rs | 53 +++ components/clarity-repl/src/repl/datastore.rs | 73 +++- .../clarity-repl/src/repl/interpreter.rs | 393 ++++++++++++------ components/clarity-repl/src/repl/session.rs | 98 ++++- 7 files changed, 455 insertions(+), 175 deletions(-) rename .cargo/{config => config.toml} (100%) diff --git a/.cargo/config b/.cargo/config.toml similarity index 100% rename from .cargo/config rename to .cargo/config.toml diff --git a/Cargo.lock b/Cargo.lock index c49b3f6f0..1848afb55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -798,7 +798,7 @@ checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "clar2wasm" version = "0.1.0" -source = "git+https://github.com/stacks-network/clarity-wasm.git?branch=chore/update-clarity#683470d54b39ede7ae83d03e3270c2897a1e11b2" +source = "git+https://github.com/stacks-network/clarity-wasm.git?branch=chore/update-clarity#ffbf49befc00d6eb3230a8e44ebf91b621f074e4" dependencies = [ "chrono", "clap", @@ -949,7 +949,7 @@ dependencies = [ [[package]] name = "clarity" version = "2.3.0" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#379d8cebddf4c073a37346690978bca28d130cb9" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#860d7da5af0a46b07d24cb29aa425247cf2b6820" dependencies = [ "getrandom 0.2.8", "hashbrown 0.14.3", @@ -2863,7 +2863,7 @@ dependencies = [ [[package]] name = "libstackerdb" version = "0.0.1" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#379d8cebddf4c073a37346690978bca28d130cb9" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#860d7da5af0a46b07d24cb29aa425247cf2b6820" dependencies = [ "clarity", "secp256k1 0.24.3", @@ -3594,7 +3594,7 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "pox-locking" version = "2.4.0" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#379d8cebddf4c073a37346690978bca28d130cb9" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#860d7da5af0a46b07d24cb29aa425247cf2b6820" dependencies = [ "clarity", "slog", @@ -4882,7 +4882,7 @@ dependencies = [ [[package]] name = "stacks-common" version = "0.0.2" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#379d8cebddf4c073a37346690978bca28d130cb9" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#860d7da5af0a46b07d24cb29aa425247cf2b6820" dependencies = [ "chrono", "curve25519-dalek 2.0.0", @@ -4985,7 +4985,7 @@ dependencies = [ [[package]] name = "stackslib" version = "0.0.1" -source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#379d8cebddf4c073a37346690978bca28d130cb9" +source = "git+https://github.com/stacks-network/stacks-core.git?branch=feat/clarity-wasm-develop#860d7da5af0a46b07d24cb29aa425247cf2b6820" dependencies = [ "chrono", "clar2wasm", diff --git a/components/clarinet-cli/src/generate/project.rs b/components/clarinet-cli/src/generate/project.rs index 8bb1c84ea..056c81808 100644 --- a/components/clarinet-cli/src/generate/project.rs +++ b/components/clarinet-cli/src/generate/project.rs @@ -24,7 +24,6 @@ impl GetChangesForNewProject { use_current_dir: bool, telemetry_enabled: bool, ) -> Self { - println!("project_path: {project_path}, project_name: {project_name}"); let project_path = if use_current_dir { project_path.clone() } else { diff --git a/components/clarity-repl/src/analysis/ast_visitor.rs b/components/clarity-repl/src/analysis/ast_visitor.rs index 9f46c852f..240decb78 100644 --- a/components/clarity-repl/src/analysis/ast_visitor.rs +++ b/components/clarity-repl/src/analysis/ast_visitor.rs @@ -412,6 +412,23 @@ pub trait ASTVisitor<'a> { .unwrap_or(&DEFAULT_NAME), args.get(1).unwrap_or(&DEFAULT_EXPR), ), + GetStacksBlockInfo => self.traverse_get_stacks_block_info( + expr, + args.get(0) + .unwrap_or(&DEFAULT_EXPR) + .match_atom() + .unwrap_or(&DEFAULT_NAME), + args.get(1).unwrap_or(&DEFAULT_EXPR), + ), + GetTenureInfo => self.traverse_get_tenure_info( + expr, + args.get(0) + .unwrap_or(&DEFAULT_EXPR) + .match_atom() + .unwrap_or(&DEFAULT_NAME), + args.get(1).unwrap_or(&DEFAULT_EXPR), + ), + ConsError => self.traverse_err(expr, args.get(0).unwrap_or(&DEFAULT_EXPR)), ConsOkay => self.traverse_ok(expr, args.get(0).unwrap_or(&DEFAULT_EXPR)), ConsSome => self.traverse_some(expr, args.get(0).unwrap_or(&DEFAULT_EXPR)), @@ -1465,6 +1482,42 @@ pub trait ASTVisitor<'a> { true } + fn traverse_get_stacks_block_info( + &mut self, + expr: &'a SymbolicExpression, + prop_name: &'a ClarityName, + block: &'a SymbolicExpression, + ) -> bool { + self.traverse_expr(block) && self.visit_get_stacks_block_info(expr, prop_name, block) + } + + fn visit_get_stacks_block_info( + &mut self, + expr: &'a SymbolicExpression, + prop_name: &'a ClarityName, + block: &'a SymbolicExpression, + ) -> bool { + true + } + + fn traverse_get_tenure_info( + &mut self, + expr: &'a SymbolicExpression, + prop_name: &'a ClarityName, + block: &'a SymbolicExpression, + ) -> bool { + self.traverse_expr(block) && self.visit_get_tenure_info(expr, prop_name, block) + } + + fn visit_get_tenure_info( + &mut self, + expr: &'a SymbolicExpression, + prop_name: &'a ClarityName, + block: &'a SymbolicExpression, + ) -> bool { + true + } + fn traverse_err( &mut self, expr: &'a SymbolicExpression, diff --git a/components/clarity-repl/src/repl/datastore.rs b/components/clarity-repl/src/repl/datastore.rs index 7bdce034f..39f3c8f84 100644 --- a/components/clarity-repl/src/repl/datastore.rs +++ b/components/clarity-repl/src/repl/datastore.rs @@ -8,6 +8,7 @@ use clarity::types::chainstate::StacksAddress; use clarity::types::chainstate::StacksBlockId; use clarity::types::chainstate::VRFSeed; use clarity::types::StacksEpochId; +use clarity::types::PEER_VERSION_EPOCH_2_1; use clarity::util::hash::Sha512Trunc256Sum; use clarity::vm::analysis::AnalysisDatabase; use clarity::vm::database::BurnStateDB; @@ -19,6 +20,8 @@ use clarity::vm::StacksEpoch; use pox_locking::handle_contract_call_special_cases; use sha2::{Digest, Sha512_256}; +use super::interpreter::BLOCK_LIMIT_MAINNET; + #[derive(Clone, Debug)] pub struct Datastore { store: HashMap>, @@ -51,7 +54,6 @@ pub struct StacksConstants { pub pox_prepare_length: u32, pub pox_reward_cycle_length: u32, pub pox_rejection_fraction: u64, - pub epoch_21_start_height: u32, } #[derive(Clone, Debug)] @@ -64,6 +66,8 @@ pub struct BurnDatastore { current_chain_tip: StacksBlockId, chain_height: u32, height_at_chain_tip: HashMap, + current_epoch: StacksEpochId, + current_epoch_start_height: u32, constants: StacksConstants, genesis_time: u64, } @@ -302,7 +306,7 @@ impl ClarityBackingStore for Datastore { } impl BurnDatastore { - pub fn new(constants: StacksConstants) -> BurnDatastore { + pub fn new(constants: StacksConstants) -> Self { let bytes = height_to_hashed_bytes(0); let id = StacksBlockId(bytes); let sortition_id = SortitionId(bytes); @@ -349,6 +353,8 @@ impl BurnDatastore { current_chain_tip: id, chain_height: 0, height_at_chain_tip, + current_epoch: StacksEpochId::Epoch2_05, + current_epoch_start_height: 0, constants, genesis_time, } @@ -380,6 +386,11 @@ impl BurnDatastore { self.open_chain_tip = height_to_id(self.chain_height); self.current_chain_tip = self.open_chain_tip; } + + pub fn set_current_epoch(&mut self, epoch: StacksEpochId) { + self.current_epoch = epoch; + self.current_epoch_start_height = self.chain_height; + } } impl HeadersDB for BurnDatastore { @@ -399,6 +410,7 @@ impl HeadersDB for BurnDatastore { fn get_stacks_block_header_hash_for_block( &self, id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, ) -> Option { self.store.get(id_bhh).map(|id| id.block_header_hash) } @@ -410,32 +422,63 @@ impl HeadersDB for BurnDatastore { self.store.get(id_bhh).map(|id| id.burn_block_header_hash) } - fn get_consensus_hash_for_block(&self, id_bhh: &StacksBlockId) -> Option { + fn get_consensus_hash_for_block( + &self, + id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, + ) -> Option { self.store.get(id_bhh).map(|id| id.consensus_hash) } - fn get_vrf_seed_for_block(&self, id_bhh: &StacksBlockId) -> Option { + fn get_vrf_seed_for_block( + &self, + id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, + ) -> Option { self.store.get(id_bhh).map(|id| id.vrf_seed) } - fn get_burn_block_time_for_block(&self, id_bhh: &StacksBlockId) -> Option { + fn get_stacks_block_time_for_block(&self, id_bhh: &StacksBlockId) -> Option { + self.store.get(id_bhh).map(|id| id.burn_block_time) + } + fn get_burn_block_time_for_block( + &self, + id_bhh: &StacksBlockId, + _epoch_id: Option<&StacksEpochId>, + ) -> Option { self.store.get(id_bhh).map(|id| id.burn_block_time) } fn get_burn_block_height_for_block(&self, id_bhh: &StacksBlockId) -> Option { self.store.get(id_bhh).map(|id| id.burn_block_height) } - fn get_miner_address(&self, id_bhh: &StacksBlockId) -> Option { + fn get_miner_address( + &self, + id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, + ) -> Option { self.store.get(id_bhh).map(|id| id.miner) } - fn get_burnchain_tokens_spent_for_block(&self, id_bhh: &StacksBlockId) -> Option { + fn get_burnchain_tokens_spent_for_block( + &self, + id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, + ) -> Option { self.store .get(id_bhh) .map(|id| id.burnchain_tokens_spent_for_block) } - fn get_burnchain_tokens_spent_for_winning_block(&self, id_bhh: &StacksBlockId) -> Option { + fn get_burnchain_tokens_spent_for_winning_block( + &self, + id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, + ) -> Option { self.store .get(id_bhh) .map(|id| id.get_burnchain_tokens_spent_for_winning_block) } - fn get_tokens_earned_for_block(&self, id_bhh: &StacksBlockId) -> Option { + fn get_tokens_earned_for_block( + &self, + id_bhh: &StacksBlockId, + _epoch_id: &StacksEpochId, + ) -> Option { self.store.get(id_bhh).map(|id| id.tokens_earned_for_block) } } @@ -481,7 +524,7 @@ impl BurnStateDB for BurnDatastore { /// Returns the height of the burnchain when the Stacks chain started running. fn get_burn_start_height(&self) -> u32 { - 0 + self.constants.burn_start_height } fn get_pox_prepare_length(&self) -> u32 { @@ -523,11 +566,17 @@ impl BurnStateDB for BurnDatastore { /// The epoch is defined as by a start and end height. This returns /// the epoch enclosing `height`. fn get_stacks_epoch(&self, _height: u32) -> Option { - None + Some(StacksEpoch { + epoch_id: self.current_epoch, + start_height: self.current_epoch_start_height.into(), + end_height: u64::MAX, + block_limit: BLOCK_LIMIT_MAINNET, + network_epoch: PEER_VERSION_EPOCH_2_1, + }) } fn get_stacks_epoch_by_epoch_id(&self, _epoch_id: &StacksEpochId) -> Option { - None + self.get_stacks_epoch(0) } /// Get the PoX payout addresses for a given burnchain block diff --git a/components/clarity-repl/src/repl/interpreter.rs b/components/clarity-repl/src/repl/interpreter.rs index 72f40f725..9b7aac669 100644 --- a/components/clarity-repl/src/repl/interpreter.rs +++ b/components/clarity-repl/src/repl/interpreter.rs @@ -57,10 +57,9 @@ impl ClarityInterpreter { pub fn new(tx_sender: StandardPrincipalData, repl_settings: Settings) -> Self { let constants = StacksConstants { burn_start_height: 0, - pox_prepare_length: 0, - pox_reward_cycle_length: 0, + pox_prepare_length: 50, + pox_reward_cycle_length: 1050, pox_rejection_fraction: 0, - epoch_21_start_height: 0, }; Self { tx_sender, @@ -326,63 +325,6 @@ impl ClarityInterpreter { Ok((contract_analysis, diagnostics)) } - pub fn save_contract( - &mut self, - contract: &ClarityContract, - contract_ast: &mut ContractAST, - contract_analysis: ContractAnalysis, - mainnet: bool, - ) { - let contract_id = contract.expect_resolved_contract_identifier(Some(&self.tx_sender)); - { - let mut contract_context = ContractContext::new( - contract.expect_resolved_contract_identifier(Some(&self.tx_sender)), - contract.clarity_version, - ); - - let conn = ClarityDatabase::new( - &mut self.datastore, - &self.burn_datastore, - &self.burn_datastore, - ); - - let cost_tracker = LimitedCostTracker::new_free(); - let mut global_context = GlobalContext::new( - mainnet, - clarity::consts::CHAIN_ID_TESTNET, - conn, - cost_tracker, - contract.epoch, - ); - global_context.begin(); - - let _ = global_context - .execute(|g| eval_all(&contract_ast.expressions, &mut contract_context, g, None)); - - global_context - .database - .insert_contract_hash(&contract_id, contract.expect_in_memory_code_source()) - .unwrap(); - let contract = Contract { contract_context }; - global_context - .database - .insert_contract(&contract_id, contract) - .expect("failed to insert contract"); - global_context - .database - .set_contract_data_size(&contract_id, 0) - .unwrap(); - global_context.commit().unwrap(); - }; - - let mut analysis_db = AnalysisDatabase::new(&mut self.datastore); - analysis_db.begin(); - analysis_db - .insert_contract(&contract_id, &contract_analysis) - .unwrap(); - analysis_db.commit().expect("unable to save data"); - } - pub fn get_block_time(&mut self) -> u64 { let block_height = self.get_block_height(); let mut conn = ClarityDatabase::new( @@ -1257,9 +1199,57 @@ mod tests { }; use clarity::{ types::{chainstate::StacksAddress, Address}, - vm::{self, ClarityVersion}, + vm::{self, types::TupleData, ClarityVersion}, }; + #[track_caller] + fn deploy_contract( + interpreter: &mut ClarityInterpreter, + contract: &ClarityContract, + ) -> Result { + let source = contract.expect_in_memory_code_source(); + let (mut ast, ..) = interpreter.build_ast(contract); + let (annotations, _) = interpreter.collect_annotations(source); + + let (analysis, _) = interpreter + .run_analysis(contract, &mut ast, &annotations) + .unwrap(); + + let result = interpreter.execute(contract, &mut ast, analysis, false, None); + assert!(result.is_ok()); + result + } + + #[track_caller] + fn call_fn_and_assert_value( + interpreter: &mut ClarityInterpreter, + contract_id: &QualifiedContractIdentifier, + method: &str, + raw_args: &[Vec], + epoch: StacksEpochId, + clarity_version: ClarityVersion, + expected_value: Value, + ) { + let result = interpreter.call_contract_fn( + contract_id, + method, + raw_args, + epoch, + clarity_version, + false, + true, + None, + ); + + assert!(result.is_ok()); + let result = result.unwrap(); + let result = match result.result { + EvaluationResult::Contract(_) => unreachable!(), + EvaluationResult::Snippet(res) => res, + }; + assert_eq!(result.result, expected_value); + } + #[test] fn test_get_tx_sender() { let mut interpreter = @@ -1454,16 +1444,8 @@ mod tests { ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); let contract = ClarityContract::fixture(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); + let result = deploy_contract(&mut interpreter, &contract); - let result = interpreter.execute(&contract, &mut ast, analysis, false, None); - assert!(result.is_ok()); let ExecutionResult { diagnostics, events, @@ -1504,7 +1486,7 @@ mod tests { ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); let contract = ClarityContract::fixture(); - let _ = interpreter.run(&contract, &mut None, false, None); + let _ = deploy_contract(&mut interpreter, &contract); let call_contract = ClarityContractBuilder::default() .code_source("(contract-call? .contract incr)".to_owned()) @@ -1519,14 +1501,9 @@ mod tests { let contract = ClarityContractBuilder::default() .code_source(["(define-data-var count uint u9)"].join("\n")) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - interpreter.save_contract(&contract, &mut ast, analysis, false); + let deploy = deploy_contract(&mut interpreter, &contract); + assert!(deploy.is_ok()); let contract_id = QualifiedContractIdentifier { issuer: StandardPrincipalData::transient(), @@ -1553,14 +1530,9 @@ mod tests { .join("\n"), ) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - interpreter.save_contract(&contract, &mut ast, analysis, false); + let deploy = deploy_contract(&mut interpreter, &contract); + assert!(deploy.is_ok()); let contract_id = QualifiedContractIdentifier { issuer: StandardPrincipalData::transient(), @@ -1592,13 +1564,6 @@ mod tests { .join("\n"), ) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); let account = PrincipalData::parse_standard_principal("S1G2081040G2081040G2081040G208105NK8PE5") @@ -1606,7 +1571,7 @@ mod tests { let balance = interpreter.get_balance_for_account(&account.to_string(), "STX"); assert_eq!(balance, 100000); - let result = interpreter.execute(&contract, &mut ast, analysis, false, None); + let result = deploy_contract(&mut interpreter, &contract); assert!(result.is_ok()); let ExecutionResult { @@ -1652,15 +1617,8 @@ mod tests { .join("\n"), ) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - - let result = interpreter.execute(&contract, &mut ast, analysis, false, None); + let result = deploy_contract(&mut interpreter, &contract); assert!(result.is_ok()); let ExecutionResult { diagnostics, @@ -1704,15 +1662,8 @@ mod tests { .join("\n"), ) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - let result = interpreter.execute(&contract, &mut ast, analysis, false, None); + let result = deploy_contract(&mut interpreter, &contract); assert!(result.is_ok()); let ExecutionResult { diagnostics, @@ -1765,6 +1716,199 @@ mod tests { } } + #[test] + fn block_height_support_in_clarity2_epoch2() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + let snippet = [ + "(define-read-only (get-height)", + " { block-height: block-height }", + ")", + "(define-read-only (get-info (h uint))", + " { time: (get-block-info? time h) }", + ")", + ] + .join("\n"); + let contract = ClarityContractBuilder::new() + .code_source(snippet) + .epoch(StacksEpochId::Epoch25) + .clarity_version(ClarityVersion::Clarity2) + .build(); + + let deploy = deploy_contract(&mut interpreter, &contract); + assert!(deploy.is_ok()); + + let contract_id = + contract.expect_resolved_contract_identifier(Some(&StandardPrincipalData::transient())); + + call_fn_and_assert_value( + &mut interpreter, + &contract_id, + "get-height", + &[], + StacksEpochId::Epoch25, + ClarityVersion::Clarity2, + Value::Tuple( + TupleData::from_data(vec![("block-height".into(), Value::UInt(0))]).unwrap(), + ), + ); + + interpreter.advance_chain_tip(10); + + call_fn_and_assert_value( + &mut interpreter, + &contract_id, + "get-height", + &[], + StacksEpochId::Epoch25, + ClarityVersion::Clarity2, + Value::Tuple( + TupleData::from_data(vec![("block-height".into(), Value::UInt(10))]).unwrap(), + ), + ); + + let call_contract = ClarityContractBuilder::default() + .code_source("(contract-call? .contract get-info u1)".into()) + .epoch(StacksEpochId::Epoch25) + .clarity_version(ClarityVersion::Clarity2) + .build(); + assert!(interpreter + .run(&call_contract, &mut None, false, None) + .is_ok()); + } + + #[test] + fn block_height_support_in_clarity2_epoch3() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + interpreter.advance_chain_tip(1); + + let snippet = [ + "(define-read-only (get-height)", + " { block-height: block-height }", + ")", + "(define-read-only (get-info (h uint))", + " { time: (get-block-info? time h) }", + ")", + ] + .join("\n"); + let contract = ClarityContractBuilder::new() + .code_source(snippet) + .epoch(StacksEpochId::Epoch30) + .clarity_version(ClarityVersion::Clarity2) + .build(); + + let deploy_result = deploy_contract(&mut interpreter, &contract); + assert!(deploy_result.is_ok()); + + let contract_id = + contract.expect_resolved_contract_identifier(Some(&StandardPrincipalData::transient())); + + call_fn_and_assert_value( + &mut interpreter, + &contract_id, + "get-height", + &[], + StacksEpochId::Epoch30, + ClarityVersion::Clarity2, + Value::Tuple( + TupleData::from_data(vec![("block-height".into(), Value::UInt(1))]).unwrap(), + ), + ); + + let call_contract = ClarityContractBuilder::default() + .code_source("(contract-call? .contract get-info u1)".into()) + .epoch(StacksEpochId::Epoch30) + .clarity_version(ClarityVersion::Clarity3) + .build(); + assert!(interpreter + .run(&call_contract, &mut None, false, None) + .is_ok()); + } + + #[test] + fn block_height_support_in_clarity3_epoch3() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + + interpreter.advance_chain_tip(1); + + let snippet = [ + "(define-read-only (get-height)", + " {", + " stacks-block-height: stacks-block-height,", + " tenure-height: tenure-height,", + " }", + ")", + "(define-read-only (get-info (h uint))", + " {", + " stacks-time: (get-stacks-block-info? time h),", + " stacks-id-header-hash: (get-stacks-block-info? id-header-hash h),", + " stacks-header-hash: (get-stacks-block-info? header-hash h),", + " tenure-time: (get-tenure-info? time h),", + " tenure-miner-address: (get-tenure-info? miner-address h),", + " }", + ")", + ] + .join("\n"); + let contract = ClarityContractBuilder::new() + .code_source(snippet) + .epoch(StacksEpochId::Epoch30) + .clarity_version(ClarityVersion::Clarity3) + .build(); + + let deploy_result = deploy_contract(&mut interpreter, &contract); + assert!(deploy_result.is_ok()); + + let contract_id = + contract.expect_resolved_contract_identifier(Some(&StandardPrincipalData::transient())); + + call_fn_and_assert_value( + &mut interpreter, + &contract_id, + "get-height", + &[], + StacksEpochId::Epoch30, + ClarityVersion::Clarity3, + Value::Tuple( + TupleData::from_data(vec![ + ("stacks-block-height".into(), Value::UInt(1)), + ("tenure-height".into(), Value::UInt(1)), + ]) + .unwrap(), + ), + ); + + interpreter.advance_chain_tip(10); + + call_fn_and_assert_value( + &mut interpreter, + &contract_id, + "get-height", + &[], + StacksEpochId::Epoch30, + ClarityVersion::Clarity3, + Value::Tuple( + TupleData::from_data(vec![ + ("stacks-block-height".into(), Value::UInt(11)), + ("tenure-height".into(), Value::UInt(11)), + ]) + .unwrap(), + ), + ); + + let call_contract = ClarityContractBuilder::default() + .code_source("(contract-call? .contract get-info u1)".into()) + .epoch(StacksEpochId::Epoch30) + .clarity_version(ClarityVersion::Clarity3) + .build(); + assert!(interpreter + .run(&call_contract, &mut None, false, None) + .is_ok()); + } + #[test] fn can_call_a_public_function() { let mut interpreter = @@ -1773,15 +1917,7 @@ mod tests { let contract = ClarityContractBuilder::default() .code_source("(define-public (public-func) (ok true))".into()) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - - let _ = interpreter.execute(&contract, &mut ast, analysis, false, None); + let _ = deploy_contract(&mut interpreter, &contract); let allow_private = false; let result = interpreter.call_contract_fn( @@ -1812,15 +1948,7 @@ mod tests { let contract = ClarityContractBuilder::default() .code_source("(define-private (private-func) true)".into()) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - - let _ = interpreter.execute(&contract, &mut ast, analysis, false, None); + let _ = deploy_contract(&mut interpreter, &contract); let allow_private = true; let result = interpreter.call_contract_fn( @@ -1835,7 +1963,6 @@ mod tests { None, ); - println!("{:?}", result); assert!(result.is_ok()); let ExecutionResult { result, .. } = result.unwrap(); @@ -1852,15 +1979,7 @@ mod tests { let contract = ClarityContractBuilder::default() .code_source("(define-private (private-func) true)".into()) .build(); - let source = contract.expect_in_memory_code_source(); - let (mut ast, ..) = interpreter.build_ast(&contract); - let (annotations, _) = interpreter.collect_annotations(source); - - let (analysis, _) = interpreter - .run_analysis(&contract, &mut ast, &annotations) - .unwrap(); - - let _ = interpreter.execute(&contract, &mut ast, analysis, false, None); + let _ = deploy_contract(&mut interpreter, &contract); let allow_private = false; let result = interpreter.call_contract_fn( diff --git a/components/clarity-repl/src/repl/session.rs b/components/clarity-repl/src/repl/session.rs index da648bd4e..976da5377 100644 --- a/components/clarity-repl/src/repl/session.rs +++ b/components/clarity-repl/src/repl/session.rs @@ -504,6 +504,19 @@ impl Session { test_name: Option, ast: &mut Option, ) -> Result> { + if contract.epoch != self.current_epoch { + let diagnostic = Diagnostic { + level: Level::Error, + message: format!( + "contract epoch ({}) does not match current epoch ({})", + contract.epoch, self.current_epoch + ), + spans: vec![], + suggestion: None, + }; + return Err(vec![diagnostic]); + } + if contract.clarity_version > ClarityVersion::default_for_epoch(contract.epoch) { let diagnostic = Diagnostic { level: Level::Error, @@ -533,19 +546,16 @@ impl Session { let result = self.interpreter.run(contract, ast, cost_track, Some(hooks)); - match result { - Ok(result) => { - if let Some(ref coverage) = coverage { - self.coverage_reports.push(coverage.clone()); - } - if let EvaluationResult::Contract(contract_result) = &result.result { - self.contracts - .insert(contract_id.clone(), contract_result.contract.clone()); - }; - Ok(result) + result.map(|result| { + if let EvaluationResult::Contract(contract_result) = &result.result { + self.contracts + .insert(contract_id.clone(), contract_result.contract.clone()); } - Err(res) => Err(res), - } + if let Some(coverage) = coverage { + self.coverage_reports.push(coverage); + } + result + }) } pub fn invoke_contract_call( @@ -951,11 +961,13 @@ impl Session { )) } }; + self.update_epoch(epoch); output.push(green!(format!("Epoch updated to: {epoch}"))); } pub fn update_epoch(&mut self, epoch: StacksEpochId) { self.current_epoch = epoch; + self.interpreter.burn_datastore.set_current_epoch(epoch); if epoch >= StacksEpochId::Epoch30 { self.interpreter.set_tenure_height(); } @@ -1312,7 +1324,7 @@ fn clarity_keywords() -> HashMap { #[allow(clippy::items_after_test_module)] #[cfg(test)] mod tests { - use crate::repl::{self, settings::Account}; + use crate::{repl::settings::Account, test_fixtures::clarity_contract::ClarityContractBuilder}; use super::*; @@ -1379,6 +1391,29 @@ mod tests { ); } + #[test] + fn set_epoch_command() { + let mut session = Session::new(SessionSettings::default()); + let initial_epoch = session.handle_command("::get_epoch"); + // initial epoch is 2.05 + assert_eq!(initial_epoch.1[0], "Current epoch: 2.05"); + + // it can be lowered to 2.0 + // it's possible that in the feature we want to start from 2.0 and forbid lowering the epoch + // this test would have to be updated + session.handle_command("::set_epoch 2.0"); + let current_epoch = session.handle_command("::get_epoch"); + assert_eq!(current_epoch.1[0], "Current epoch: 2.0"); + + session.handle_command("::set_epoch 2.4"); + let current_epoch = session.handle_command("::get_epoch"); + assert_eq!(current_epoch.1[0], "Current epoch: 2.4"); + + session.handle_command("::set_epoch 3.0"); + let current_epoch = session.handle_command("::get_epoch"); + assert_eq!(current_epoch.1[0], "Current epoch: 3.0"); + } + #[test] fn encode_error() { let mut session = Session::new(SessionSettings::default()); @@ -1443,20 +1478,45 @@ mod tests { fn clarity_epoch_mismatch() { let settings = SessionSettings::default(); let mut session = Session::new(settings); - session.start().expect("session could not start"); let snippet = "(define-data-var x uint u0)"; + + // can not use ClarityContractBuilder to build an invalid contract let contract = ClarityContract { code_source: ClarityCodeSource::ContractInMemory(snippet.to_string()), name: "should_error".to_string(), deployer: ContractDeployer::Address("ST000000000000000000002AMW42H".into()), clarity_version: ClarityVersion::Clarity2, - epoch: StacksEpochId::Epoch20, + epoch: StacksEpochId::Epoch2_05, }; let result = session.deploy_contract(&contract, None, false, None, &mut None); assert!(result.is_err(), "Expected error for clarity mismatch"); } + #[test] + fn deploy_contract_with_wrong_epoch() { + let settings = SessionSettings::default(); + let mut session = Session::new(settings); + + session.update_epoch(StacksEpochId::Epoch24); + + let snippet = "(define-data-var x uint u0)"; + let contract = ClarityContractBuilder::new() + .code_source(snippet.into()) + .epoch(StacksEpochId::Epoch25) + .clarity_version(ClarityVersion::Clarity2) + .build(); + + let result = session.deploy_contract(&contract, None, false, None, &mut None); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.len() == 1); + assert_eq!( + err.first().unwrap().message, + "contract epoch (2.5) does not match current epoch (2.4)" + ); + } + #[test] fn evaluate_at_block() { let settings = SessionSettings { @@ -1467,13 +1527,13 @@ mod tests { let mut session = Session::new(settings); session.start().expect("session could not start"); + session.handle_command("::set_epoch 2.5"); + // setup contract state let snippet = " (define-data-var x uint u0) - (define-read-only (get-x) (var-get x)) - (define-public (incr) (begin (var-set x (+ (var-get x) u1)) @@ -1483,8 +1543,8 @@ mod tests { code_source: ClarityCodeSource::ContractInMemory(snippet.to_string()), name: "contract".to_string(), deployer: ContractDeployer::Address("ST000000000000000000002AMW42H".into()), - clarity_version: ClarityVersion::Clarity1, - epoch: repl::DEFAULT_EPOCH, + clarity_version: ClarityVersion::Clarity2, + epoch: StacksEpochId::Epoch25, }; let _ = session.deploy_contract(&contract, None, false, None, &mut None);