From ac5a262e2eb0333df2fcf6bfd0281069c51e02bf Mon Sep 17 00:00:00 2001 From: evan-schott <53463459+evan-schott@users.noreply.github.com> Date: Fri, 7 Jun 2024 14:22:02 -0700 Subject: [PATCH] execute working --- errors/src/errors/package/package_errors.rs | 7 + leo/cli/commands/deploy.rs | 5 +- leo/cli/commands/execute.rs | 222 ++++++++++++++------ leo/cli/commands/mod.rs | 9 - leo/cli/commands/query/mod.rs | 17 +- 5 files changed, 170 insertions(+), 90 deletions(-) diff --git a/errors/src/errors/package/package_errors.rs b/errors/src/errors/package/package_errors.rs index 19546afbec..530811a7e3 100644 --- a/errors/src/errors/package/package_errors.rs +++ b/errors/src/errors/package/package_errors.rs @@ -404,4 +404,11 @@ create_messages!( msg: format!("Failed to read manifest file from the provided file path {path} - {error}"), help: None, } + + @backtraced + insufficient_balance { + args: (balance: impl Display, fee: impl Display), + msg: format!("❌ The public balance of {balance} is insufficient to pay the base fee of {fee}"), + help: None, + } ); diff --git a/leo/cli/commands/deploy.rs b/leo/cli/commands/deploy.rs index b5dc03a1da..0c7ec0282e 100644 --- a/leo/cli/commands/deploy.rs +++ b/leo/cli/commands/deploy.rs @@ -126,7 +126,6 @@ fn handle_deploy, N: Network>( let deployment = package.deploy::(None)?; let deployment_id = deployment.to_deployment_id()?; - let store = ConsensusStore::>::open(StorageMode::Production)?; // Initialize the VM. @@ -137,7 +136,7 @@ fn handle_deploy, N: Network>( if command.fee_options.estimate_fee { // Use `credit` denomination instead of `microcredit`. - display_cost_breakdown(name, total_cost as f64 / 1_000_000.0, storage_cost as f64 / 1_000_000.0, synthesis_cost as f64 / 1_000_000.0, namespace_cost as f64 / 1_000_000.0); + deploy_cost_breakdown(name, total_cost as f64 / 1_000_000.0, storage_cost as f64 / 1_000_000.0, synthesis_cost as f64 / 1_000_000.0, namespace_cost as f64 / 1_000_000.0); continue; } @@ -193,7 +192,7 @@ fn handle_deploy, N: Network>( } // A helper function to display a cost breakdown of the deployment. -fn display_cost_breakdown(name: &String, total_cost: f64, storage_cost: f64, synthesis_cost: f64, namespace_cost: f64) { +fn deploy_cost_breakdown(name: &String, total_cost: f64, storage_cost: f64, synthesis_cost: f64, namespace_cost: f64) { println!("✅ Estimated deployment cost for '{}' is {} credits.", name.bold(), total_cost); // Display the cost breakdown in a table. let data = [[name, "Cost (credits)", "Cost reduction tips"], ["Storage", &format!("{:.6}", storage_cost), "Use less instructions"], ["Synthesis", &format!("{:.6}", synthesis_cost), "Remove expensive operations (Ex: SHA3), or unnecessary imports"], ["Namespace", &format!("{:.6}", namespace_cost), "Lengthen the program name (each additional character makes it 10x cheaper)"], ["Total", &format!("{:.6}", total_cost), ""]]; diff --git a/leo/cli/commands/execute.rs b/leo/cli/commands/execute.rs index ada45b5b6b..27955d4e6f 100644 --- a/leo/cli/commands/execute.rs +++ b/leo/cli/commands/execute.rs @@ -14,13 +14,22 @@ // You should have received a copy of the GNU General Public License // along with the Leo library. If not, see . +use aleo_std::StorageMode; use super::*; use clap::Parser; -use leo_retriever::NetworkName; use snarkvm::{ cli::{helpers::dotenv_private_key, Execute as SnarkVMExecute}, prelude::{MainnetV0, Network, Parser as SnarkVMParser, TestnetV0}, -}; + }; +use snarkvm::file::AleoFile; +use snarkvm::ledger::Execution; +use snarkvm::ledger::Transaction::Execute as ExecuteTransaction; +use snarkvm::prelude::store::ConsensusStore; +use snarkvm::prelude::store::helpers::memory::{BlockMemory, ConsensusMemory}; +use snarkvm::prelude::{Address, execution_cost, Process, Program as SnarkVMProgram, ProgramID, VM}; +use snarkvm::prelude::query::Query as SnarkVMQuery; +use crate::cli::query::QueryCommands; +use crate::cli::query::QueryCommands::Program; /// Build, Prove and Run Leo program with inputs #[derive(Parser, Debug)] @@ -59,7 +68,7 @@ impl Command for Execute { (Build { options: self.compiler_options.clone() }).execute(context) } - fn apply(self, context: Context, _input: Self::Input) -> Result { + fn apply(self, context: Context, input: Self::Input) -> Result { // Parse the network. let network = NetworkName::try_from(self.compiler_options.network.as_str())?; match network { @@ -71,80 +80,16 @@ impl Command for Execute { // A helper function to handle the `execute` command. fn handle_execute(command: Execute, context: Context) -> Result<::Output> { - // If the `broadcast` flag is set, then broadcast the transaction. - if command.broadcast { - // // Get the program name. - // let program_name = match (command.program, command.local) { - // (Some(name), true) => { - // let local = context.open_manifest::()?.program_id().to_string(); - // // Throw error if local name doesn't match the specified name. - // if name == local { - // local - // } else { - // return Err(PackageError::conflicting_on_chain_program_name(local, name).into()); - // } - // } - // (Some(name), false) => name, - // (None, true) => context.open_manifest::()?.program_id().to_string(), - // (None, false) => return Err(PackageError::missing_on_chain_program_name().into()), - // }; - // - // // Get the private key. - // let private_key = match command.fee_options.private_key { - // Some(private_key) => private_key, - // None => dotenv_private_key().map_err(CliError::failed_to_read_environment_private_key)?.to_string(), - // }; - // - // // Set deploy arguments. - // let mut fee_args = vec![ - // "snarkos".to_string(), - // "--private-key".to_string(), - // private_key.clone(), - // "--query".to_string(), - // command.compiler_options.endpoint.clone(), - // "--priority-fee".to_string(), - // command.fee_options.priority_fee.to_string(), - // "--broadcast".to_string(), - // format!("{}/{}/transaction/broadcast", command.compiler_options.endpoint, command.compiler_options.network) - // .to_string(), - // ]; - // - // // Use record as payment option if it is provided. - // if let Some(record) = command.fee_options.record.clone() { - // fee_args.push("--record".to_string()); - // fee_args.push(record); - // }; - // - // // Execute program. - // Developer::Execute( - // SnarkOSExecute::try_parse_from( - // [ - // // The arguments for determining fee. - // fee_args, - // // The program ID and function name. - // vec![program_name, command.name], - // // The function inputs. - // command.inputs, - // ] - // .concat(), - // ) - // .unwrap(), - // ) - // .parse() - // .map_err(CliError::failed_to_execute_deploy)?; - // - // return Ok(()); - } // If input values are provided, then run the program with those inputs. // Otherwise, use the input file. - let mut inputs = command.inputs; + let mut inputs = command.inputs.clone(); // Compose the `execute` command. - let mut arguments = vec![SNARKVM_COMMAND.to_string(), command.name]; + let mut arguments = vec![SNARKVM_COMMAND.to_string(), command.name.clone()]; // Add the inputs to the arguments. - match command.file { + match command.file.clone() { Some(file) => { // Get the contents from the file. let path = context.dir()?.join(file); @@ -169,6 +114,99 @@ fn handle_execute(command: Execute, context: Context) -> Result< arguments.append(&mut inputs), } + // If the `broadcast` flag is set, then broadcast the transaction. + if command.broadcast { + + // Get the program name. + let program_name = match (command.program.clone(), command.local.clone()) { + (Some(name), true) => { + let local = context.open_manifest::()?.program_id().to_string(); + // Throw error if local name doesn't match the specified name. + if name == local { + local + } else { + return Err(PackageError::conflicting_on_chain_program_name(local, name).into()); + } + } + (Some(name), false) => name.clone(), + (None, true) => context.open_manifest::()?.program_id().to_string(), + (None, false) => return Err(PackageError::missing_on_chain_program_name().into()), + }; + + // Get the private key. + let private_key = match command.fee_options.private_key.clone() { + Some(key) => PrivateKey::from_str(&key)?, + None => PrivateKey::from_str( + &dotenv_private_key().map_err(CliError::failed_to_read_environment_private_key)?.to_string(), + )?, + }; + + // Specify the query + let query = SnarkVMQuery::>::from(command.compiler_options.endpoint.clone()); + + // Initialize an RNG. + let rng = &mut rand::thread_rng(); + + // Initialize the storage. + let store = ConsensusStore::>::open(StorageMode::Production)?; + + // Initialize the VM. + let vm = VM::from(store)?; + + // Load the main program, and all of its imports. + let program_id = &ProgramID::::from_str(&format!("{}.aleo", program_name))?; + // TODO: create a local version too + load_program_from_network(&command, context.clone(), &mut vm.process().write(), program_id)?; + + let fee_record = if let Some(record) = command.fee_options.record { + Some(parse_record(&private_key, &record)?) + } else { + None + }; + + // Create a new transaction. + let transaction = vm.execute(&private_key, (program_id, command.name), inputs.iter(), fee_record.clone(), command.fee_options.priority_fee, Some(query), rng)?; + + // Check the transaction cost. + let (total_cost, (storage_cost, finalize_cost)) = if let ExecuteTransaction(_, execution, _) = &transaction { + execution_cost(&vm.process().read(), &execution)? + } else { + panic!("All transactions should be of type Execute.") + }; + + // Check if the public balance is sufficient. + if fee_record.is_none() { + // Derive the account address. + let address = Address::::try_from(ViewKey::try_from(&private_key)?)?; + // Query the public balance of the address on the `account` mapping from `credits.aleo`. + let mut public_balance = Query{ endpoint: command.compiler_options.endpoint.clone(), network: command.compiler_options.network.clone(), command: QueryCommands::Program { + command: crate::cli::commands::query::Program { + name: "credits".to_string(), + mappings: false, + mapping_value: Some(vec!["account".to_string(), address.to_string()]), + }, + } }.execute(context.clone())?; + // Check balance. + // Remove the last 3 characters since they represent the `u64` suffix. + public_balance.truncate(public_balance.len() - 3); + if public_balance.parse::().unwrap() < total_cost { + return Err(PackageError::insufficient_balance(public_balance, total_cost).into()); + } + } + + // Print the cost breakdown. + if command.fee_options.estimate_fee { + execution_cost_breakdown(&program_name, total_cost as f64 / 1_000_000.0, storage_cost as f64 / 1_000_000.0, finalize_cost as f64 / 1_000_000.0); + return Ok(()); + } + + println!("✅ Created execution transaction for '{}'", program_id.to_string().bold()); + + handle_broadcast(&format!("{}/{}/transaction/broadcast", command.compiler_options.endpoint, command.compiler_options.network), transaction, &program_name)?; + + return Ok(()); + } + // Add the compiler options to the arguments. if command.compiler_options.offline { arguments.push(String::from("--offline")); @@ -199,3 +237,47 @@ fn handle_execute(command: Execute, context: Context) -> Result<(command: &Execute, context: Context, process: &mut Process, program_id: &ProgramID) -> Result<()> { + // Fetch the program. + let program_src = Query{ endpoint: command.compiler_options.endpoint.clone(), network: command.compiler_options.network.clone(), command: QueryCommands::Program { + command: crate::cli::commands::query::Program { + name: program_id.to_string(), + mappings: false, + mapping_value: None, + }, + } }.execute(context.clone())?; + let program = SnarkVMProgram::::from_str(&program_src).unwrap(); + + // Return early if the program is already loaded. + if process.contains_program(program.id()) { + return Ok(()); + } + + // Iterate through the program imports. + for import_program_id in program.imports().keys() { + // Add the imports to the process if does not exist yet. + if !process.contains_program(import_program_id) { + // Recursively load the program and its imports. + load_program_from_network(command, context.clone(), process, import_program_id)?; + } + } + + // Add the program to the process if it does not already exist. + if !process.contains_program(program.id()) { + process.add_program(&program)?; + } + + Ok(()) +} + +// A helper function to display a cost breakdown of the execution. +fn execution_cost_breakdown(name: &String, total_cost: f64, storage_cost: f64, finalize_cost: f64) { + println!("✅ Estimated execution cost for '{}' is {} credits.", name.bold(), total_cost); + // Display the cost breakdown in a table. + let data = [[name, "Cost (credits)", "Cost reduction tips"], ["Storage", &format!("{:.6}", storage_cost), "Use fewer nested transition functions and smaller input and output datatypes"], ["On-chain", &format!("{:.6}", finalize_cost), "Remove operations that are expensive computationally (Ex: hash functions) or storage-wise (Ex: Mapping insertions)"], ["Total", &format!("{:.6}", total_cost), ""]]; + let mut out = Vec::new(); + text_tables::render(&mut out, data).unwrap(); + println!("{}", ::std::str::from_utf8(&out).unwrap()); +} \ No newline at end of file diff --git a/leo/cli/commands/mod.rs b/leo/cli/commands/mod.rs index fe629bb6bf..36395dfb1a 100644 --- a/leo/cli/commands/mod.rs +++ b/leo/cli/commands/mod.rs @@ -240,15 +240,6 @@ fn handle_broadcast(endpoint: &String, transaction: Transaction, // Get the transaction id. let transaction_id = transaction.id(); - // TODO: remove - println!("Transaction {:?}", transaction); - let tx_bytes = transaction.to_bytes_le()?; - println!("Transaction bytes: {:?}", tx_bytes); - let tx_json = serde_json::to_string(&transaction).unwrap(); - println!("Transaction JSON: {:?}", tx_json); - let deserialize_tx_json = serde_json::from_str::>(&tx_json).unwrap(); - println!("Deserialized transaction: {:?}", deserialize_tx_json); - // Send the deployment request to the local development node. return match ureq::post(endpoint).send_json(&transaction) { Ok(id) => { diff --git a/leo/cli/commands/query/mod.rs b/leo/cli/commands/query/mod.rs index f5030f378f..347a9bc152 100644 --- a/leo/cli/commands/query/mod.rs +++ b/leo/cli/commands/query/mod.rs @@ -19,8 +19,8 @@ use super::*; mod block; use block::Block; -mod program; -use program::Program; +pub mod program; +pub use program::Program; mod state_root; use state_root::StateRoot; @@ -53,15 +53,15 @@ pub struct Query { default_value = "http://api.explorer.aleo.org/v1" )] pub endpoint: String, - #[clap(short, long, global = true, help = "Network to use. Defaults to testnet3.", default_value = "testnet3")] + #[clap(short, long, global = true, help = "Network to use. Defaults to mainnet.", default_value = "mainnet")] pub(crate) network: String, #[clap(subcommand)] - command: QueryCommands, + pub command: QueryCommands, } impl Command for Query { type Input = (); - type Output = (); + type Output = String; fn log_span(&self) -> Span { tracing::span!(tracing::Level::INFO, "Leo") @@ -105,8 +105,9 @@ impl Command for Query { if response.status() == 200 { tracing::info!("✅ Successfully retrieved data from '{url}'.\n"); // Unescape the newlines. - println!("{}\n", response.into_string().unwrap().replace("\\n", "\n")); - Ok(()) + let result = response.into_string().unwrap().replace("\\n", "\n").replace('\"', ""); + println!("{}\n", result); + Ok(result) } else { Err(UtilError::network_error(url, response.status(), Default::default()).into()) } @@ -114,7 +115,7 @@ impl Command for Query { } #[derive(Parser, Debug)] -enum QueryCommands { +pub enum QueryCommands { #[clap(about = "Query block information")] Block { #[clap(flatten)]