From efeadc5cc21c94ec2c02bb71fc7e8e8207344fff Mon Sep 17 00:00:00 2001 From: Brady Ouren Date: Fri, 26 Jul 2024 04:00:57 -0700 Subject: [PATCH] feat: advance burn and stacks chain tips independently (#1506) * block height changes * use get_block_height to mean stacks specifically * separate chaintip advancing * change chaintip back to u32 and no burn increment on stacks advance * fix tests with burn changes * add back mineEmptyBlock(s) * fix up some double advance bugs * error on advance_stacks if epoch < 3 * handle advance_stacks errors in tests * add more rust tests and stub out simnet 3 tests * mine_empty_stacks* should return Result * clean up fixtures * add session command testing * remove redundant evaluate_at_block epoch 3 test * change error message for advance_stacks_chaintip * add generic interpreter chain_tip advance which acts differently in epoch pre-3 vs 3 * chaintip -> chain_tip * add back the advance_chain_tip help --------- Co-authored-by: brady.ouren --- components/clarinet-sdk-wasm/src/core.rs | 45 +++++- .../node/tests/fixtures/Clarinet3.toml | 21 +++ .../node/tests/simnet-usage.test.ts | 20 +++ components/clarity-repl/src/repl/datastore.rs | 10 +- .../clarity-repl/src/repl/interpreter.rs | 93 +++++++++++- components/clarity-repl/src/repl/session.rs | 137 +++++++++++++++++- .../stacks-network/src/chains_coordinator.rs | 10 +- 7 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 components/clarinet-sdk/node/tests/fixtures/Clarinet3.toml diff --git a/components/clarinet-sdk-wasm/src/core.rs b/components/clarinet-sdk-wasm/src/core.rs index 21fe2ad09..b042a84fd 100644 --- a/components/clarinet-sdk-wasm/src/core.rs +++ b/components/clarinet-sdk-wasm/src/core.rs @@ -576,6 +576,18 @@ impl SDK { session.interpreter.get_block_height() } + #[wasm_bindgen(getter, js_name=stacksBlockHeight)] + pub fn stacks_block_height(&mut self) -> u32 { + let session = self.get_session_mut(); + session.interpreter.get_block_height() + } + + #[wasm_bindgen(getter, js_name=burnBlockHeight)] + pub fn burn_block_height(&mut self) -> u32 { + let session = self.get_session_mut(); + session.interpreter.get_burn_block_height() + } + #[wasm_bindgen(getter, js_name=currentEpoch)] pub fn current_epoch(&mut self) -> String { let session = self.get_session_mut(); @@ -911,14 +923,39 @@ impl SDK { #[wasm_bindgen(js_name=mineEmptyBlock)] pub fn mine_empty_block(&mut self) -> u32 { - let session = self.get_session_mut(); - session.advance_chain_tip(1) + self.mine_empty_burn_block() } - #[wasm_bindgen(js_name=mineEmptyBlocks)] pub fn mine_empty_blocks(&mut self, count: Option) -> u32 { + self.mine_empty_burn_blocks(count) + } + #[wasm_bindgen(js_name=mineEmptyStacksBlock)] + pub fn mine_empty_stacks_block(&mut self) -> Result { + let session = self.get_session_mut(); + match session.advance_stacks_chain_tip(1) { + Ok(new_height) => Ok(new_height), + Err(_) => Err("use mineEmptyBurnBlock in epoch lower than 3.0".to_string()), + } + } + + #[wasm_bindgen(js_name=mineEmptyStacksBlocks)] + pub fn mine_empty_stacks_blocks(&mut self, count: Option) -> Result { + let session = self.get_session_mut(); + match session.advance_stacks_chain_tip(count.unwrap_or(1)) { + Ok(new_height) => Ok(new_height), + Err(_) => Err("use mineEmptyBurnBlocks in epoch lower than 3.0".to_string()), + } + } + + #[wasm_bindgen(js_name=mineEmptyBurnBlock)] + pub fn mine_empty_burn_block(&mut self) -> u32 { + let session = self.get_session_mut(); + session.advance_burn_chain_tip(1) + } + #[wasm_bindgen(js_name=mineEmptyBurnBlocks)] + pub fn mine_empty_burn_blocks(&mut self, count: Option) -> u32 { let session = self.get_session_mut(); - session.advance_chain_tip(count.unwrap_or(1)) + session.advance_burn_chain_tip(count.unwrap_or(1)) } #[wasm_bindgen(js_name=runSnippet)] diff --git a/components/clarinet-sdk/node/tests/fixtures/Clarinet3.toml b/components/clarinet-sdk/node/tests/fixtures/Clarinet3.toml new file mode 100644 index 000000000..2cd8f7d7c --- /dev/null +++ b/components/clarinet-sdk/node/tests/fixtures/Clarinet3.toml @@ -0,0 +1,21 @@ +[project] +name = 'fixtures' +description = 'sample project to test clarinet-sdk wasm' +telemetry = false +cache_dir = './.cache' +requirements = [] + +[contracts.multiplier-trait] +path = 'contracts/multiplier-trait.clar' +clarity_version = 3 +epoch = 3.0 + +[contracts.multiplier-contract] +path = 'contracts/multiplier-contract.clar' +clarity_version = 3 +epoch = 3.0 + +[contracts.counter] +path = 'contracts/counter.clar' +clarity_version = 3 +epoch = 3.0 diff --git a/components/clarinet-sdk/node/tests/simnet-usage.test.ts b/components/clarinet-sdk/node/tests/simnet-usage.test.ts index 99d1ca4a4..736a5601a 100644 --- a/components/clarinet-sdk/node/tests/simnet-usage.test.ts +++ b/components/clarinet-sdk/node/tests/simnet-usage.test.ts @@ -47,6 +47,11 @@ describe("basic simnet interactions", () => { simnet.mineEmptyBlocks(4); expect(simnet.blockHeight).toBe(blockHeight + 5); }); + it("can not mine empty stacks block in pre-3.0", () => { + expect(() => simnet.mineEmptyStacksBlock()).toThrowError( + "use mineEmptyBurnBlock in epoch lower than 3.0" + ); + }) it("exposes devnet stacks accounts", () => { const accounts = simnet.getAccounts(); @@ -83,6 +88,21 @@ describe("basic simnet interactions", () => { }); }); +describe("simnet epoch 3", () => { + it("can mine empty blocks", () => { + simnet.setEpoch("3.0"); + const blockHeight = simnet.stacksBlockHeight; + const burnBlockHeight = simnet.burnBlockHeight; + simnet.mineEmptyStacksBlock(); + expect(simnet.stacksBlockHeight).toBe(blockHeight + 1); + expect(simnet.burnBlockHeight).toBe(burnBlockHeight); + simnet.mineEmptyStacksBlocks(4); + expect(simnet.stacksBlockHeight).toBe(blockHeight + 5); + simnet.mineEmptyBurnBlocks(4); + expect(simnet.burnBlockHeight).toBe(burnBlockHeight + 4); + expect(simnet.stacksBlockHeight).toBe(blockHeight + 9); + }) +}) describe("simnet can run arbitrary snippets", () => { it("can run simple snippets", () => { const res = simnet.execute("(+ 1 2)"); diff --git a/components/clarity-repl/src/repl/datastore.rs b/components/clarity-repl/src/repl/datastore.rs index 033aea39c..569557df0 100644 --- a/components/clarity-repl/src/repl/datastore.rs +++ b/components/clarity-repl/src/repl/datastore.rs @@ -374,7 +374,14 @@ impl BurnDatastore { } } - pub fn advance_chain_tip(&mut self, count: u32) { + pub fn get_current_epoch(&self) -> StacksEpochId { + self.current_epoch + } + + pub fn get_current_block_height(&self) -> u32 { + self.chain_height + } + pub fn advance_chain_tip(&mut self, count: u32) -> u32 { let cur_height = self.chain_height; let current_lookup_id = *self .block_id_lookup @@ -399,6 +406,7 @@ impl BurnDatastore { self.chain_height += count; self.open_chain_tip = height_to_id(self.chain_height); self.current_chain_tip = self.open_chain_tip; + self.chain_height } pub fn set_current_epoch(&mut self, epoch: StacksEpochId) { diff --git a/components/clarity-repl/src/repl/interpreter.rs b/components/clarity-repl/src/repl/interpreter.rs index 7c2c07e87..d8759fb76 100644 --- a/components/clarity-repl/src/repl/interpreter.rs +++ b/components/clarity-repl/src/repl/interpreter.rs @@ -1099,11 +1099,31 @@ impl ClarityInterpreter { } pub fn advance_chain_tip(&mut self, count: u32) -> u32 { - self.burn_datastore.advance_chain_tip(count); - let new_height = self.datastore.advance_chain_tip(count); + let current_epoch = self.burn_datastore.get_current_epoch(); + if current_epoch < StacksEpochId::Epoch30 { + self.advance_burn_chain_tip(count) + } else { + match self.advance_stacks_chain_tip(count) { + Ok(count) => count, + Err(_) => unreachable!("Epoch checked already"), + } + } + } + + pub fn advance_burn_chain_tip(&mut self, count: u32) -> u32 { + let new_height = self.burn_datastore.advance_chain_tip(count); + let _ = self.datastore.advance_chain_tip(count); self.set_tenure_height(); new_height } + pub fn advance_stacks_chain_tip(&mut self, count: u32) -> Result { + let current_epoch = self.burn_datastore.get_current_epoch(); + if current_epoch < StacksEpochId::Epoch30 { + Err("only burn chain height can be advanced in epoch lower than 3.0".to_string()) + } else { + Ok(self.datastore.advance_chain_tip(count)) + } + } pub fn set_tenure_height(&mut self) { let block_height = self.get_block_height(); @@ -1122,6 +1142,10 @@ impl ClarityInterpreter { self.datastore.get_current_block_height() } + pub fn get_burn_block_height(&mut self) -> u32 { + self.burn_datastore.get_current_block_height() + } + fn credit_token(&mut self, account: String, token: String, value: u128) { self.accounts.insert(account.clone()); match self.tokens.entry(token) { @@ -1184,6 +1208,7 @@ impl ClarityInterpreter { #[cfg(test)] mod tests { use super::*; + use crate::analysis::Settings as AnalysisSettings; use crate::{ repl::session::BOOT_CONTRACTS_DATA, test_fixtures::clarity_contract::ClarityContractBuilder, }; @@ -1259,14 +1284,68 @@ mod tests { assert_eq!(interpreter.get_block_height(), 0); } + #[test] + fn test_advance_stacks_chain_tip_pre_epoch_3() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + interpreter + .burn_datastore + .set_current_epoch(StacksEpochId::Epoch2_05); + let count = 5; + let initial_block_height = interpreter.get_burn_block_height(); + assert_ne!(interpreter.advance_stacks_chain_tip(count), Ok(count)); + assert_eq!(interpreter.get_burn_block_height(), initial_block_height); + assert_eq!(interpreter.get_block_height(), initial_block_height); + } + #[test] + fn test_advance_stacks_chain_tip() { + let wasm_settings = Settings { + analysis: AnalysisSettings::default(), + clarity_wasm_mode: true, + show_timings: false, + }; + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), wasm_settings); + interpreter + .burn_datastore + .set_current_epoch(StacksEpochId::Epoch30); + let count = 5; + let initial_block_height = interpreter.get_burn_block_height(); + assert_eq!(interpreter.advance_stacks_chain_tip(count), Ok(count)); + assert_eq!(interpreter.get_burn_block_height(), initial_block_height); + assert_eq!(interpreter.get_block_height(), initial_block_height + count); + } + #[test] + fn test_advance_chain_tip_pre_epoch3() { + let mut interpreter = + ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + interpreter + .burn_datastore + .set_current_epoch(StacksEpochId::Epoch2_05); + let count = 5; + let initial_block_height = interpreter.get_block_height(); + interpreter.advance_burn_chain_tip(count); + assert_eq!(interpreter.get_block_height(), initial_block_height + count); + assert_eq!( + interpreter.get_burn_block_height(), + initial_block_height + count + ); + } #[test] fn test_advance_chain_tip() { let mut interpreter = ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); + interpreter + .burn_datastore + .set_current_epoch(StacksEpochId::Epoch30); let count = 5; let initial_block_height = interpreter.get_block_height(); - interpreter.advance_chain_tip(count); + interpreter.advance_burn_chain_tip(count); assert_eq!(interpreter.get_block_height(), initial_block_height + count); + assert_eq!( + interpreter.get_burn_block_height(), + initial_block_height + count + ); } #[test] @@ -1730,7 +1809,7 @@ mod tests { ), ); - interpreter.advance_chain_tip(10); + interpreter.advance_burn_chain_tip(10); let result = interpreter.call_contract_fn( &contract_id, @@ -1762,7 +1841,7 @@ mod tests { let mut interpreter = ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); - interpreter.advance_chain_tip(1); + interpreter.advance_burn_chain_tip(1); let snippet = [ "(define-read-only (get-height)", @@ -1815,7 +1894,7 @@ mod tests { let mut interpreter = ClarityInterpreter::new(StandardPrincipalData::transient(), Settings::default()); - interpreter.advance_chain_tip(1); + interpreter.advance_burn_chain_tip(1); let snippet = [ "(define-read-only (get-height)", @@ -1868,7 +1947,7 @@ mod tests { ), ); - interpreter.advance_chain_tip(10); + interpreter.advance_burn_chain_tip(10); let result = interpreter.call_contract_fn( &contract_id, diff --git a/components/clarity-repl/src/repl/session.rs b/components/clarity-repl/src/repl/session.rs index a7371b5d7..9deb28a69 100644 --- a/components/clarity-repl/src/repl/session.rs +++ b/components/clarity-repl/src/repl/session.rs @@ -248,8 +248,16 @@ impl Session { cmd if cmd.starts_with("::get_contracts") => { self.get_contracts().unwrap_or("No contract found".into()) } + cmd if cmd.starts_with("::get_burn_block_height") => self.get_burn_block_height(), + cmd if cmd.starts_with("::get_stacks_block_height") => self.get_block_height(), cmd if cmd.starts_with("::get_block_height") => self.get_block_height(), cmd if cmd.starts_with("::advance_chain_tip") => self.parse_and_advance_chain_tip(cmd), + cmd if cmd.starts_with("::advance_stacks_chain_tip") => { + self.parse_and_advance_stacks_chain_tip(cmd) + } + cmd if cmd.starts_with("::advance_burn_chain_tip") => { + self.parse_and_advance_burn_chain_tip(cmd) + } cmd if cmd.starts_with("::get_epoch") => self.get_epoch(), cmd if cmd.starts_with("::set_epoch") => self.set_epoch(cmd), cmd if cmd.starts_with("::encode") => self.encode(cmd), @@ -761,6 +769,15 @@ impl Session { "{}", "::advance_chain_tip \t\tSimulate mining of blocks".yellow() )); + output.push(format!( + "{}", + "::advance_stacks_chain_tip \tSimulate mining of stacks blocks".yellow() + )); + output.push(format!( + "{}", + "::advance_burn_chain_tip \tSimulate mining of burnchain blocks" + .yellow() + )); output.push(format!( "{}", "::set_epoch \t\t\tUpdate the current epoch".yellow() @@ -809,6 +826,31 @@ impl Session { output.join("\n") } + fn parse_and_advance_stacks_chain_tip(&mut self, command: &str) -> String { + let args: Vec<_> = command.split(' ').collect(); + + if args.len() != 2 { + return format!("{}", "Usage: ::advance_stacks_chain_tip ".red()); + } + + let count = match args[1].parse::() { + Ok(count) => count, + _ => { + return format!("{}", "Unable to parse count".red()); + } + }; + + match self.advance_stacks_chain_tip(count) { + Ok(new_height) => format!("{} blocks simulated, new height: {}", count, new_height) + .green() + .to_string(), + Err(_) => format!( + "{}", + "advance_stacks_chain_tip can't be called in epoch lower than 3.0".red() + ), + } + } + fn parse_and_advance_chain_tip(&mut self, command: &str) -> String { let args: Vec<_> = command.split(' ').collect(); @@ -828,7 +870,32 @@ impl Session { .green() .to_string() } + fn parse_and_advance_burn_chain_tip(&mut self, command: &str) -> String { + let args: Vec<_> = command.split(' ').collect(); + + if args.len() != 2 { + return format!("{}", "Usage: ::advance_burn_chain_tip ".red()); + } + + let count = match args[1].parse::() { + Ok(count) => count, + _ => { + return format!("{}", "Unable to parse count".red()); + } + }; + + let new_height = self.advance_burn_chain_tip(count); + format!("{} blocks simulated, new height: {}", count, new_height) + .green() + .to_string() + } + pub fn advance_stacks_chain_tip(&mut self, count: u32) -> Result { + self.interpreter.advance_stacks_chain_tip(count) + } + pub fn advance_burn_chain_tip(&mut self, count: u32) -> u32 { + self.interpreter.advance_burn_chain_tip(count) + } pub fn advance_chain_tip(&mut self, count: u32) -> u32 { self.interpreter.advance_chain_tip(count) } @@ -866,6 +933,11 @@ impl Session { format!("Current height: {}", height) } + fn get_burn_block_height(&mut self) -> String { + let height = self.interpreter.get_burn_block_height(); + format!("Current height: {}", height) + } + fn get_account_name(&self, address: &String) -> Option<&String> { for account in self.settings.initial_accounts.iter() { if &account.address == address { @@ -1237,9 +1309,8 @@ fn clarity_keywords() -> HashMap { mod tests { use clarity::vm::types::TupleData; - use crate::{repl::settings::Account, test_fixtures::clarity_contract::ClarityContractBuilder}; - use super::*; + use crate::{repl::settings::Account, test_fixtures::clarity_contract::ClarityContractBuilder}; #[track_caller] fn assert_execution_result_value( @@ -1297,6 +1368,66 @@ mod tests { ); } + #[test] + fn test_parse_and_advance_stacks_chain_tip() { + let mut session = Session::new(SessionSettings::default()); + let result = session.handle_command("::advance_stacks_chain_tip 1"); + assert_eq!( + result, + "advance_stacks_chain_tip can't be called in epoch lower than 3.0" + .to_string() + .red() + .to_string() + ); + session.handle_command("::set_epoch 3.0"); + let _ = session.handle_command("::advance_stacks_chain_tip 1"); + let new_height = session.handle_command("::get_stacks_block_height"); + assert_eq!(new_height, "Current height: 1"); + } + + #[test] + fn test_parse_and_advance_burn_chain_tip_pre_epoch3() { + let mut session = Session::new(SessionSettings::default()); + let result = session.handle_command("::advance_burn_chain_tip 1"); + assert_eq!( + result, + "1 blocks simulated, new height: 1" + .to_string() + .green() + .to_string() + ); + // before epoch 3 this acts the same as burn_chain_tip + let result = session.handle_command("::advance_chain_tip 1"); + assert_eq!( + result, + "1 blocks simulated, new height: 2" + .to_string() + .green() + .to_string() + ); + } + + #[test] + fn test_parse_and_advance_burn_chain_tip_epoch3() { + let mut session = Session::new(SessionSettings::default()); + session.handle_command("::set_epoch 3.0"); + let result = session.handle_command("::advance_burn_chain_tip 1"); + assert_eq!( + result, + "1 blocks simulated, new height: 1" + .to_string() + .green() + .to_string() + ); + let new_height = session.handle_command("::get_stacks_block_height"); + assert_eq!(new_height, "Current height: 1"); + // advance_chain_tip will only affect stacks height in epoch 3 or greater + let _ = session.handle_command("::advance_chain_tip 1"); + let new_height = session.handle_command("::get_stacks_block_height"); + assert_eq!(new_height, "Current height: 2"); + let new_height = session.handle_command("::get_burn_block_height"); + assert_eq!(new_height, "Current height: 1"); + } #[test] fn set_epoch_command() { let mut session = Session::new(SessionSettings::default()); @@ -1458,7 +1589,7 @@ mod tests { ); // advance chain tip and test at-block - session.advance_chain_tip(10000); + let _ = session.advance_chain_tip(10000); assert_eq!( session .process_console_input("(contract-call? .contract get-x)") diff --git a/components/stacks-network/src/chains_coordinator.rs b/components/stacks-network/src/chains_coordinator.rs index 22414e25a..0c8dc27e7 100644 --- a/components/stacks-network/src/chains_coordinator.rs +++ b/components/stacks-network/src/chains_coordinator.rs @@ -308,7 +308,7 @@ pub async fn start_chains_coordinator( let bitcoin_block_height = tip.block_identifier.index; let log = format!("Bitcoin block #{} received", bitcoin_block_height); let comment = - format!("mining blocks (chaintip = #{})", bitcoin_block_height); + format!("mining blocks (chain_tip = #{})", bitcoin_block_height); // Stacking orders can't be published until devnet is ready if bitcoin_block_height >= DEFAULT_FIRST_BURN_HEADER_HEIGHT + 10 { @@ -337,8 +337,10 @@ pub async fn start_chains_coordinator( "Bitcoin reorg received (new height: {})", tip.block_identifier.index ); - let status = - format!("mining blocks (chaintip = #{})", tip.block_identifier.index); + let status = format!( + "mining blocks (chain_tip = #{})", + tip.block_identifier.index + ); (log, status) } }; @@ -399,7 +401,7 @@ pub async fn start_chains_coordinator( "stacks-node", Status::Green, &format!( - "mining blocks (chaintip = #{})", + "mining blocks (chain_tip = #{})", known_tip.block.block_identifier.index ), );