From 75529a71824dad74299894441b0602304998482e Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 14:04:54 -0400 Subject: [PATCH 01/13] destruct a ClarityBlockConnection back into its underlying datastore --- src/vm/clarity.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/vm/clarity.rs b/src/vm/clarity.rs index ede8d27ad3..ac8c9617c7 100644 --- a/src/vm/clarity.rs +++ b/src/vm/clarity.rs @@ -639,6 +639,10 @@ impl<'a> ClarityBlockConnection<'a> { pub fn get_root_hash(&mut self) -> TrieHash { self.datastore.get_root_hash() } + + pub fn destruct(self) -> WritableMarfStore<'a> { + self.datastore + } } impl<'a, 'b> ClarityConnection for ClarityTransactionConnection<'a, 'b> { From e3644765b46690b7791a56e406590ff4499072ec Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 14:05:27 -0400 Subject: [PATCH 02/13] turn an AssetMap into JSON --- src/vm/contexts.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/vm/contexts.rs b/src/vm/contexts.rs index 543ebf10d7..d038a61bde 100644 --- a/src/vm/contexts.rs +++ b/src/vm/contexts.rs @@ -91,6 +91,59 @@ pub struct AssetMap { asset_map: HashMap>>, } +impl AssetMap { + pub fn to_json(&self) -> serde_json::Value { + let stx : serde_json::map::Map<_, _> = self.stx_map + .iter() + .map(|(principal, amount)| (format!("{}", principal), serde_json::value::Value::String(format!("{}", amount)))) + .collect(); + + let burns : serde_json::map::Map<_, _> = self.burn_map + .iter() + .map(|(principal, amount)| (format!("{}", principal), serde_json::value::Value::String(format!("{}", amount)))) + .collect(); + + let tokens : serde_json::map::Map<_, _> = self.token_map + .iter() + .map(|(principal, token_map)| { + let token_json : serde_json::map::Map<_, _> = token_map + .iter() + .map(|(asset_id, amount)| (format!("{}", asset_id), serde_json::value::Value::String(format!("{}", amount)))) + .collect(); + + (format!("{}", principal), serde_json::value::Value::Object(token_json)) + }) + .collect(); + + let assets : serde_json::map::Map<_, _> = self.asset_map + .iter() + .map(|(principal, nft_map)| { + let nft_json : serde_json::map::Map<_, _> = nft_map + .iter() + .map(|(asset_id, nft_values)| { + let nft_array = nft_values + .iter() + .map(|nft_value| serde_json::value::Value::String(format!("{}", nft_value))) + .collect(); + + (format!("{}", asset_id), serde_json::value::Value::Array(nft_array)) + }) + .collect(); + + (format!("{}", principal), serde_json::value::Value::Object(nft_json)) + }) + .collect(); + + json!({ + "stx": stx, + "burns": burns, + "tokens": tokens, + "assets": assets + }) + } +} + + #[derive(Debug, Clone)] pub struct EventBatch { pub events: Vec, @@ -640,6 +693,10 @@ impl<'a> OwnedEnvironment<'a> { Ok((asset_map, event_batch)) } + pub fn get_cost_total(&self) -> ExecutionCost { + self.context.cost_track.get_total() + } + /// Destroys this environment, returning ownership of its database reference. /// If the context wasn't top-level (i.e., it had uncommitted data), return None, /// because the database is not guaranteed to be in a sane state. From c071672f9a375e169b1189ddbd7263696d159944 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 14:05:46 -0400 Subject: [PATCH 03/13] address #2573, as well as PR #2587. The clarity-cli binary will optionally report runtime costs and asset movements, and will output everything in JSON --- src/clarity.rs | 1194 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 943 insertions(+), 251 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index 629cd5a7ed..d1540d31bd 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -39,15 +39,18 @@ use util::db::FromColumn; use util::hash::Sha512Trunc256Sum; +use vm::ContractName; + use vm::analysis; use vm::analysis::contract_interface_builder::build_contract_interface; -use vm::analysis::{errors::CheckResult, AnalysisDatabase, ContractAnalysis}; +use vm::analysis::{errors::CheckError, errors::CheckResult, AnalysisDatabase, ContractAnalysis}; use vm::ast::build_ast; use vm::contexts::OwnedEnvironment; +use vm::costs::ExecutionCost; use vm::costs::LimitedCostTracker; use vm::database::{ - ClarityDatabase, HeadersDB, MarfedKV, MemoryBackingStore, STXBalance, SqliteConnection, - NULL_BURN_STATE_DB, NULL_HEADER_DB, + BurnStateDB, ClarityDatabase, HeadersDB, MarfedKV, MemoryBackingStore, STXBalance, + SqliteConnection, NULL_BURN_STATE_DB, NULL_HEADER_DB, }; use vm::errors::{Error, InterpreterResult, RuntimeErrorType}; use vm::types::{PrincipalData, QualifiedContractIdentifier}; @@ -56,10 +59,19 @@ use vm::{execute as vm_execute, SymbolicExpression, SymbolicExpressionType, Valu use address::c32::c32_address; use burnchains::BurnchainHeaderHash; +use burnchains::PoxConstants; +use burnchains::Txid; use chainstate::burn::VRFSeed; use chainstate::stacks::StacksAddress; +use chainstate::stacks::boot::{boot_code_addr, boot_code_id}; +use chainstate::stacks::boot::{STACKS_BOOT_CODE_MAINNET, STACKS_BOOT_CODE_TESTNET}; +use core::BLOCK_LIMIT_MAINNET; use serde::Serialize; +use serde_json::json; +use util::strings::StacksString; + +use std::convert::TryFrom; use crate::vm::database::marf::WritableMarfStore; @@ -76,6 +88,15 @@ macro_rules! panic_test { }; } +const HELIUM_BLOCK_LIMIT: ExecutionCost = ExecutionCost { + write_length: 15_0_000_000, + write_count: 5_0_000, + read_length: 1_000_000_000, + read_count: 5_0_000, + // allow much more runtime in helium blocks than mainnet + runtime: 100_000_000_000, +}; + #[cfg_attr(tarpaulin, skip)] fn print_usage(invoked_by: &str) { eprintln!( @@ -130,35 +151,108 @@ fn parse( Ok(ast.expressions) } -fn run_analysis( +trait ClarityStorage { + fn get_clarity_db<'a>( + &'a mut self, + headers_db: &'a dyn HeadersDB, + burn_db: &'a dyn BurnStateDB, + ) -> ClarityDatabase<'a>; + fn get_analysis_db<'a>(&'a mut self) -> AnalysisDatabase<'a>; +} + +impl ClarityStorage for WritableMarfStore<'_> { + fn get_clarity_db<'a>( + &'a mut self, + headers_db: &'a dyn HeadersDB, + burn_db: &'a dyn BurnStateDB, + ) -> ClarityDatabase<'a> { + self.as_clarity_db(headers_db, burn_db) + } + + fn get_analysis_db<'a>(&'a mut self) -> AnalysisDatabase<'a> { + self.as_analysis_db() + } +} + +impl ClarityStorage for MemoryBackingStore { + fn get_clarity_db<'a>( + &'a mut self, + _headers_db: &'a dyn HeadersDB, + _burn_db: &'a dyn BurnStateDB, + ) -> ClarityDatabase<'a> { + self.as_clarity_db() + } + + fn get_analysis_db<'a>(&'a mut self) -> AnalysisDatabase<'a> { + self.as_analysis_db() + } +} + +fn run_analysis_free( contract_identifier: &QualifiedContractIdentifier, expressions: &mut [SymbolicExpression], - analysis_db: &mut AnalysisDatabase, + marf_kv: &mut C, save_contract: bool, -) -> CheckResult { +) -> Result { analysis::run_analysis( contract_identifier, expressions, - analysis_db, + &mut marf_kv.get_analysis_db(), save_contract, LimitedCostTracker::new_free(), ) - .map_err(|(e, _)| e) +} + +fn run_analysis( + contract_identifier: &QualifiedContractIdentifier, + expressions: &mut [SymbolicExpression], + header_db: &CLIHeadersDB, + marf_kv: &mut C, + save_contract: bool, +) -> Result { + let mainnet = header_db.is_mainnet(); + let cost_track = LimitedCostTracker::new( + mainnet, + if mainnet { + BLOCK_LIMIT_MAINNET.clone() + } else { + HELIUM_BLOCK_LIMIT.clone() + }, + &mut marf_kv.get_clarity_db(header_db, &NULL_BURN_STATE_DB), + ) + .unwrap(); + analysis::run_analysis( + contract_identifier, + expressions, + &mut marf_kv.get_analysis_db(), + save_contract, + cost_track, + ) } fn create_or_open_db(path: &String) -> Connection { - let open_flags = match fs::metadata(path) { - Err(e) => { - if e.kind() == io::ErrorKind::NotFound { - // need to create - OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE - } else { - panic!("FATAL: could not stat {}", path); + let open_flags = if path == ":memory:" { + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } else { + match fs::metadata(path) { + Err(e) => { + if e.kind() == io::ErrorKind::NotFound { + // need to create + if let Some(dirp) = PathBuf::from(path).parent() { + fs::create_dir_all(dirp).unwrap_or_else(|e| { + eprintln!("Failed to create {:?}: {:?}", dirp, &e); + panic_test!(); + }); + } + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } else { + panic!("FATAL: could not stat {}", path); + } + } + Ok(_md) => { + // can just open + OpenFlags::SQLITE_OPEN_READ_WRITE } - } - Ok(_md) => { - // can just open - OpenFlags::SQLITE_OPEN_READ_WRITE } }; @@ -209,47 +303,11 @@ fn get_cli_block_height(conn: &Connection, block_id: &StacksBlockId) -> Option (StacksBlockId, StacksBlockId) { - let mut conn = create_or_open_db(path); - let tx = friendly_expect( - conn.transaction(), - &format!("FATAL: failed to begin transaction on '{}'", path), - ); - - friendly_expect(tx.execute("CREATE TABLE IF NOT EXISTS cli_chain_tips(id INTEGER PRIMARY KEY AUTOINCREMENT, block_hash TEXT UNIQUE NOT NULL);", NO_PARAMS), - &format!("FATAL: failed to create 'cli_chain_tips' table")); - - let parent_block_hash = get_cli_chain_tip(&tx); - - let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); - let next_block_hash = friendly_expect_opt( - StacksBlockId::from_bytes(&random_bytes), - "Failed to generate random block header.", - ); - - friendly_expect( - tx.execute( - "INSERT INTO cli_chain_tips (block_hash) VALUES (?1)", - &[&next_block_hash], - ), - &format!("FATAL: failed to store next block hash in '{}'", path), - ); - - friendly_expect( - tx.commit(), - &format!("FATAL: failed to commit new chain tip to '{}'", path), - ); - - (parent_block_hash, next_block_hash) -} +fn get_cli_db_path(db_path: &str) -> String { + if db_path == ":memory:" { + return db_path.to_string(); + } -// This function is pretty weird! But it helps cut down on -// repeating a lot of block initialization for the simulation commands. -fn in_block(db_path: &str, mut marf_kv: MarfedKV, f: F) -> R -where - F: FnOnce(WritableMarfStore) -> (WritableMarfStore, R), -{ - // store CLI data alongside the MARF database state let mut cli_db_path_buf = PathBuf::from(db_path); cli_db_path_buf.push("cli.sqlite"); let cli_db_path = cli_db_path_buf @@ -259,13 +317,28 @@ where db_path )) .to_string(); + cli_db_path +} +// This function is pretty weird! But it helps cut down on +// repeating a lot of block initialization for the simulation commands. +fn in_block( + mut headers_db: CLIHeadersDB, + mut marf_kv: MarfedKV, + f: F, +) -> (CLIHeadersDB, MarfedKV, R) +where + F: FnOnce(CLIHeadersDB, WritableMarfStore) -> (CLIHeadersDB, WritableMarfStore, R), +{ // need to load the last block - let (from, to) = advance_cli_chain_tip(&cli_db_path); - let marf_tx = marf_kv.begin(&from, &to); - let (marf_return, result) = f(marf_tx); - marf_return.commit_to(&to); - result + let (from, to) = headers_db.advance_cli_chain_tip(); + let (headers_return, result) = { + let marf_tx = marf_kv.begin(&from, &to); + let (headers_return, marf_return, result) = f(headers_db, marf_tx); + marf_return.commit_to(&to); + (headers_return, result) + }; + (headers_return, marf_kv, result) } // like in_block, but does _not_ advance the chain tip. Used for read-only queries against the @@ -275,16 +348,7 @@ where F: FnOnce(WritableMarfStore) -> (WritableMarfStore, R), { // store CLI data alongside the MARF database state - let mut cli_db_path_buf = PathBuf::from(db_path); - cli_db_path_buf.push("cli.sqlite"); - let cli_db_path = cli_db_path_buf - .to_str() - .expect(&format!( - "FATAL: failed to convert '{}' to a string", - db_path - )) - .to_string(); - + let cli_db_path = get_cli_db_path(db_path); let cli_db_conn = create_or_open_db(&cli_db_path); let from = get_cli_chain_tip(&cli_db_conn); let to = StacksBlockId([2u8; 32]); // 0x0202020202 ... (pattern not used anywhere else) @@ -312,28 +376,142 @@ where struct CLIHeadersDB { db_path: String, + conn: Connection, } impl CLIHeadersDB { - pub fn new(db_path: &str) -> CLIHeadersDB { - CLIHeadersDB { + fn instantiate(&mut self, mainnet: bool) { + let cli_db_path = self.get_cli_db_path(); + let tx = friendly_expect( + self.conn.transaction(), + &format!("FATAL: failed to begin transaction on '{}'", cli_db_path), + ); + + friendly_expect( + tx.execute( + "CREATE TABLE IF NOT EXISTS cli_chain_tips(id INTEGER PRIMARY KEY AUTOINCREMENT, block_hash TEXT UNIQUE NOT NULL);", + NO_PARAMS + ), + &format!("FATAL: failed to create 'cli_chain_tips' table"), + ); + + friendly_expect( + tx.execute( + "CREATE TABLE IF NOT EXISTS cli_config(testnet BOOLEAN NOT NULL);", + NO_PARAMS, + ), + &format!("FATAL: failed to create 'cli_config' table"), + ); + + if !mainnet { + friendly_expect( + tx.execute("INSERT INTO cli_config (testnet) VALUES (?1)", &[&true]), + &format!("FATAL: failed to set testnet flag"), + ); + } + + friendly_expect( + tx.commit(), + &format!("FATAL: failed to instantiate CLI DB at {:?}", &cli_db_path), + ); + } + + /// Create or open a new CLI DB at db_path. If it already exists, then this method is a no-op. + pub fn new(db_path: &str, mainnet: bool) -> CLIHeadersDB { + let instantiate = db_path == ":memory:" || fs::metadata(&db_path).is_err(); + + let cli_db_path = get_cli_db_path(db_path); + let conn = create_or_open_db(&cli_db_path); + let mut db = CLIHeadersDB { db_path: db_path.to_string(), + conn: conn, + }; + + if instantiate { + db.instantiate(mainnet); } + db + } + + /// Open an CLI DB at db_path. Returns Err() if it doesn't exist. + /// Normally this would be Option<..>, but since this gets used with friendly_expect, + /// using a Result<..> is necessary. + pub fn resume(db_path: &str) -> Result { + let cli_db_path = get_cli_db_path(db_path); + if let Err(e) = fs::metadata(&cli_db_path) { + return Err(format!("Failed to access {:?}: {:?}", &cli_db_path, &e)); + } + let conn = create_or_open_db(&cli_db_path); + let db = CLIHeadersDB { + db_path: db_path.to_string(), + conn: conn, + }; + + Ok(db) } - pub fn open(&self) -> Connection { - let mut cli_db_path_buf = PathBuf::from(&self.db_path); - cli_db_path_buf.push("cli.sqlite"); - let cli_db_path = cli_db_path_buf - .to_str() - .expect(&format!( - "FATAL: failed to convert '{}' to a string", + /// Make a new CLI DB in memory. + pub fn new_memory(mainnet: bool) -> CLIHeadersDB { + let db = CLIHeadersDB::new(":memory:", mainnet); + db + } + + fn get_cli_db_path(&self) -> String { + get_cli_db_path(&self.db_path) + } + + pub fn conn(&self) -> &Connection { + &self.conn + } + + pub fn is_mainnet(&self) -> bool { + let mut stmt = friendly_expect( + self.conn.prepare("SELECT testnet FROM cli_config LIMIT 1"), + "FATAL: could not prepare query", + ); + let mut rows = friendly_expect(stmt.query(NO_PARAMS), "FATAL: could not fetch rows"); + let mut mainnet = true; + while let Some(row) = rows.next().expect("FATAL: could not read config row") { + let testnet: bool = row.get_unwrap("testnet"); + mainnet = !testnet; + } + mainnet + } + + pub fn advance_cli_chain_tip(&mut self) -> (StacksBlockId, StacksBlockId) { + let tx = friendly_expect( + self.conn.transaction(), + &format!("FATAL: failed to begin transaction on '{}'", &self.db_path), + ); + + let parent_block_hash = get_cli_chain_tip(&tx); + + let random_bytes = rand::thread_rng().gen::<[u8; 32]>(); + let next_block_hash = friendly_expect_opt( + StacksBlockId::from_bytes(&random_bytes), + "Failed to generate random block header.", + ); + + friendly_expect( + tx.execute( + "INSERT INTO cli_chain_tips (block_hash) VALUES (?1)", + &[&next_block_hash], + ), + &format!( + "FATAL: failed to store next block hash in '{}'", &self.db_path - )) - .to_string(); + ), + ); - let cli_db_conn = create_or_open_db(&cli_db_path); - cli_db_conn + friendly_expect( + tx.commit(), + &format!( + "FATAL: failed to commit new chain tip to '{}'", + &self.db_path + ), + ); + + (parent_block_hash, next_block_hash) } } @@ -343,7 +521,7 @@ impl HeadersDB for CLIHeadersDB { id_bhh: &StacksBlockId, ) -> Option { // mock it - let conn = self.open(); + let conn = self.conn(); if let Some(_) = get_cli_block_height(&conn, id_bhh) { let hash_bytes = Sha512Trunc256Sum::from_data(&id_bhh.0); Some(BurnchainHeaderHash(hash_bytes.0)) @@ -353,7 +531,7 @@ impl HeadersDB for CLIHeadersDB { } fn get_vrf_seed_for_block(&self, id_bhh: &StacksBlockId) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(_) = get_cli_block_height(&conn, id_bhh) { // mock it, but make it unique let hash_bytes = Sha512Trunc256Sum::from_data(&id_bhh.0); @@ -368,7 +546,7 @@ impl HeadersDB for CLIHeadersDB { &self, id_bhh: &StacksBlockId, ) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(_) = get_cli_block_height(&conn, id_bhh) { // mock it, but make it unique let hash_bytes = Sha512Trunc256Sum::from_data(&id_bhh.0); @@ -380,7 +558,7 @@ impl HeadersDB for CLIHeadersDB { } } fn get_burn_block_time_for_block(&self, id_bhh: &StacksBlockId) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(height) = get_cli_block_height(&conn, id_bhh) { Some((height * 600 + 1231006505) as u64) } else { @@ -388,7 +566,7 @@ impl HeadersDB for CLIHeadersDB { } } fn get_burn_block_height_for_block(&self, id_bhh: &StacksBlockId) -> Option { - let conn = self.open(); + let conn = self.conn(); if let Some(height) = get_cli_block_height(&conn, id_bhh) { Some(height as u32) } else { @@ -488,15 +666,95 @@ fn consume_arg( } } -pub fn invoke_command(invoked_by: &str, args: &[String]) { +fn install_boot_code(header_db: &CLIHeadersDB, marf: &mut C) { + let mainnet = header_db.is_mainnet(); + let boot_code = if mainnet { + *STACKS_BOOT_CODE_MAINNET + } else { + *STACKS_BOOT_CODE_TESTNET + }; + + for (boot_code_name, boot_code_contract) in boot_code.iter() { + let contract_identifier = QualifiedContractIdentifier::new( + boot_code_addr(mainnet).into(), + ContractName::try_from(boot_code_name.to_string()).unwrap(), + ); + let contract_content = *boot_code_contract; + + debug!( + "Instantiate boot code contract '{}.{}' ({} bytes)...", + &contract_identifier, + boot_code_name, + boot_code_contract.len() + ); + + let mut ast = friendly_expect( + parse(&contract_identifier, &contract_content), + "Failed to parse program.", + ); + + let analysis_result = run_analysis_free(&contract_identifier, &mut ast, marf, true); + match analysis_result { + Ok(_) => { + let db = marf.get_clarity_db(header_db, &NULL_BURN_STATE_DB); + let mut vm_env = OwnedEnvironment::new_free(mainnet, db); + vm_env + .initialize_contract(contract_identifier, &contract_content) + .unwrap(); + } + Err(_) => { + panic!("failed to instantiate boot contract"); + } + }; + } + + // set up PoX + let pox_contract = boot_code_id("pox", mainnet); + let sender = PrincipalData::from(pox_contract.clone()); + let pox_params = if mainnet { + PoxConstants::mainnet_default() + } else { + PoxConstants::testnet_default() + }; + + let params = vec![ + SymbolicExpression::atom_value(Value::UInt(0)), + SymbolicExpression::atom_value(Value::UInt(pox_params.prepare_length as u128)), + SymbolicExpression::atom_value(Value::UInt(pox_params.reward_cycle_length as u128)), + SymbolicExpression::atom_value(Value::UInt(pox_params.pox_rejection_fraction as u128)), + ]; + + let db = marf.get_clarity_db(header_db, &NULL_BURN_STATE_DB); + let mut vm_env = OwnedEnvironment::new_free(mainnet, db); + vm_env + .execute_transaction( + sender, + pox_contract, + "set-burnchain-parameters", + params.as_slice(), + ) + .unwrap(); +} + +/// Returns (process-exit-code, Option) +pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option) { if args.len() < 1 { - print_usage(invoked_by) + print_usage(invoked_by); + return (1, None); } match args[0].as_ref() { "initialize" => { - let (db_name, allocations) = if args.len() == 3 { - let filename = &args[1]; + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { + false + } else { + true + }; + + let (db_name, allocations) = if argv.len() == 3 { + let filename = &argv[1]; let json_in = if filename == "-" { let mut buffer = String::new(); friendly_expect( @@ -526,26 +784,38 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }) .collect(); - (&args[2], allocations) - } else if args.len() == 2 { - (&args[1], Vec::new()) + (&argv[2], allocations) + } else if argv.len() == 2 { + (&argv[1], Vec::new()) } else { eprintln!( - "Usage: {} {} (initial-allocations.json) [vm-state.db]", - invoked_by, args[0] + "Usage: {} {} [--testnet] (initial-allocations.json) [vm-state.db]", + invoked_by, argv[0] ); eprintln!(" initial-allocations.json is a JSON array of {{ principal: \"ST...\", amount: 100 }} like objects."); eprintln!(" if the provided filename is `-`, the JSON is read from stdin."); + eprintln!(" If --testnet is given, then testnet bootcode and block-limits are used instead of mainnet."); panic_test!(); }; - let marf_kv = + debug!("Initialize {}", &db_name); + let mut header_db = CLIHeadersDB::new(&db_name, mainnet); + let mut marf_kv = friendly_expect(MarfedKV::open(db_name, None), "Failed to open VM database."); - let header_db = CLIHeadersDB::new(&db_name); - in_block(db_name, marf_kv, |mut kv| { + + // install bootcode + let state = in_block(header_db, marf_kv, |header_db, mut marf| { + install_boot_code(&header_db, &mut marf); + (header_db, marf, ()) + }); + + header_db = state.0; + marf_kv = state.1; + + // set initial balances + in_block(header_db, marf_kv, |header_db, mut kv| { { let mut db = kv.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - db.initialize(); db.begin(); for (principal, amount) in allocations.iter() { let balance = STXBalance::initial(*amount as u128); @@ -559,9 +829,20 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } db.commit(); }; - (kv, ()) + (header_db, kv, ()) }); - println!("Database created."); + + if mainnet { + (0, Some(json!({ + "message": "Database created.", + "network": "mainnet" + }))) + } else { + (0, Some(json!({ + "message": "Database created.", + "network": "testnet" + }))) + } } "generate_address" => { // random 20 bytes @@ -569,12 +850,15 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { // version = 22 let addr = friendly_expect(c32_address(22, &random_bytes), "Failed to generate address"); - println!("{}", addr); + + (0, Some(json!({ + "address": format!("{}", addr) + }))) } "check" => { if args.len() < 2 { eprintln!( - "Usage: {} {} [program-file.clar] [--contract_id CONTRACT_ID] [--output_analysis] (vm-state.db)", + "Usage: {} {} [program-file.clar] [--contract_id CONTRACT_ID] [--output_analysis] [--costs] [--testnet] (vm-state.db)", invoked_by, args[0] ); panic_test!(); @@ -603,6 +887,21 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { panic_test!(); }; + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + // NOTE: ignored if we're using a DB + let mut testnet_given = false; + let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { + testnet_given = true; + false + } else { + true + }; + let content: String = if &argv[1] == "-" { let mut buffer = String::new(); friendly_expect( @@ -619,52 +918,72 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { let mut ast = friendly_expect(parse(&contract_id, &content), "Failed to parse program"); - let contract_analysis = { + let contract_analysis_res = { if argv.len() >= 3 { // use a persisted marf + if testnet_given { + eprintln!("WARN: ignoring --testnet in favor of DB state in {:?}. Re-instantiate the DB to change.", &argv[2]); + } + + let vm_filename = &argv[2]; + let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( - MarfedKV::open(&argv[2], None), + MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); + let result = at_chaintip(&argv[2], marf_kv, |mut marf| { - let result = { - let mut db = marf.as_analysis_db(); - run_analysis(&contract_id, &mut ast, &mut db, false) - }; + let result = run_analysis(&contract_id, &mut ast, &header_db, &mut marf, false); (marf, result) }); result } else { + let header_db = CLIHeadersDB::new_memory(mainnet); let mut analysis_marf = MemoryBackingStore::new(); - let mut db = analysis_marf.as_analysis_db(); - run_analysis(&contract_id, &mut ast, &mut db, false) + + install_boot_code(&header_db, &mut analysis_marf); + run_analysis(&contract_id, &mut ast, &header_db, &mut analysis_marf, false) } - } - .unwrap_or_else(|e| { - println!("{}", &e.diagnostic); - panic_test!(); - }); + }; + + let mut contract_analysis = match contract_analysis_res { + Ok(contract_analysis) => contract_analysis, + Err((e, cost_tracker)) => { + let mut result = json!({ + "message": "Checks failed.", + "error": { + "analysis": serde_json::to_value(&e.diagnostic).unwrap(), + } + }); + if costs { + result["costs"] = serde_json::to_value(&cost_tracker.get_total()).unwrap(); + } + return (1, Some(result)); + } + }; + let mut result = json!({ + "message": "Checks passed." + }); + if costs { + result["costs"] = serde_json::to_value(&contract_analysis.take_contract_cost_tracker().get_total()).unwrap(); + } if output_analysis { - println!( - "{}", - build_contract_interface(&contract_analysis).serialize() - ); - } else { - println!("Checks passed."); + result["analysis"] = serde_json::to_value(&build_contract_interface(&contract_analysis)).unwrap(); } + (0, Some(result)) } "repl" => { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { + false + } else { + true + }; let mut marf = MemoryBackingStore::new(); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - marf.as_clarity_db(), - LimitedCostTracker::new_free(), - ); + let mut vm_env = OwnedEnvironment::new_free(mainnet, marf.as_clarity_db()); let mut exec_env = vm_env.get_exec_environment(None); - let mut analysis_marf = MemoryBackingStore::new(); - let mut analysis_db = analysis_marf.as_analysis_db(); let contract_id = QualifiedContractIdentifier::transient(); @@ -696,9 +1015,9 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } }; - match run_analysis(&contract_id, &mut ast, &mut analysis_db, true) { + match run_analysis_free(&contract_id, &mut ast, &mut analysis_marf, true) { Ok(_) => (), - Err(error) => { + Err((error, _)) => { println!("Type check error:\n{}", error); continue; } @@ -716,6 +1035,12 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } } "eval_raw" => { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { + false + } else { + true + }; let content: String = { let mut buffer = String::new(); friendly_expect( @@ -726,51 +1051,55 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }; let mut analysis_marf = MemoryBackingStore::new(); - let mut analysis_db = analysis_marf.as_analysis_db(); - let mut marf = MemoryBackingStore::new(); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - marf.as_clarity_db(), - LimitedCostTracker::new_free(), - ); + let mut vm_env = OwnedEnvironment::new_free(mainnet, marf.as_clarity_db()); let contract_id = QualifiedContractIdentifier::transient(); let mut ast = friendly_expect(parse(&contract_id, &content), "Failed to parse program."); - match run_analysis(&contract_id, &mut ast, &mut analysis_db, true) { + match run_analysis_free(&contract_id, &mut ast, &mut analysis_marf, true) { Ok(_) => { let result = vm_env.get_exec_environment(None).eval_raw(&content); match result { Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + (0, Some(json!({ + "output": serde_json::to_value(&x).unwrap() + }))) } Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (1, Some(json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }))) } } } - Err(error) => { - eprintln!("Type check error.\n{}", error); - panic_test!(); + Err((error, _)) => { + (1, Some(json!({ + "error": { + "analysis": serde_json::to_value(&format!("{}", error)).unwrap() + } + }))) } } } "eval" => { let evalInput = get_eval_input(invoked_by, args); let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - let result = in_block(vm_filename, marf_kv, |mut marf| { + let mainnet = header_db.is_mainnet(); + let (_, _, result) = in_block(header_db, marf_kv, |header_db, mut marf| { let result = { let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); let mut vm_env = OwnedEnvironment::new_cost_limited( - false, + mainnet, db, LimitedCostTracker::new_free(), ); @@ -778,32 +1107,39 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { .get_exec_environment(None) .eval_read_only(&evalInput.contract_identifier, &evalInput.content) }; - (marf, result) + (header_db, marf, result) }); match result { Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + (0, Some(json!({ + "output": serde_json::to_value(&x).unwrap() + }))) } Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (1, Some(json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }))) } } } "eval_at_chaintip" => { let evalInput = get_eval_input(invoked_by, args); let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); + let mainnet = header_db.is_mainnet(); let result = at_chaintip(vm_filename, marf_kv, |mut marf| { let result = { let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); let mut vm_env = OwnedEnvironment::new_cost_limited( - false, + mainnet, db, LimitedCostTracker::new_free(), ); @@ -816,11 +1152,16 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { match result { Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + (0, Some(json!({ + "output": serde_json::to_value(&x).unwrap() + }))) } Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (1, Some(json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }))) } } } @@ -847,16 +1188,18 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }; let vm_filename = &args[3]; + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); + let mainnet = header_db.is_mainnet(); let result = at_block(chain_tip, marf_kv, |mut marf| { let result = { let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); let mut vm_env = OwnedEnvironment::new_cost_limited( - false, + mainnet, db, LimitedCostTracker::new_free(), ); @@ -869,108 +1212,175 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { match result { Ok(x) => { - println!("Program executed successfully! Output: \n{}", x); + (0, Some(json!({ + "output": serde_json::to_value(&x).unwrap() + }))) } Err(error) => { - eprintln!("Program execution error: \n{}", error); - panic_test!(); + (1, Some(json!({ + "error": { + "runtime": serde_json::to_value(&format!("{}", error)).unwrap() + } + }))) } } } "launch" => { - if args.len() < 4 { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + let assets = if let Ok(Some(_)) = consume_arg(&mut argv, &["--assets"], false) { + true + } else { + false + }; + let output_analysis = + if let Ok(Some(_)) = consume_arg(&mut argv, &["--output_analysis"], false) { + true + } else { + false + }; + if argv.len() < 4 { eprintln!( - "Usage: {} {} [contract-identifier] [contract-definition.clar] [vm-state.db]", - invoked_by, args[0] + "Usage: {} {} [--costs] [--assets] [--output_analysis] [contract-identifier] [contract-definition.clar] [vm-state.db]", + invoked_by, argv[0] ); panic_test!(); } - let vm_filename = &args[3]; + let vm_filename = &argv[3]; let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[1]), + QualifiedContractIdentifier::parse(&argv[1]), "Failed to parse contract identifier.", ); let contract_content: String = friendly_expect( - fs::read_to_string(&args[2]), - &format!("Error reading file: {}", args[2]), + fs::read_to_string(&argv[2]), + &format!("Error reading file: {}", argv[2]), ); let mut ast = friendly_expect( parse(&contract_identifier, &contract_content), "Failed to parse program.", ); + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - let result = in_block(vm_filename, marf_kv, |mut marf| { - let analysis_result = { - let mut db = AnalysisDatabase::new(&mut marf); - - run_analysis(&contract_identifier, &mut ast, &mut db, true) - }; + let mainnet = header_db.is_mainnet(); + let (_, _, result) = in_block(header_db, marf_kv, |header_db, mut marf| { + let analysis_result = + run_analysis(&contract_identifier, &mut ast, &header_db, &mut marf, true); match analysis_result { - Err(e) => (marf, Err(e)), + Err(e) => (header_db, marf, Err(e)), Ok(analysis) => { let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), - ); - vm_env.initialize_contract(contract_identifier, &contract_content) + let mut db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); + let cost_track = LimitedCostTracker::new( + mainnet, + if mainnet { + BLOCK_LIMIT_MAINNET.clone() + } else { + HELIUM_BLOCK_LIMIT.clone() + }, + &mut db, + ) + .unwrap(); + let mut vm_env = + OwnedEnvironment::new_cost_limited(mainnet, db, cost_track); + let result = + vm_env.initialize_contract(contract_identifier, &contract_content); + let cost = vm_env.get_cost_total(); + (result, cost) }; - (marf, Ok((analysis, result))) + (header_db, marf, Ok((analysis, result))) } } }); match result { - Ok((contract_analysis, Ok(_x))) => match args.last() { - Some(s) if s == "--output_analysis" => { - println!( - "{}", - build_contract_interface(&contract_analysis).serialize() - ); + Ok((contract_analysis, (Ok((_x, asset_map, events)), cost))) => { + let mut result = json!({ + "message": "Contract initialized!" + }); + + if costs { + result["costs"] = serde_json::to_value(&cost).unwrap(); } - _ => { - println!("Contract initialized!"); + if assets { + result["assets"] = asset_map.to_json(); } - }, - Err(error) => { - eprintln!("Contract initialization error: \n{}", error); - panic_test!(); + if output_analysis { + result["analysis"] = serde_json::to_value(&build_contract_interface(&contract_analysis)).unwrap(); + } + let events_json : Vec<_> = events + .into_iter() + .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true)) + .collect(); + + result["events"] = serde_json::Value::Array(events_json); + (0, Some(result)) } - Ok((_, Err(error))) => { - eprintln!("Contract initialization error: \n{}", error); - panic_test!(); + Err((error, cost_tracker)) => { + let mut result = json!({ + "error": { + "initialization": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + if costs { + result["costs"] = serde_json::to_value(&cost_tracker.get_total()).unwrap() + } + (1, Some(result)) + } + Ok((_, (Err(error), ..))) => { + (1, Some(json!({ + "error": { + "initialization": serde_json::to_value(&format!("{}", error)).unwrap() + } + }))) } } } "execute" => { - if args.len() < 5 { - eprintln!("Usage: {} {} [vm-state.db] [contract-identifier] [public-function-name] [sender-address] [args...]", invoked_by, args[0]); + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + let assets = if let Ok(Some(_)) = consume_arg(&mut argv, &["--assets"], false) { + true + } else { + false + }; + + if argv.len() < 5 { + eprintln!("Usage: {} {} [--costs] [--assets] [vm-state.db] [contract-identifier] [public-function-name] [sender-address] [args...]", invoked_by, argv[0]); panic_test!(); } - let vm_filename = &args[1]; + + let vm_filename = &argv[1]; + let header_db = + friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( MarfedKV::open(vm_filename, None), "Failed to open VM database.", ); - let header_db = CLIHeadersDB::new(&vm_filename); - + let mainnet = header_db.is_mainnet(); let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[2]), + QualifiedContractIdentifier::parse(&argv[2]), "Failed to parse contract identifier.", ); - let tx_name = &args[3]; - let sender_in = &args[4]; + let tx_name = &argv[3]; + let sender_in = &argv[4]; let sender = { if let Ok(sender) = PrincipalData::parse_standard_principal(sender_in) { @@ -981,7 +1391,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { } }; - let arguments: Vec<_> = args[5..] + let arguments: Vec<_> = argv[5..] .iter() .map(|argument| { let argument_parsed = friendly_expect( @@ -996,50 +1406,95 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) { }) .collect(); - let result = in_block(vm_filename, marf_kv, |mut marf| { - let result = { - let db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let mut vm_env = OwnedEnvironment::new_cost_limited( - false, - db, - LimitedCostTracker::new_free(), + let (_, _, result) = in_block(header_db, marf_kv, |header_db, mut marf| { + let x = { + let mut db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); + let cost_track = LimitedCostTracker::new( + mainnet, + if mainnet { + BLOCK_LIMIT_MAINNET.clone() + } else { + HELIUM_BLOCK_LIMIT.clone() + }, + &mut db, + ) + .unwrap(); + let mut vm_env = OwnedEnvironment::new_cost_limited(mainnet, db, cost_track); + let result = vm_env.execute_transaction( + sender, + contract_identifier, + &tx_name, + &arguments, ); - vm_env.execute_transaction(sender, contract_identifier, &tx_name, &arguments) + let cost = vm_env.get_cost_total(); + (result, cost) }; - (marf, result) + (header_db, marf, (x.0, x.1)) }); match result { - Ok((x, _, events)) => { + (Ok((x, asset_map, events)), cost) => { if let Value::Response(data) = x { if data.committed { - println!( - "Transaction executed and committed. Returned: {}\n{:?}", - data.data, events - ); + let mut result = json!({ + "message": "Transaction executed and committed.", + "output": serde_json::to_value(&data.data).unwrap(), + }); + if costs { + result["costs"] = serde_json::to_value(&cost).unwrap(); + } + if assets { + result["assets"] = asset_map.to_json(); + } + let events_json : Vec<_> = events + .into_iter() + .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true)) + .collect(); + + result["events"] = serde_json::Value::Array(events_json); + (0, Some(result)) } else { - println!("Aborted: {}", data.data); + let mut result = json!({ + "message": "Aborted.", + "output": serde_json::to_value(&data.data).unwrap(), + }); + if costs { + result["costs"] = serde_json::to_value(&cost).unwrap(); + } + (0, Some(result)) } } else { - panic!(format!( - "Expected a ResponseType result from transaction. Found: {}", - x - )); + let result = json!({ + "error": { + "runtime": "Expected a ResponseType result from transaction.", + "output": serde_json::to_value(&x).unwrap() + } + }); + (1, Some(result)) } } - Err(error) => { - eprintln!("Transaction execution error: \n{}", error); - panic_test!(); + (Err(error), _) => { + let result = json!({ + "error": { + "runtime": "Transaction execution error.", + "error": serde_json::to_value(&format!("{}", error)).unwrap() + } + }); + (1, Some(result)) } } } - _ => print_usage(invoked_by), + _ => { + print_usage(invoked_by); + (1, None) + } } } #[cfg(test)] mod test { use super::*; + #[test] fn test_initial_alloc() { let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); @@ -1062,12 +1517,17 @@ mod test { (unwrap-panic (if (is-eq (stx-get-balance 'S1G2081040G2081040G2081040G208105NK8PE5.names) u2000) (ok 1) (err 2))) "#).unwrap(); - invoke_command( + let invoked = invoke_command( "test", &["initialize".to_string(), json_name.clone(), db_name.clone()], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); - invoke_command( + assert_eq!(exit, 0); + assert_eq!(result["network"], "mainnet"); + + let invoked = invoke_command( "test", &[ "launch".to_string(), @@ -1076,6 +1536,47 @@ mod test { db_name, ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + } + + #[test] + fn test_init_mainnet() { + let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); + let invoked = invoke_command("test", &["initialize".to_string(), db_name.clone()]); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["network"], "mainnet"); + + let header_db = CLIHeadersDB::new(&db_name, true); + assert!(header_db.is_mainnet()); + } + + #[test] + fn test_init_testnet() { + let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); + let invoked = invoke_command( + "test", + &[ + "initialize".to_string(), + "--testnet".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["network"], "testnet"); + + let header_db = CLIHeadersDB::new(&db_name, true); + assert!(!header_db.is_mainnet()); } #[test] @@ -1086,7 +1587,7 @@ mod test { invoke_command("test", &["initialize".to_string(), db_name.clone()]); eprintln!("check tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1094,8 +1595,14 @@ mod test { ], ); - eprintln!("check tokens"); - invoke_command( + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + + eprintln!("check tokens (idempotency)"); + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1103,9 +1610,15 @@ mod test { db_name.clone(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); eprintln!("launch tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "launch".to_string(), @@ -1115,8 +1628,14 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + eprintln!("check names"); - invoke_command( + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1124,9 +1643,15 @@ mod test { db_name.clone(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); eprintln!("check names with different contract ID"); - invoke_command( + let invoked = invoke_command( "test", &[ "check".to_string(), @@ -1136,20 +1661,72 @@ mod test { "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + + eprintln!("check names with analysis"); + let invoked = invoke_command( + "test", + &[ + "check".to_string(), + "--output_analysis".to_string(), + "sample-contracts/names.clar".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["analysis"] != json!(null)); + + eprintln!("check names with cost"); + let invoked = invoke_command( + "test", + &[ + "check".to_string(), + "--costs".to_string(), + "sample-contracts/names.clar".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); - eprintln!("launch names"); - invoke_command( + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["costs"] != json!(null)); + + eprintln!("launch names with costs and assets"); + let invoked = invoke_command( "test", &[ "launch".to_string(), "S1G2081040G2081040G2081040G208105NK8PE5.names".to_string(), "sample-contracts/names.clar".to_string(), + "--costs".to_string(), + "--assets".to_string(), db_name.clone(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["costs"] != json!(null)); + assert!(result["assets"] != json!(null)); eprintln!("execute tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "execute".to_string(), @@ -1160,9 +1737,17 @@ mod test { "(+ u900 u100)".to_string(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["events"].as_array().unwrap().len() == 0); + assert_eq!(result["output"], json!({"UInt": 1000})); eprintln!("eval tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "eval".to_string(), @@ -1172,8 +1757,21 @@ mod test { ], ); + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["output"], json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + })); + eprintln!("eval_at_chaintip tokens"); - invoke_command( + let invoked = invoke_command( "test", &[ "eval_at_chaintip".to_string(), @@ -1182,5 +1780,99 @@ mod test { db_name.clone(), ], ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!(result["output"], json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + })); + } + + #[test] + fn test_assets() { + let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); + + eprintln!("initialize"); + invoke_command("test", &["initialize".to_string(), db_name.clone()]); + + eprintln!("check tokens"); + let invoked = invoke_command( + "test", + &[ + "check".to_string(), + "sample-contracts/tokens-ft.clar".to_string(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + + eprintln!("launch tokens"); + let invoked = invoke_command( + "test", + &[ + "launch".to_string(), + "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft".to_string(), + "sample-contracts/tokens-ft.clar".to_string(), + db_name.clone(), + "--assets".to_string(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + eprintln!("{}", serde_json::to_string(&result).unwrap()); + + assert_eq!(exit, 0); + assert!(result["message"].as_str().unwrap().len() > 0); + assert!(result["assets"]["tokens"]["S1G2081040G2081040G2081040G208105NK8PE5"]["S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens"] == "10300"); + assert!(result["events"].as_array().unwrap().len() == 3); + assert!(result["events"].as_array().unwrap()[0] == json!({ + "committed": true, + "event_index": 0, + "ft_mint_event": { + "amount": "10300", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_mint_event" + })); + assert!(result["events"].as_array().unwrap()[1] == json!({ + "committed": true, + "event_index": 0, + "ft_transfer_event": { + "amount": "10000", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", + "sender": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_transfer_event" + })); + assert!(result["events"].as_array().unwrap()[2] == json!({ + "committed": true, + "event_index": 0, + "ft_transfer_event": { + "amount": "300", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G", + "sender": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_transfer_event" + })); + } } From d56927395566c5fdb0fd2dc329789592092778ee Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 14:07:20 -0400 Subject: [PATCH 04/13] use invoke_command()'s returned process exit code and JSON --- src/clarity_cli.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/clarity_cli.rs b/src/clarity_cli.rs index a4dfcc13f6..809bd7ce2c 100644 --- a/src/clarity_cli.rs +++ b/src/clarity_cli.rs @@ -21,12 +21,23 @@ #![allow(non_upper_case_globals)] extern crate blockstack_lib; +extern crate serde_json; use blockstack_lib::{clarity, util::log}; use std::env; +use std::process; fn main() { let argv: Vec = env::args().collect(); - clarity::invoke_command(&argv[0], &argv[1..]); + let result = clarity::invoke_command(&argv[0], &argv[1..]); + match result { + (exit_code, Some(output)) => { + println!("{}", &serde_json::to_string(&output).unwrap()); + process::exit(exit_code); + } + (exit_code, None) => { + process::exit(exit_code); + } + } } From 5a5212169b26d97980e65617cdd21d5239f1d095 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 14:07:34 -0400 Subject: [PATCH 05/13] new sample contracts that use the native fungible token type (for testing asset movement in clarity-cli) --- sample-contracts/tokens-ft-mint.clar | 5 +++++ sample-contracts/tokens-ft.clar | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 sample-contracts/tokens-ft-mint.clar create mode 100644 sample-contracts/tokens-ft.clar diff --git a/sample-contracts/tokens-ft-mint.clar b/sample-contracts/tokens-ft-mint.clar new file mode 100644 index 0000000000..6477f3f5d0 --- /dev/null +++ b/sample-contracts/tokens-ft-mint.clar @@ -0,0 +1,5 @@ +(begin + (as-contract + (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens mint! u100) + ) +) \ No newline at end of file diff --git a/sample-contracts/tokens-ft.clar b/sample-contracts/tokens-ft.clar new file mode 100644 index 0000000000..140d3e7ee8 --- /dev/null +++ b/sample-contracts/tokens-ft.clar @@ -0,0 +1,16 @@ +(define-fungible-token tokens) +(define-private (get-balance (account principal)) + (ft-get-balance tokens account)) + +(define-private (token-credit! (account principal) (amount uint)) + (ft-mint? tokens amount account)) + +(define-public (token-transfer (to principal) (amount uint)) + (ft-transfer? tokens amount tx-sender to)) + +(define-public (mint! (amount uint)) + (token-credit! tx-sender amount)) + +(token-credit! tx-sender u10300) +(token-transfer 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR u10000) +(token-transfer 'SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G u300) From 4655ff63d9c0c3b79884fd80e78b458b7a790e76 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 14:07:58 -0400 Subject: [PATCH 06/13] cargo fmt --- src/clarity.rs | 312 ++++++++++++++++++++++++++------------------- src/vm/contexts.rs | 57 +++++++-- 2 files changed, 222 insertions(+), 147 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index d1540d31bd..2f417699b9 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -833,15 +833,21 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { @@ -851,9 +857,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { if args.len() < 2 { @@ -926,14 +930,16 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option (i32, Option (i32, Option (i32, Option { let result = vm_env.get_exec_environment(None).eval_raw(&content); match result { - Ok(x) => { - (0, Some(json!({ + Ok(x) => ( + 0, + Some(json!({ "output": serde_json::to_value(&x).unwrap() - }))) - } - Err(error) => { - (1, Some(json!({ + })), + ), + Err(error) => ( + 1, + Some(json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - }))) - } + })), + ), } } - Err((error, _)) => { - (1, Some(json!({ + Err((error, _)) => ( + 1, + Some(json!({ "error": { "analysis": serde_json::to_value(&format!("{}", error)).unwrap() } - }))) - } + })), + ), } } "eval" => { @@ -1111,18 +1130,20 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - (0, Some(json!({ + Ok(x) => ( + 0, + Some(json!({ "output": serde_json::to_value(&x).unwrap() - }))) - } - Err(error) => { - (1, Some(json!({ + })), + ), + Err(error) => ( + 1, + Some(json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - }))) - } + })), + ), } } "eval_at_chaintip" => { @@ -1151,18 +1172,20 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - (0, Some(json!({ + Ok(x) => ( + 0, + Some(json!({ "output": serde_json::to_value(&x).unwrap() - }))) - } - Err(error) => { - (1, Some(json!({ + })), + ), + Err(error) => ( + 1, + Some(json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - }))) - } + })), + ), } } "eval_at_block" => { @@ -1211,18 +1234,20 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - (0, Some(json!({ + Ok(x) => ( + 0, + Some(json!({ "output": serde_json::to_value(&x).unwrap() - }))) - } - Err(error) => { - (1, Some(json!({ + })), + ), + Err(error) => ( + 1, + Some(json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - }))) - } + })), + ), } } "launch" => { @@ -1317,9 +1342,11 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option = events + let events_json: Vec<_> = events .into_iter() .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true)) .collect(); @@ -1338,13 +1365,14 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - (1, Some(json!({ + Ok((_, (Err(error), ..))) => ( + 1, + Some(json!({ "error": { "initialization": serde_json::to_value(&format!("{}", error)).unwrap() } - }))) - } + })), + ), } } "execute" => { @@ -1446,7 +1474,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option = events + let events_json: Vec<_> = events .into_iter() .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true)) .collect(); @@ -1546,10 +1574,10 @@ mod test { fn test_init_mainnet() { let db_name = format!("/tmp/db_{}", rand::thread_rng().gen::()); let invoked = invoke_command("test", &["initialize".to_string(), db_name.clone()]); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); assert_eq!(result["network"], "mainnet"); @@ -1568,10 +1596,10 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); assert_eq!(result["network"], "testnet"); @@ -1610,7 +1638,7 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); @@ -1643,7 +1671,7 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); @@ -1661,13 +1689,13 @@ mod test { "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); assert_eq!(exit, 0); assert!(result["message"].as_str().unwrap().len() > 0); - + eprintln!("check names with analysis"); let invoked = invoke_command( "test", @@ -1678,14 +1706,14 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); assert_eq!(exit, 0); assert!(result["message"].as_str().unwrap().len() > 0); assert!(result["analysis"] != json!(null)); - + eprintln!("check names with cost"); let invoked = invoke_command( "test", @@ -1696,7 +1724,7 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); @@ -1716,10 +1744,10 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); assert!(result["message"].as_str().unwrap().len() > 0); assert!(result["costs"] != json!(null)); @@ -1737,10 +1765,10 @@ mod test { "(+ u900 u100)".to_string(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); assert!(result["message"].as_str().unwrap().len() > 0); assert!(result["events"].as_array().unwrap().len() == 0); @@ -1759,16 +1787,19 @@ mod test { let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); - assert_eq!(result["output"], json!({ - "Response": { - "committed": true, - "data": { - "UInt": 100 + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } } - } - })); + }) + ); eprintln!("eval_at_chaintip tokens"); let invoked = invoke_command( @@ -1780,19 +1811,22 @@ mod test { db_name.clone(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); - assert_eq!(result["output"], json!({ - "Response": { - "committed": true, - "data": { - "UInt": 100 + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } } - } - })); + }) + ); } #[test] @@ -1810,10 +1844,10 @@ mod test { "sample-contracts/tokens-ft.clar".to_string(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + assert_eq!(exit, 0); assert!(result["message"].as_str().unwrap().len() > 0); @@ -1828,51 +1862,63 @@ mod test { "--assets".to_string(), ], ); - + let exit = invoked.0; let result = invoked.1.unwrap(); - + eprintln!("{}", serde_json::to_string(&result).unwrap()); - + assert_eq!(exit, 0); assert!(result["message"].as_str().unwrap().len() > 0); - assert!(result["assets"]["tokens"]["S1G2081040G2081040G2081040G208105NK8PE5"]["S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens"] == "10300"); + assert!( + result["assets"]["tokens"]["S1G2081040G2081040G2081040G208105NK8PE5"] + ["S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens"] + == "10300" + ); assert!(result["events"].as_array().unwrap().len() == 3); - assert!(result["events"].as_array().unwrap()[0] == json!({ - "committed": true, - "event_index": 0, - "ft_mint_event": { - "amount": "10300", - "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", - "recipient": "S1G2081040G2081040G2081040G208105NK8PE5" - }, - "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", - "type": "ft_mint_event" - })); - assert!(result["events"].as_array().unwrap()[1] == json!({ - "committed": true, - "event_index": 0, - "ft_transfer_event": { - "amount": "10000", - "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", - "recipient": "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", - "sender": "S1G2081040G2081040G2081040G208105NK8PE5" - }, - "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", - "type": "ft_transfer_event" - })); - assert!(result["events"].as_array().unwrap()[2] == json!({ - "committed": true, - "event_index": 0, - "ft_transfer_event": { - "amount": "300", - "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", - "recipient": "SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G", - "sender": "S1G2081040G2081040G2081040G208105NK8PE5" - }, - "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", - "type": "ft_transfer_event" - })); - + assert!( + result["events"].as_array().unwrap()[0] + == json!({ + "committed": true, + "event_index": 0, + "ft_mint_event": { + "amount": "10300", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_mint_event" + }) + ); + assert!( + result["events"].as_array().unwrap()[1] + == json!({ + "committed": true, + "event_index": 0, + "ft_transfer_event": { + "amount": "10000", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR", + "sender": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_transfer_event" + }) + ); + assert!( + result["events"].as_array().unwrap()[2] + == json!({ + "committed": true, + "event_index": 0, + "ft_transfer_event": { + "amount": "300", + "asset_identifier": "S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft::tokens", + "recipient": "SM2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQVX8X0G", + "sender": "S1G2081040G2081040G2081040G208105NK8PE5" + }, + "txid": "0x0000000000000000000000000000000000000000000000000000000000000000", + "type": "ft_transfer_event" + }) + ); } } diff --git a/src/vm/contexts.rs b/src/vm/contexts.rs index d038a61bde..e91b55d171 100644 --- a/src/vm/contexts.rs +++ b/src/vm/contexts.rs @@ -93,44 +93,74 @@ pub struct AssetMap { impl AssetMap { pub fn to_json(&self) -> serde_json::Value { - let stx : serde_json::map::Map<_, _> = self.stx_map + let stx: serde_json::map::Map<_, _> = self + .stx_map .iter() - .map(|(principal, amount)| (format!("{}", principal), serde_json::value::Value::String(format!("{}", amount)))) + .map(|(principal, amount)| { + ( + format!("{}", principal), + serde_json::value::Value::String(format!("{}", amount)), + ) + }) .collect(); - let burns : serde_json::map::Map<_, _> = self.burn_map + let burns: serde_json::map::Map<_, _> = self + .burn_map .iter() - .map(|(principal, amount)| (format!("{}", principal), serde_json::value::Value::String(format!("{}", amount)))) + .map(|(principal, amount)| { + ( + format!("{}", principal), + serde_json::value::Value::String(format!("{}", amount)), + ) + }) .collect(); - let tokens : serde_json::map::Map<_, _> = self.token_map + let tokens: serde_json::map::Map<_, _> = self + .token_map .iter() .map(|(principal, token_map)| { - let token_json : serde_json::map::Map<_, _> = token_map + let token_json: serde_json::map::Map<_, _> = token_map .iter() - .map(|(asset_id, amount)| (format!("{}", asset_id), serde_json::value::Value::String(format!("{}", amount)))) + .map(|(asset_id, amount)| { + ( + format!("{}", asset_id), + serde_json::value::Value::String(format!("{}", amount)), + ) + }) .collect(); - (format!("{}", principal), serde_json::value::Value::Object(token_json)) + ( + format!("{}", principal), + serde_json::value::Value::Object(token_json), + ) }) .collect(); - let assets : serde_json::map::Map<_, _> = self.asset_map + let assets: serde_json::map::Map<_, _> = self + .asset_map .iter() .map(|(principal, nft_map)| { - let nft_json : serde_json::map::Map<_, _> = nft_map + let nft_json: serde_json::map::Map<_, _> = nft_map .iter() .map(|(asset_id, nft_values)| { let nft_array = nft_values .iter() - .map(|nft_value| serde_json::value::Value::String(format!("{}", nft_value))) + .map(|nft_value| { + serde_json::value::Value::String(format!("{}", nft_value)) + }) .collect(); - (format!("{}", asset_id), serde_json::value::Value::Array(nft_array)) + ( + format!("{}", asset_id), + serde_json::value::Value::Array(nft_array), + ) }) .collect(); - (format!("{}", principal), serde_json::value::Value::Object(nft_json)) + ( + format!("{}", principal), + serde_json::value::Value::Object(nft_json), + ) }) .collect(); @@ -143,7 +173,6 @@ impl AssetMap { } } - #[derive(Debug, Clone)] pub struct EventBatch { pub events: Vec, From da0059ba68e076e45807ccc907317d35ec3d0ee9 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 21:30:30 -0400 Subject: [PATCH 07/13] extend --costs to eval* --- src/clarity.rs | 295 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 213 insertions(+), 82 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index 2f417699b9..a000926f08 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -581,7 +581,7 @@ impl HeadersDB for CLIHeadersDB { fn get_eval_input(invoked_by: &str, args: &[String]) -> EvalInput { if args.len() < 3 || args.len() > 4 { eprintln!( - "Usage: {} {} [contract-identifier] (program.clar) [vm-state.db]", + "Usage: {} {} [--costs] [contract-identifier] (program.clar) [vm-state.db]", invoked_by, args[0] ); panic_test!(); @@ -1051,12 +1051,6 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { - let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); - let mainnet = if let Ok(Some(_)) = consume_arg(&mut argv, &["--testnet"], false) { - false - } else { - true - }; let content: String = { let mut buffer = String::new(); friendly_expect( @@ -1068,7 +1062,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option (i32, Option { - let evalInput = get_eval_input(invoked_by, args); - let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + let evalInput = get_eval_input(invoked_by, &argv); + let vm_filename = if argv.len() == 3 { &argv[2] } else { &argv[3] }; let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( @@ -1114,41 +1116,68 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option ( - 0, - Some(json!({ - "output": serde_json::to_value(&x).unwrap() - })), - ), - Err(error) => ( - 1, - Some(json!({ + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap() + }); + + if costs { + result_json["costs"] = serde_json::to_value(&cost).unwrap(); + } + + (0, Some(result_json)) + } + (Err(error), cost) => { + let mut result_json = json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - })), - ), + }); + + if costs { + result_json["costs"] = serde_json::to_value(&cost).unwrap(); + } + + (1, Some(result_json)) + } } } "eval_at_chaintip" => { - let evalInput = get_eval_input(invoked_by, args); - let vm_filename = if args.len() == 3 { &args[2] } else { &args[3] }; + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + let evalInput = get_eval_input(invoked_by, &argv); + let vm_filename = if argv.len() == 3 { &argv[2] } else { &argv[3] }; let header_db = friendly_expect(CLIHeadersDB::resume(vm_filename), "Failed to open CLI DB"); let marf_kv = friendly_expect( @@ -1156,49 +1185,75 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option ( - 0, - Some(json!({ - "output": serde_json::to_value(&x).unwrap() - })), - ), - Err(error) => ( - 1, - Some(json!({ + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap() + }); + + if costs { + result_json["costs"] = serde_json::to_value(&cost).unwrap(); + } + + (0, Some(result_json)) + } + (Err(error), cost) => { + let mut result_json = json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - })), - ), + }); + + if costs { + result_json["costs"] = serde_json::to_value(&cost).unwrap(); + } + + (1, Some(result_json)) + } } } "eval_at_block" => { - if args.len() != 4 { + let mut argv: Vec = args.into_iter().map(|x| x.clone()).collect(); + + let costs = if let Ok(Some(_)) = consume_arg(&mut argv, &["--costs"], false) { + true + } else { + false + }; + + if argv.len() != 4 { eprintln!( - "Usage: {} {} [index-block-hash] [contract-identifier] [vm/clarity dir]", - invoked_by, &args[0] + "Usage: {} {} [--costs] [index-block-hash] [contract-identifier] [vm/clarity dir]", + invoked_by, &argv[0] ); panic_test!(); } - let chain_tip = &args[1]; + let chain_tip = &argv[1]; let contract_identifier = friendly_expect( - QualifiedContractIdentifier::parse(&args[2]), + QualifiedContractIdentifier::parse(&argv[2]), "Failed to parse contract identifier.", ); let content: String = { @@ -1210,7 +1265,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option (i32, Option ( - 0, - Some(json!({ - "output": serde_json::to_value(&x).unwrap() - })), - ), - Err(error) => ( - 1, - Some(json!({ + match result_and_cost { + (Ok(result), cost) => { + let mut result_json = json!({ + "output": serde_json::to_value(&result).unwrap() + }); + + if costs { + result_json["costs"] = serde_json::to_value(&cost).unwrap(); + } + + (0, Some(result_json)) + } + (Err(error), cost) => { + let mut result_json = json!({ "error": { "runtime": serde_json::to_value(&format!("{}", error)).unwrap() } - })), - ), + }); + + if costs { + result_json["costs"] = serde_json::to_value(&cost).unwrap(); + } + + (1, Some(result_json)) + } } } "launch" => { @@ -1801,6 +1874,35 @@ mod test { }) ); + eprintln!("eval tokens with cost"); + let invoked = invoke_command( + "test", + &[ + "eval".to_string(), + "--costs".to_string(), + "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), + "sample-contracts/tokens-mint.clar".to_string(), + db_name.clone(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + }) + ); + assert!(result["costs"] != json!(null)); + eprintln!("eval_at_chaintip tokens"); let invoked = invoke_command( "test", @@ -1827,6 +1929,35 @@ mod test { } }) ); + + eprintln!("eval_at_chaintip tokens with cost"); + let invoked = invoke_command( + "test", + &[ + "eval_at_chaintip".to_string(), + "S1G2081040G2081040G2081040G208105NK8PE5.tokens".to_string(), + "sample-contracts/tokens-mint.clar".to_string(), + db_name.clone(), + "--costs".to_string(), + ], + ); + + let exit = invoked.0; + let result = invoked.1.unwrap(); + + assert_eq!(exit, 0); + assert_eq!( + result["output"], + json!({ + "Response": { + "committed": true, + "data": { + "UInt": 100 + } + } + }) + ); + assert!(result["costs"] != json!(null)); } #[test] From 7513b9d760a2c082b3cf2749516eca964204b199 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 22:08:01 -0400 Subject: [PATCH 08/13] invoke tokens-ft --- sample-contracts/tokens-mint.clar | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-contracts/tokens-mint.clar b/sample-contracts/tokens-mint.clar index 6477f3f5d0..0e82447390 100644 --- a/sample-contracts/tokens-mint.clar +++ b/sample-contracts/tokens-mint.clar @@ -1,5 +1,5 @@ (begin (as-contract - (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens mint! u100) + (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft mint! u100) ) -) \ No newline at end of file +) From 91c7a03a86e5d6f46392455c642e0d4c5169e568 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 20 Apr 2021 22:30:17 -0400 Subject: [PATCH 09/13] whoops -- revert contract-calls --- sample-contracts/tokens-ft-mint.clar | 4 ++-- sample-contracts/tokens-mint.clar | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sample-contracts/tokens-ft-mint.clar b/sample-contracts/tokens-ft-mint.clar index 6477f3f5d0..0e82447390 100644 --- a/sample-contracts/tokens-ft-mint.clar +++ b/sample-contracts/tokens-ft-mint.clar @@ -1,5 +1,5 @@ (begin (as-contract - (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens mint! u100) + (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft mint! u100) ) -) \ No newline at end of file +) diff --git a/sample-contracts/tokens-mint.clar b/sample-contracts/tokens-mint.clar index 0e82447390..19a7cf727e 100644 --- a/sample-contracts/tokens-mint.clar +++ b/sample-contracts/tokens-mint.clar @@ -1,5 +1,5 @@ (begin (as-contract - (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens-ft mint! u100) + (contract-call? 'S1G2081040G2081040G2081040G208105NK8PE5.tokens mint! u100) ) ) From c6276127bc3203911a45a4af5914323835d89ac7 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 22 Apr 2021 00:02:14 -0400 Subject: [PATCH 10/13] Address feedback --- src/clarity.rs | 259 ++++++++++++++++++++----------------------------- 1 file changed, 103 insertions(+), 156 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index a000926f08..e76e02f5b2 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -45,7 +45,7 @@ use vm::analysis; use vm::analysis::contract_interface_builder::build_contract_interface; use vm::analysis::{errors::CheckError, errors::CheckResult, AnalysisDatabase, ContractAnalysis}; use vm::ast::build_ast; -use vm::contexts::OwnedEnvironment; +use vm::contexts::{AssetMap, OwnedEnvironment}; use vm::costs::ExecutionCost; use vm::costs::LimitedCostTracker; use vm::database::{ @@ -374,6 +374,32 @@ where result } +fn with_env_costs( + mainnet: bool, + header_db: &CLIHeadersDB, + marf: &mut WritableMarfStore, + f: F, +) -> (R, ExecutionCost) +where + F: FnOnce(&mut OwnedEnvironment) -> R, +{ + let mut db = marf.as_clarity_db(header_db, &NULL_BURN_STATE_DB); + let cost_track = LimitedCostTracker::new( + mainnet, + if mainnet { + BLOCK_LIMIT_MAINNET.clone() + } else { + HELIUM_BLOCK_LIMIT.clone() + }, + &mut db, + ) + .unwrap(); + let mut vm_env = OwnedEnvironment::new_cost_limited(mainnet, db, cost_track); + let result = f(&mut vm_env); + let cost = vm_env.get_cost_total(); + (result, cost) +} + struct CLIHeadersDB { db_path: String, conn: Connection, @@ -718,7 +744,7 @@ fn install_boot_code(header_db: &CLIHeadersDB, marf: &mut C) }; let params = vec![ - SymbolicExpression::atom_value(Value::UInt(0)), + SymbolicExpression::atom_value(Value::UInt(0)), // first burnchain block height SymbolicExpression::atom_value(Value::UInt(pox_params.prepare_length as u128)), SymbolicExpression::atom_value(Value::UInt(pox_params.reward_cycle_length as u128)), SymbolicExpression::atom_value(Value::UInt(pox_params.pox_rejection_fraction as u128)), @@ -736,6 +762,18 @@ fn install_boot_code(header_db: &CLIHeadersDB, marf: &mut C) .unwrap(); } +pub fn add_costs(result: &mut serde_json::Value, costs: bool, runtime: ExecutionCost) { + if costs { + result["costs"] = serde_json::to_value(runtime).unwrap(); + } +} + +pub fn add_assets(result: &mut serde_json::Value, assets: bool, asset_map: AssetMap) { + if assets { + result["assets"] = asset_map.to_json(); + } +} + /// Returns (process-exit-code, Option) pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option) { if args.len() < 1 { @@ -967,9 +1005,7 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (i32, Option (header_db, marf, Err(e)), - Ok(analysis) => { - let result = { - let mut db = marf.as_clarity_db(&header_db, &NULL_BURN_STATE_DB); - let cost_track = LimitedCostTracker::new( - mainnet, - if mainnet { - BLOCK_LIMIT_MAINNET.clone() - } else { - HELIUM_BLOCK_LIMIT.clone() - }, - &mut db, - ) - .unwrap(); - let mut vm_env = - OwnedEnvironment::new_cost_limited(mainnet, db, cost_track); - let result = - vm_env.initialize_contract(contract_identifier, &contract_content); - let cost = vm_env.get_cost_total(); - (result, cost) - }; - (header_db, marf, Ok((analysis, result))) + let (_, _, analysis_result_and_cost) = + in_block(header_db, marf_kv, |header_db, mut marf| { + let analysis_result = + run_analysis(&contract_identifier, &mut ast, &header_db, &mut marf, true); + match analysis_result { + Err(e) => (header_db, marf, Err(e)), + Ok(analysis) => { + let result_and_cost = + with_env_costs(mainnet, &header_db, &mut marf, |vm_env| { + vm_env + .initialize_contract(contract_identifier, &contract_content) + }); + (header_db, marf, Ok((analysis, result_and_cost))) + } } - } - }); + }); - match result { + match analysis_result_and_cost { Ok((contract_analysis, (Ok((_x, asset_map, events)), cost))) => { let mut result = json!({ "message": "Contract initialized!" }); - if costs { - result["costs"] = serde_json::to_value(&cost).unwrap(); - } - if assets { - result["assets"] = asset_map.to_json(); - } + add_costs(&mut result, costs, cost); + add_assets(&mut result, assets, asset_map); + if output_analysis { result["analysis"] = serde_json::to_value(&build_contract_interface(&contract_analysis)) @@ -1433,9 +1400,9 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option ( @@ -1507,33 +1474,14 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option { if let Value::Response(data) = x { if data.committed { @@ -1541,12 +1489,10 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option = events .into_iter() .map(|event| event.json_serialize(0, &Txid([0u8; 32]), true)) @@ -1559,9 +1505,9 @@ pub fn invoke_command(invoked_by: &str, args: &[String]) -> (i32, Option 0); assert!(result["costs"] != json!(null)); + assert!(result["assets"] == json!(null)); eprintln!("launch names with costs and assets"); let invoked = invoke_command( From 306103e9399f1593b2a3a2ac5da53f7f5f6ac326 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 23 Apr 2021 14:17:30 -0400 Subject: [PATCH 11/13] remove redundant info --- src/clarity.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index e76e02f5b2..6a8eb41e7f 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -708,9 +708,8 @@ fn install_boot_code(header_db: &CLIHeadersDB, marf: &mut C) let contract_content = *boot_code_contract; debug!( - "Instantiate boot code contract '{}.{}' ({} bytes)...", + "Instantiate boot code contract '{}' ({} bytes)...", &contract_identifier, - boot_code_name, boot_code_contract.len() ); From d7ef7abf2c7f6e9cf8d9799f61faa5a1f1d00ee1 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 23 Apr 2021 15:17:11 -0400 Subject: [PATCH 12/13] cargo fmt --- src/clarity.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index 5c669ab7c0..6895287154 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -34,8 +34,8 @@ use chainstate::stacks::index::{storage::TrieFileStorage, MarfTrieId}; use util::db::FromColumn; use util::hash::Sha512Trunc256Sum; -use vm::ContractName; use util::log; +use vm::ContractName; use vm::analysis; use vm::analysis::contract_interface_builder::build_contract_interface; @@ -45,8 +45,8 @@ use vm::contexts::{AssetMap, OwnedEnvironment}; use vm::costs::ExecutionCost; use vm::costs::LimitedCostTracker; use vm::database::{ - BurnStateDB, ClarityDatabase, HeadersDB, STXBalance, - SqliteConnection, NULL_BURN_STATE_DB, NULL_HEADER_DB, + BurnStateDB, ClarityDatabase, HeadersDB, STXBalance, SqliteConnection, NULL_BURN_STATE_DB, + NULL_HEADER_DB, }; use vm::errors::{Error, InterpreterResult, RuntimeErrorType}; use vm::types::{PrincipalData, QualifiedContractIdentifier}; @@ -55,8 +55,8 @@ use vm::{execute as vm_execute, SymbolicExpression, SymbolicExpressionType, Valu use burnchains::PoxConstants; use burnchains::Txid; -use util::boot::{boot_code_addr, boot_code_id}; use chainstate::stacks::boot::{STACKS_BOOT_CODE_MAINNET, STACKS_BOOT_CODE_TESTNET}; +use util::boot::{boot_code_addr, boot_code_id}; use core::BLOCK_LIMIT_MAINNET; From 39b5fc8dabfa1c2370021508c5561a2f74ae018e Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 29 Apr 2021 12:55:09 -0400 Subject: [PATCH 13/13] DRY up HELIUM_BLOCK_LIMIT --- src/clarity.rs | 10 +--------- src/core/mod.rs | 9 +++++++++ testnet/stacks-node/src/config.rs | 13 ++----------- testnet/stacks-node/src/neon_node.rs | 2 +- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/src/clarity.rs b/src/clarity.rs index 6895287154..930b8b927b 100644 --- a/src/clarity.rs +++ b/src/clarity.rs @@ -59,6 +59,7 @@ use chainstate::stacks::boot::{STACKS_BOOT_CODE_MAINNET, STACKS_BOOT_CODE_TESTNE use util::boot::{boot_code_addr, boot_code_id}; use core::BLOCK_LIMIT_MAINNET; +use core::HELIUM_BLOCK_LIMIT; use serde::Serialize; use serde_json::json; @@ -89,15 +90,6 @@ macro_rules! panic_test { }; } -const HELIUM_BLOCK_LIMIT: ExecutionCost = ExecutionCost { - write_length: 15_0_000_000, - write_count: 5_0_000, - read_length: 1_000_000_000, - read_count: 5_0_000, - // allow much more runtime in helium blocks than mainnet - runtime: 100_000_000_000, -}; - #[cfg_attr(tarpaulin, skip)] fn print_usage(invoked_by: &str) { eprintln!( diff --git a/src/core/mod.rs b/src/core/mod.rs index 0ddfb4409d..c054fe84b8 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -121,6 +121,15 @@ pub const BLOCK_LIMIT_MAINNET: ExecutionCost = ExecutionCost { runtime: 5_000_000_000, }; +pub const HELIUM_BLOCK_LIMIT: ExecutionCost = ExecutionCost { + write_length: 15_0_000_000, + write_count: 5_0_000, + read_length: 1_000_000_000, + read_count: 5_0_000, + // allow much more runtime in helium blocks than mainnet + runtime: 100_000_000_000, +}; + pub const FAULT_DISABLE_MICROBLOCKS_COST_CHECK: &str = "MICROBLOCKS_DISABLE_COST_CHECK"; pub const FAULT_DISABLE_MICROBLOCKS_BYTES_CHECK: &str = "MICROBLOCKS_DISABLE_BYTES_CHECK"; diff --git a/testnet/stacks-node/src/config.rs b/testnet/stacks-node/src/config.rs index e578dd4b9e..056b807206 100644 --- a/testnet/stacks-node/src/config.rs +++ b/testnet/stacks-node/src/config.rs @@ -8,8 +8,8 @@ use rand::RngCore; use stacks::burnchains::bitcoin::BitcoinNetworkType; use stacks::burnchains::{MagicBytes, BLOCKSTACK_MAGIC_MAINNET}; use stacks::core::{ - BLOCK_LIMIT_MAINNET, CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, PEER_VERSION_MAINNET, - PEER_VERSION_TESTNET, + BLOCK_LIMIT_MAINNET, CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, HELIUM_BLOCK_LIMIT, + PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, }; use stacks::net::connection::ConnectionOptions; use stacks::net::{Neighbor, NeighborKey, PeerAddress}; @@ -408,15 +408,6 @@ lazy_static! { }; } -pub const HELIUM_BLOCK_LIMIT: ExecutionCost = ExecutionCost { - write_length: 15_0_000_000, - write_count: 5_0_000, - read_length: 1_000_000_000, - read_count: 5_0_000, - // allow much more runtime in helium blocks than mainnet - runtime: 100_000_000_000, -}; - impl Config { pub fn from_config_file(config_file: ConfigFile) -> Config { let default_node_config = NodeConfig::default(); diff --git a/testnet/stacks-node/src/neon_node.rs b/testnet/stacks-node/src/neon_node.rs index b7c0172fd3..8e59b2ae9e 100644 --- a/testnet/stacks-node/src/neon_node.rs +++ b/testnet/stacks-node/src/neon_node.rs @@ -132,7 +132,7 @@ fn set_processed_counter(blocks_processed: &BlocksProcessedCounter, value: u64) } #[cfg(not(test))] -fn set_processed_counter(_blocks_processed: &BlocksProcessedCounter, value: u64) {} +fn set_processed_counter(_blocks_processed: &BlocksProcessedCounter, _value: u64) {} /// Process artifacts from the tenure. /// At this point, we're modifying the chainstate, and merging the artifacts from the previous tenure.