diff --git a/create-aleo-app/template-node/index.js b/create-aleo-app/template-node/index.js index aee41705c..8f0341417 100644 --- a/create-aleo-app/template-node/index.js +++ b/create-aleo-app/template-node/index.js @@ -40,15 +40,7 @@ async function localProgramExecution(program, aleoFunction, inputs) { undefined, keyProviderParams, ); - console.log(executionResponse.getOutputs()) - executionResponse = await programManager.executeOffline( - hello_hello_program, - "hello", - ["5u32", "5u32"], - false, - keyProviderParams, - ); return executionResponse.getOutputs(); } diff --git a/sdk/src/polyfill/worker.ts b/sdk/src/polyfill/worker.ts index ef115a03d..8f979db29 100644 --- a/sdk/src/polyfill/worker.ts +++ b/sdk/src/polyfill/worker.ts @@ -1,216 +1,188 @@ -function patch($worker: typeof import("node:worker_threads"), $os: typeof import("node:os")) { - // This is technically not a part of the Worker polyfill, - // but Workers are used for multi-threading, so this is often - // needed when writing Worker code. - if (globalThis.navigator == null) { - globalThis.navigator = { - hardwareConcurrency: $os.cpus().length, - } as Navigator; - } - - globalThis.Worker = class Worker extends EventTarget { - private _worker: import("node:worker_threads").Worker; +import * as $worker from "node:worker_threads"; +import * as $os from "node:os"; + +// This is technically not a part of the Worker polyfill, +// but Workers are used for multi-threading, so this is often +// needed when writing Worker code. +if (globalThis.navigator == null) { + globalThis.navigator = { + hardwareConcurrency: $os.cpus().length, + } as Navigator; +} - constructor(url: string | URL, options?: WorkerOptions | undefined) { - super(); +globalThis.Worker = class Worker extends EventTarget { + private _worker: import("node:worker_threads").Worker; - if (url instanceof URL) { - if (url.protocol !== "file:") { - throw new Error("Worker only supports file: URLs"); - } + constructor(url: string | URL, options?: WorkerOptions | undefined) { + super(); - url = url.href; - - } else { - throw new Error("Filepaths are unreliable, use `new URL(\"...\", import.meta.url)` instead."); + if (url instanceof URL) { + if (url.protocol !== "file:") { + throw new Error("Worker only supports file: URLs"); } - if (!options || options.type !== "module") { - throw new Error("Workers must use \`type: \"module\"\`"); - } + url = url.href; - // This uses some funky stuff like `patch.toString()`. - // - // This is needed so that it can synchronously run the polyfill code - // inside of the worker. - // - // It can't use `require` because the file doesn't have a `.cjs` file extension. - // - // It can't use `import` because that's asynchronous, and the file path - // might be different if using a bundler. - const code = ` - ${patch.toString()} - - // Inject the polyfill into the worker - patch(require("node:worker_threads"), require("node:os")); - - const { workerData } = require("node:worker_threads"); - - // This actually loads and runs the worker file - import(workerData.url) - .catch((e) => { - // TODO maybe it should send a message to the parent? - console.error(e.stack); - }); - `; - - this._worker = new $worker.Worker(code, { - eval: true, - workerData: { - url, - }, - }); - - this._worker.on("message", (data) => { - this.dispatchEvent(new MessageEvent("message", { data })); - }); - - this._worker.on("messageerror", (error) => { - throw new Error("UNIMPLEMENTED"); - }); - - this._worker.on("error", (error) => { - // TODO attach the error to the event somehow - const event = new Event("error"); - this.dispatchEvent(event); - }); + } else { + throw new Error("Filepaths are unreliable, use `new URL(\"...\", import.meta.url)` instead."); } - set onmessage(f: () => void) { - throw new Error("UNIMPLEMENTED"); + if (!options || options.type !== "module") { + throw new Error("Workers must use \`type: \"module\"\`"); } - set onmessageerror(f: () => void) { - throw new Error("UNIMPLEMENTED"); - } + const code = ` + const { workerData } = require("node:worker_threads"); + + import(workerData.polyfill) + .then(() => import(workerData.url)) + .catch((e) => { + // TODO maybe it should send a message to the parent? + console.error(e.stack); + }); + `; + + this._worker = new $worker.Worker(code, { + eval: true, + workerData: { + url, + polyfill: new URL("node-polyfill.js", import.meta.url).href, + }, + }); - set onerror(f: () => void) { + this._worker.on("message", (data) => { + this.dispatchEvent(new MessageEvent("message", { data })); + }); + + this._worker.on("messageerror", (error) => { throw new Error("UNIMPLEMENTED"); - } + }); - postMessage(message: any, transfer: Array): void; - postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; - postMessage(value: any, transfer: any) { - this._worker.postMessage(value, transfer); - } + this._worker.on("error", (error) => { + // TODO attach the error to the event somehow + const event = new Event("error"); + this.dispatchEvent(event); + }); + } - terminate() { - this._worker.terminate(); - } + set onmessage(f: () => void) { + throw new Error("UNIMPLEMENTED"); + } - // This is Node-specific, it allows the process to exit - // even if the Worker is still running. - unref() { - this._worker.unref(); - } - }; + set onmessageerror(f: () => void) { + throw new Error("UNIMPLEMENTED"); + } + set onerror(f: () => void) { + throw new Error("UNIMPLEMENTED"); + } - if (!$worker.isMainThread) { - const globals = globalThis as unknown as DedicatedWorkerGlobalScope; + postMessage(message: any, transfer: Array): void; + postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; + postMessage(value: any, transfer: any) { + this._worker.postMessage(value, transfer); + } - // This is used to create the onmessage, onmessageerror, and onerror setters - const makeSetter = (prop: string, event: string) => { - let oldvalue: () => void; + terminate() { + this._worker.terminate(); + } - Object.defineProperty(globals, prop, { - get() { - return oldvalue; - }, - set(value) { - if (oldvalue) { - globals.removeEventListener(event, oldvalue); - } + // This is Node-specific, it allows the process to exit + // even if the Worker is still running. + unref() { + this._worker.unref(); + } +}; - oldvalue = value; - if (oldvalue) { - globals.addEventListener(event, oldvalue); - } - }, - }); - }; +if (!$worker.isMainThread) { + const globals = globalThis as unknown as DedicatedWorkerGlobalScope; - // This makes sure that `f` is only run once - const memoize = (f: () => void) => { - let run = false; + // This is used to create the onmessage, onmessageerror, and onerror setters + const makeSetter = (prop: string, event: string) => { + let oldvalue: () => void; - return () => { - if (!run) { - run = true; - f(); + Object.defineProperty(globals, prop, { + get() { + return oldvalue; + }, + set(value) { + if (oldvalue) { + globals.removeEventListener(event, oldvalue); } - }; - }; + oldvalue = value; - // We only start listening for messages / errors when the worker calls addEventListener - const startOnMessage = memoize(() => { - $worker.parentPort!.on("message", (data) => { - workerEvents.dispatchEvent(new MessageEvent("message", { data })); - }); + if (oldvalue) { + globals.addEventListener(event, oldvalue); + } + }, }); + }; - const startOnMessageError = memoize(() => { - throw new Error("UNIMPLEMENTED"); - }); + // This makes sure that `f` is only run once + const memoize = (f: () => void) => { + let run = false; - const startOnError = memoize(() => { - $worker.parentPort!.on("error", (data) => { - workerEvents.dispatchEvent(new Event("error")); - }); + return () => { + if (!run) { + run = true; + f(); + } + }; + }; + + + // We only start listening for messages / errors when the worker calls addEventListener + const startOnMessage = memoize(() => { + $worker.parentPort!.on("message", (data) => { + workerEvents.dispatchEvent(new MessageEvent("message", { data })); }); + }); + const startOnMessageError = memoize(() => { + throw new Error("UNIMPLEMENTED"); + }); - // Node workers don't have top-level events, so we have to make our own - const workerEvents = new EventTarget(); + const startOnError = memoize(() => { + $worker.parentPort!.on("error", (data) => { + workerEvents.dispatchEvent(new Event("error")); + }); + }); - globals.close = () => { - process.exit(); - }; - globals.addEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { - workerEvents.addEventListener(type, callback, options); + // Node workers don't have top-level events, so we have to make our own + const workerEvents = new EventTarget(); - if (type === "message") { - startOnMessage(); - } else if (type === "messageerror") { - startOnMessageError(); - } else if (type === "error") { - startOnError(); - } - }; + globals.close = () => { + process.exit(); + }; - globals.removeEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { - workerEvents.removeEventListener(type, callback, options); - }; + globals.addEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { + workerEvents.addEventListener(type, callback, options); - function postMessage(message: any, transfer: Transferable[]): void; - function postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; - function postMessage(value: any, transfer: any) { - $worker.parentPort!.postMessage(value, transfer); + if (type === "message") { + startOnMessage(); + } else if (type === "messageerror") { + startOnMessageError(); + } else if (type === "error") { + startOnError(); } + }; - globals.postMessage = postMessage; + globals.removeEventListener = (type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions | undefined) => { + workerEvents.removeEventListener(type, callback, options); + }; - makeSetter("onmessage", "message"); - makeSetter("onmessageerror", "messageerror"); - makeSetter("onerror", "error"); + function postMessage(message: any, transfer: Transferable[]): void; + function postMessage(message: any, options?: StructuredSerializeOptions | undefined): void; + function postMessage(value: any, transfer: any) { + $worker.parentPort!.postMessage(value, transfer); } -} - -async function polyfill() { - const [$worker, $os] = await Promise.all([ - import("node:worker_threads"), - import("node:os"), - ]); + globals.postMessage = postMessage; - patch($worker, $os); + makeSetter("onmessage", "message"); + makeSetter("onmessageerror", "messageerror"); + makeSetter("onerror", "error"); } - -if (globalThis.Worker == null) { - await polyfill(); -} - -export {}; diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs index c68c8a2b1..52336d90a 100644 --- a/wasm/src/lib.rs +++ b/wasm/src/lib.rs @@ -166,10 +166,20 @@ pub(crate) mod types; #[cfg(not(test))] mod thread_pool; -use wasm_bindgen::prelude::*; +#[cfg(test)] +mod thread_pool { + use std::future::Future; + + pub fn spawn(f: F) -> impl Future + where + A: Send + 'static, + F: FnOnce() -> A + Send + 'static, + { + async move { f() } + } +} -#[cfg(not(test))] -use thread_pool::ThreadPool; +use wasm_bindgen::prelude::*; use std::str::FromStr; @@ -215,7 +225,7 @@ pub use thread_pool::run_rayon_thread; pub async fn init_thread_pool(url: web_sys::Url, num_threads: usize) -> Result<(), JsValue> { console_error_panic_hook::set_once(); - ThreadPool::builder().url(url).num_threads(num_threads).build_global().await?; + thread_pool::ThreadPool::builder().url(url).num_threads(num_threads).build_global().await?; Ok(()) } diff --git a/wasm/src/programs/macros.rs b/wasm/src/programs/macros.rs deleted file mode 100644 index 577b7edd2..000000000 --- a/wasm/src/programs/macros.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (C) 2019-2023 Aleo Systems Inc. -// This file is part of the Aleo SDK library. - -// The Aleo SDK library is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// The Aleo SDK library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with the Aleo SDK library. If not, see . - -#[macro_export] -macro_rules! process_inputs { - ($inputs:expr) => {{ - let mut inputs_native = Vec::::new(); - log("parsing inputs"); - for input in $inputs.to_vec().iter() { - if let Some(input) = input.as_string() { - inputs_native.push(input); - } else { - return Err("Invalid input - all inputs must be a string specifying the type".to_string()); - } - } - inputs_native - }}; -} - -#[macro_export] -macro_rules! execute_program { - ($process:expr, $inputs:expr, $program_string:expr, $function_id_string:expr, $private_key:expr, $proving_key:expr, $verifying_key:expr, $rng:expr) => {{ - if (($proving_key.is_some() && $verifying_key.is_none()) - || ($proving_key.is_none() && $verifying_key.is_some())) - { - return Err( - "If specifying a key for a program execution, both the proving and verifying key must be specified" - .to_string(), - ); - } - - log("Loading program"); - let program = - ProgramNative::from_str($program_string).map_err(|_| "The program ID provided was invalid".to_string())?; - log("Loading function"); - let function_name = IdentifierNative::from_str($function_id_string) - .map_err(|_| "The function name provided was invalid".to_string())?; - - let program_id = program.id().to_string(); - - if program_id != "credits.aleo" { - log("Adding program to the process"); - if let Ok(stored_program) = $process.get_program(program.id()) { - if stored_program != &program { - return Err("The program provided does not match the program stored in the cache, please clear the cache before proceeding".to_string()); - } - } else { - $process.add_program(&program).map_err(|e| e.to_string())?; - } - } - - if let Some(proving_key) = $proving_key { - if Self::contains_key($process, program.id(), &function_name) { - log(&format!("Proving & verifying keys were specified for {program_id} - {function_name:?} but a key already exists in the cache. Using cached keys")); - } else { - log(&format!("Inserting externally provided proving and verifying keys for {program_id} - {function_name:?}")); - $process - .insert_proving_key(program.id(), &function_name, ProvingKeyNative::from(proving_key)) - .map_err(|e| e.to_string())?; - if let Some(verifying_key) = $verifying_key { - $process.insert_verifying_key(program.id(), &function_name, VerifyingKeyNative::from(verifying_key)).map_err(|e| e.to_string())?; - } - } - }; - - log("Creating authorization"); - let authorization = $process - .authorize::( - $private_key, - program.id(), - function_name, - $inputs.iter(), - $rng, - ) - .map_err(|err| err.to_string())?; - - log("Executing program"); - let result = $process - .execute::(authorization) - .map_err(|err| err.to_string())?; - - result - }}; -} - -#[macro_export] -macro_rules! execute_fee { - ($process:expr, $private_key:expr, $fee_record:expr, $fee_microcredits:expr, $submission_url:expr, $fee_proving_key:expr, $fee_verifying_key:expr, $execution_id:expr, $rng:expr) => {{ - if (($fee_proving_key.is_some() && $fee_verifying_key.is_none()) - || ($fee_proving_key.is_none() && $fee_verifying_key.is_some())) - { - return Err( - "Missing key - both the proving and verifying key must be specified for a program execution" - .to_string(), - ); - } - - if let Some(fee_proving_key) = $fee_proving_key { - let credits = ProgramIDNative::from_str("credits.aleo").unwrap(); - let fee = if $fee_record.is_some() { - IdentifierNative::from_str("fee_private").unwrap() - } else { - IdentifierNative::from_str("fee_public").unwrap() - }; - if Self::contains_key($process, &credits, &fee) { - log("Fee proving & verifying keys were specified but a key already exists in the cache. Using cached keys"); - } else { - log("Inserting externally provided fee proving and verifying keys"); - $process - .insert_proving_key(&credits, &fee, ProvingKeyNative::from(fee_proving_key)).map_err(|e| e.to_string())?; - if let Some(fee_verifying_key) = $fee_verifying_key { - $process - .insert_verifying_key(&credits, &fee, VerifyingKeyNative::from(fee_verifying_key)) - .map_err(|e| e.to_string())?; - } - } - }; - - log("Authorizing Fee"); - let fee_authorization = match $fee_record { - Some(fee_record) => { - let fee_record_native = RecordPlaintextNative::from_str(&fee_record.to_string()).unwrap(); - $process.authorize_fee_private::( - $private_key, - fee_record_native, - $fee_microcredits, - 0u64, - $execution_id, - $rng, - ).map_err(|e| e.to_string())? - } - None => { - $process.authorize_fee_public::($private_key, $fee_microcredits, 0u64, $execution_id, $rng).map_err(|e| e.to_string())? - } - }; - - log("Executing fee"); - let (_, mut trace) = $process - .execute::(fee_authorization) - .map_err(|err| err.to_string())?; - - let query = QueryNative::from($submission_url); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; - let fee = trace.prove_fee::(&mut StdRng::from_entropy()).map_err(|e|e.to_string())?; - - log("Verifying fee execution"); - $process.verify_fee(&fee, $execution_id).map_err(|e| e.to_string())?; - - fee - }}; -} diff --git a/wasm/src/programs/manager/deploy.rs b/wasm/src/programs/manager/deploy.rs index d077d3eef..4412e267c 100644 --- a/wasm/src/programs/manager/deploy.rs +++ b/wasm/src/programs/manager/deploy.rs @@ -16,27 +16,9 @@ use super::*; -use crate::{ - execute_fee, - log, - types::{ - CurrentAleo, - CurrentNetwork, - ProcessNative, - ProgramIDNative, - ProgramNative, - ProgramOwnerNative, - RecordPlaintextNative, - TransactionNative, - }, - PrivateKey, - RecordPlaintext, - Transaction, -}; +use crate::{log, PrivateKey, RecordPlaintext, Transaction}; use js_sys::Object; -use rand::{rngs::StdRng, SeedableRng}; -use std::str::FromStr; #[wasm_bindgen] impl ProgramManager { @@ -61,74 +43,37 @@ impl ProgramManager { #[allow(clippy::too_many_arguments)] pub async fn deploy( private_key: &PrivateKey, - program: &str, + program: String, fee_credits: f64, fee_record: Option, - url: &str, + url: String, imports: Option, fee_proving_key: Option, fee_verifying_key: Option, ) -> Result { log("Creating deployment transaction"); // Convert fee to microcredits and check that the fee record has enough credits to pay it - let fee_microcredits = match &fee_record { - Some(fee_record) => Self::validate_amount(fee_credits, fee_record, true)?, - None => (fee_credits * 1_000_000.0) as u64, - }; - - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; - - log("Checking program has a valid name"); - let program = ProgramNative::from_str(program).map_err(|err| err.to_string())?; - - log("Checking program imports are valid and add them to the process"); - ProgramManager::resolve_imports(process, &program, imports)?; - let rng = &mut StdRng::from_entropy(); - - log("Creating deployment"); - let deployment = process.deploy::(&program, rng).map_err(|err| err.to_string())?; - if deployment.program().functions().is_empty() { - return Err("Attempted to create an empty transaction deployment".to_string()); - } - - log("Ensuring the fee is sufficient to pay for the deployment"); - let (minimum_deployment_cost, (_, _)) = - deployment_cost::(&deployment).map_err(|err| err.to_string())?; - if fee_microcredits < minimum_deployment_cost { - return Err(format!( - "Fee is too low to pay for the deployment. The minimum fee is {} credits", - minimum_deployment_cost as f64 / 1_000_000.0 - )); - } - - let deployment_id = deployment.to_deployment_id().map_err(|e| e.to_string())?; - - let fee = execute_fee!( - process, - private_key, - fee_record, - fee_microcredits, - url, - fee_proving_key, - fee_verifying_key, - deployment_id, - rng - ); + let fee_microcredits = Self::microcredits(fee_credits, &fee_record)?; - // Create the program owner - let owner = ProgramOwnerNative::new(private_key, deployment_id, &mut StdRng::from_entropy()) - .map_err(|err| err.to_string())?; + let state = ProgramState::new(program, imports).await?; - log("Verifying the deployment and fees"); - process - .verify_deployment::(&deployment, &mut StdRng::from_entropy()) - .map_err(|err| err.to_string())?; + let (state, deploy) = state.deploy().await?; - log("Creating deployment transaction"); - Ok(Transaction::from( - TransactionNative::from_deployment(owner, deployment, fee).map_err(|err| err.to_string())?, - )) + deploy.check_fee(fee_microcredits)?; + + let (state, fee) = state + .execute_fee( + deploy.execution_id()?, + url, + private_key.clone(), + fee_microcredits, + fee_record, + fee_proving_key, + fee_verifying_key, + ) + .await?; + + state.deploy_transaction(deploy, fee).await } /// Estimate the fee for a program deployment @@ -141,31 +86,16 @@ impl ProgramManager { /// are a string representing the program source code \{ "hello.aleo": "hello.aleo source code" \} /// @returns {u64 | Error} #[wasm_bindgen(js_name = estimateDeploymentFee)] - pub async fn estimate_deployment_fee(program: &str, imports: Option) -> Result { + pub async fn estimate_deployment_fee(program: String, imports: Option) -> Result { log( "Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network", ); - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; - - log("Check program has a valid name"); - let program = ProgramNative::from_str(program).map_err(|err| err.to_string())?; - - log("Check program imports are valid and add them to the process"); - ProgramManager::resolve_imports(process, &program, imports)?; - log("Create sample deployment"); - let deployment = - process.deploy::(&program, &mut StdRng::from_entropy()).map_err(|err| err.to_string())?; - if deployment.program().functions().is_empty() { - return Err("Attempted to create an empty transaction deployment".to_string()); - } + let state = ProgramState::new(program, imports).await?; - log("Estimate the deployment fee"); - let (minimum_deployment_cost, (_, _)) = - deployment_cost::(&deployment).map_err(|err| err.to_string())?; + let (_state, deploy) = state.deploy().await?; - Ok(minimum_deployment_cost) + Ok(deploy.minimum_deployment_cost()) } /// Estimate the component of the deployment cost which comes from the fee for the program name. diff --git a/wasm/src/programs/manager/execute.rs b/wasm/src/programs/manager/execute.rs index d36cae543..43d691419 100644 --- a/wasm/src/programs/manager/execute.rs +++ b/wasm/src/programs/manager/execute.rs @@ -15,14 +15,10 @@ // along with the Aleo SDK library. If not, see . use super::*; -use core::ops::Add; use crate::{ - execute_fee, - execute_program, log, - process_inputs, - types::{CurrentAleo, IdentifierNative, ProcessNative, ProgramNative, RecordPlaintextNative, TransactionNative}, + types::{IdentifierNative, ProgramNative}, ExecutionResponse, PrivateKey, RecordPlaintext, @@ -30,7 +26,6 @@ use crate::{ }; use js_sys::{Array, Object}; -use rand::{rngs::StdRng, SeedableRng}; use std::str::FromStr; #[wasm_bindgen] @@ -57,8 +52,8 @@ impl ProgramManager { #[allow(clippy::too_many_arguments)] pub async fn execute_function_offline( private_key: &PrivateKey, - program: &str, - function: &str, + program: String, + function: String, inputs: Array, prove_execution: bool, cache: bool, @@ -67,40 +62,19 @@ impl ProgramManager { verifying_key: Option, ) -> Result { log(&format!("Executing local function: {function}")); - let inputs = inputs.to_vec(); - let rng = &mut StdRng::from_entropy(); - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; + let inputs = ProgramState::get_inputs(inputs)?; - log("Check program imports are valid and add them to the process"); - let program_native = ProgramNative::from_str(program).map_err(|e| e.to_string())?; - ProgramManager::resolve_imports(process, &program_native, imports)?; + let state = ProgramState::new(program, imports).await?; - let (response, mut trace) = execute_program!( - process, - process_inputs!(inputs), - program, - function, - private_key, - proving_key, - verifying_key, - rng - ); - - let process_native = if cache { Some(process_native) } else { None }; + let (state, execute) = + state.execute_program(function, inputs, private_key.clone(), proving_key, verifying_key).await?; if prove_execution { - log("Preparing inclusion proofs for execution"); - let query = QueryNative::from("https://vm.aleo.org/api"); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; - - log("Proving execution"); - let locator = program_native.id().to_string().add("/").add(function); - let execution = trace.prove_execution::(&locator, rng).map_err(|e| e.to_string())?; - Ok(ExecutionResponse::from((response, execution, process_native))) + let (state, execution) = state.prove_execution(execute, "https://vm.aleo.org/api".to_string()).await?; + state.prove_response(execution, cache) } else { - Ok(ExecutionResponse::from((response, process_native))) + state.execute_response(execute, cache) } } @@ -129,12 +103,12 @@ impl ProgramManager { #[allow(clippy::too_many_arguments)] pub async fn execute( private_key: &PrivateKey, - program: &str, - function: &str, + program: String, + function: String, inputs: Array, fee_credits: f64, fee_record: Option, - url: &str, + url: String, imports: Option, proving_key: Option, verifying_key: Option, @@ -142,62 +116,31 @@ impl ProgramManager { fee_verifying_key: Option, ) -> Result { log(&format!("Executing function: {function} on-chain")); - let fee_microcredits = match &fee_record { - Some(fee_record) => Self::validate_amount(fee_credits, fee_record, true)?, - None => (fee_credits * 1_000_000.0) as u64, - }; - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; + let inputs = ProgramState::get_inputs(inputs)?; - log("Check program imports are valid and add them to the process"); - let program_native = ProgramNative::from_str(program).map_err(|e| e.to_string())?; - ProgramManager::resolve_imports(process, &program_native, imports)?; - let rng = &mut StdRng::from_entropy(); + let fee_microcredits = Self::microcredits(fee_credits, &fee_record)?; - log("Executing program"); - let (_, mut trace) = execute_program!( - process, - process_inputs!(inputs), - program, - function, - private_key, - proving_key, - verifying_key, - rng - ); + let state = ProgramState::new(program, imports).await?; - log("Preparing inclusion proofs for execution"); - let query = QueryNative::from(url); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; + let (state, execute) = + state.execute_program(function, inputs, private_key.clone(), proving_key, verifying_key).await?; - log("Proving execution"); - let program = ProgramNative::from_str(program).map_err(|err| err.to_string())?; - let locator = program.id().to_string().add("/").add(function); - let execution = trace - .prove_execution::(&locator, &mut StdRng::from_entropy()) - .map_err(|e| e.to_string())?; - let execution_id = execution.to_execution_id().map_err(|e| e.to_string())?; - - log("Executing fee"); - let fee = execute_fee!( - process, - private_key, - fee_record, - fee_microcredits, - url, - fee_proving_key, - fee_verifying_key, - execution_id, - rng - ); + let (state, execution) = state.prove_execution(execute, url.clone()).await?; - // Verify the execution - process.verify_execution(&execution).map_err(|err| err.to_string())?; + let (_state, fee) = state + .execute_fee( + execution.execution_id()?, + url, + private_key.clone(), + fee_microcredits, + fee_record, + fee_proving_key, + fee_verifying_key, + ) + .await?; - log("Creating execution transaction"); - let transaction = TransactionNative::from_execution(execution, Some(fee)).map_err(|err| err.to_string())?; - Ok(Transaction::from(transaction)) + execution.into_transaction(Some(fee)).await } /// Estimate Fee for Aleo function execution. Note if "cache" is set to true, the proving and @@ -221,10 +164,10 @@ impl ProgramManager { #[allow(clippy::too_many_arguments)] pub async fn estimate_execution_fee( private_key: &PrivateKey, - program: &str, - function: &str, + program: String, + function: String, inputs: Array, - url: &str, + url: String, imports: Option, proving_key: Option, verifying_key: Option, @@ -234,58 +177,16 @@ impl ProgramManager { ); log(&format!("Executing local function: {function}")); - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; + let inputs = ProgramState::get_inputs(inputs)?; - log("Check program imports are valid and add them to the process"); - let program_native = ProgramNative::from_str(program).map_err(|e| e.to_string())?; - ProgramManager::resolve_imports(process, &program_native, imports)?; - let rng = &mut StdRng::from_entropy(); + let state = ProgramState::new(program, imports).await?; - log("Generating execution trace"); - let (_, mut trace) = execute_program!( - process, - process_inputs!(inputs), - program, - function, - private_key, - proving_key, - verifying_key, - rng - ); - - // Execute the program - let program = ProgramNative::from_str(program).map_err(|err| err.to_string())?; - let locator = program.id().to_string().add("/").add(function); - let query = QueryNative::from(url); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; - let execution = trace.prove_execution::(&locator, rng).map_err(|e| e.to_string())?; + let (state, execute) = + state.execute_program(function, inputs, private_key.clone(), proving_key, verifying_key).await?; - // Get the storage cost in bytes for the program execution - log("Estimating cost"); - let storage_cost = execution.size_in_bytes().map_err(|e| e.to_string())?; + let (state, execution) = state.prove_execution(execute, url).await?; - // Compute the finalize cost in microcredits. - let mut finalize_cost = 0u64; - // Iterate over the transitions to accumulate the finalize cost. - for transition in execution.transitions() { - // Retrieve the function name, program id, and program. - let function_name = transition.function_name(); - let program_id = transition.program_id(); - let program = process.get_program(program_id).map_err(|e| e.to_string())?; - - // Calculate the finalize cost for the function identified in the transition - let cost = match &program.get_function(function_name).map_err(|e| e.to_string())?.finalize_logic() { - Some(finalize) => cost_in_microcredits(finalize).map_err(|e| e.to_string())?, - None => continue, - }; - - // Accumulate the finalize cost. - finalize_cost = finalize_cost - .checked_add(cost) - .ok_or("The finalize cost computation overflowed for an execution".to_string())?; - } - Ok(storage_cost + finalize_cost) + state.estimate_fee(execution).await } /// Estimate the finalize fee component for executing a function. This fee is additional to the diff --git a/wasm/src/programs/manager/join.rs b/wasm/src/programs/manager/join.rs index af1adea72..ff0e9db76 100644 --- a/wasm/src/programs/manager/join.rs +++ b/wasm/src/programs/manager/join.rs @@ -16,20 +16,7 @@ use super::*; -use crate::{ - execute_fee, - execute_program, - log, - process_inputs, - types::{CurrentAleo, IdentifierNative, ProcessNative, ProgramNative, RecordPlaintextNative, TransactionNative}, - PrivateKey, - RecordPlaintext, - Transaction, -}; - -use js_sys::Array; -use rand::{rngs::StdRng, SeedableRng}; -use std::str::FromStr; +use crate::{log, types::ProgramNative, PrivateKey, RecordPlaintext, Transaction}; #[wasm_bindgen] impl ProgramManager { @@ -55,83 +42,44 @@ impl ProgramManager { record_2: RecordPlaintext, fee_credits: f64, fee_record: Option, - url: &str, + url: String, join_proving_key: Option, join_verifying_key: Option, fee_proving_key: Option, fee_verifying_key: Option, ) -> Result { log("Executing join program"); - let fee_microcredits = match &fee_record { - Some(fee_record) => Self::validate_amount(fee_credits, fee_record, true)?, - None => (fee_credits * 1_000_000.0) as u64, - }; - let rng = &mut StdRng::from_entropy(); + let fee_microcredits = Self::microcredits(fee_credits, &fee_record)?; - log("Setup program and inputs"); let program = ProgramNative::credits().unwrap().to_string(); - let inputs = Array::new_with_length(2); - inputs.set(0u32, wasm_bindgen::JsValue::from_str(&record_1.to_string())); - inputs.set(1u32, wasm_bindgen::JsValue::from_str(&record_2.to_string())); - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; + let state = ProgramState::new(program, None).await?; - let stack = process.get_stack("credits.aleo").map_err(|e| e.to_string())?; - let fee_identifier = if fee_record.is_some() { - IdentifierNative::from_str("fee_private").map_err(|e| e.to_string())? - } else { - IdentifierNative::from_str("fee_public").map_err(|e| e.to_string())? - }; - if !stack.contains_proving_key(&fee_identifier) && fee_proving_key.is_some() && fee_verifying_key.is_some() { - let fee_proving_key = fee_proving_key.clone().unwrap(); - let fee_verifying_key = fee_verifying_key.clone().unwrap(); - stack - .insert_proving_key(&fee_identifier, ProvingKeyNative::from(fee_proving_key)) - .map_err(|e| e.to_string())?; - stack - .insert_verifying_key(&fee_identifier, VerifyingKeyNative::from(fee_verifying_key)) - .map_err(|e| e.to_string())?; - } + let (state, fee_record, join_proving_key, join_verifying_key) = + state.insert_proving_keys(fee_record, join_proving_key, join_verifying_key).await?; - log("Executing the join function"); - let (_, mut trace) = execute_program!( - process, - process_inputs!(inputs), - &program, - "join", - private_key, - join_proving_key, - join_verifying_key, - rng - ); + let inputs = vec![record_1.to_string(), record_2.to_string()]; - log("Preparing inclusion proof for the join execution"); - let query = QueryNative::from(url); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; + let (state, mut execute) = state + .execute_program("join".to_string(), inputs, private_key.clone(), join_proving_key, join_verifying_key) + .await?; - log("Proving the join execution"); - let execution = trace.prove_execution::("credits.aleo/join", rng).map_err(|e| e.to_string())?; - let execution_id = execution.to_execution_id().map_err(|e| e.to_string())?; + execute.set_locator("credits.aleo/join".to_string()); - log("Verifying the join execution"); - process.verify_execution(&execution).map_err(|err| err.to_string())?; + let (state, execution) = state.prove_execution(execute, url.clone()).await?; - log("Executing the fee"); - let fee = execute_fee!( - process, - private_key, - fee_record, - fee_microcredits, - url, - fee_proving_key, - fee_verifying_key, - execution_id, - rng - ); + let (_state, fee) = state + .execute_fee( + execution.execution_id()?, + url, + private_key.clone(), + fee_microcredits, + fee_record, + fee_proving_key, + fee_verifying_key, + ) + .await?; - log("Creating execution transaction for join"); - let transaction = TransactionNative::from_execution(execution, Some(fee)).map_err(|err| err.to_string())?; - Ok(Transaction::from(transaction)) + execution.into_transaction(Some(fee)).await } } diff --git a/wasm/src/programs/manager/mod.rs b/wasm/src/programs/manager/mod.rs index 6ff96c4cf..66f098340 100644 --- a/wasm/src/programs/manager/mod.rs +++ b/wasm/src/programs/manager/mod.rs @@ -30,34 +30,500 @@ pub mod transfer; pub use transfer::*; use crate::{ + log, types::{ cost_in_microcredits, deployment_cost, + CurrentAleo, + CurrentNetwork, + Execution, + Field, IdentifierNative, ProcessNative, ProgramIDNative, ProgramNative, + ProgramOwnerNative, ProvingKeyNative, QueryNative, + Response, + Testnet3, + TransactionNative, VerifyingKeyNative, }, + ExecutionResponse, KeyPair, PrivateKey, ProvingKey, RecordPlaintext, + RecordPlaintextNative, + Transaction, VerifyingKey, }; -use js_sys::{Object, Reflect}; -use std::str::FromStr; +use snarkvm_ledger_block::{Deployment, Fee}; +use snarkvm_synthesizer::Trace; + +use core::ops::Add; +use js_sys::{Array, Object, Reflect}; +use rand::{rngs::StdRng, SeedableRng}; +use std::{collections::HashMap, future::Future, str::FromStr}; use wasm_bindgen::prelude::wasm_bindgen; +/// Converts a JS object into a `HashMap`. +fn get_imports(imports: Option) -> Result>, String> { + if let Some(imports) = imports { + let mut hash = HashMap::new(); + + for key in Object::keys(&imports).iter() { + let value = Reflect::get(&imports, &key).unwrap(); + + let key = key.as_string().ok_or_else(|| "Import key must be a string".to_string())?; + let value = value.as_string().ok_or_else(|| "Import value must be a string".to_string())?; + + hash.insert(key, value); + } + + Ok(Some(hash)) + } else { + Ok(None) + } +} + +pub(crate) struct ExecuteFee { + fee: Fee, + private_key: PrivateKey, +} + +pub(crate) struct ProveExecution { + execute: ExecuteProgram, + execution: Execution, +} + +impl ProveExecution { + pub(crate) fn execution_id(&self) -> Result, String> { + self.execution.to_execution_id().map_err(|e| e.to_string()) + } + + pub(crate) fn into_transaction(self, fee: Option) -> impl Future> { + crate::thread_pool::spawn(move || { + log("Creating execution transaction"); + let transaction = TransactionNative::from_execution(self.execution, fee.map(|fee| fee.fee)) + .map_err(|err| err.to_string())?; + + Ok(Transaction::from(transaction)) + }) + } +} + +pub(crate) struct Deploy { + deployment: Deployment, + minimum_deployment_cost: u64, +} + +impl Deploy { + pub(crate) fn minimum_deployment_cost(&self) -> u64 { + self.minimum_deployment_cost + } + + pub(crate) fn execution_id(&self) -> Result, String> { + self.deployment.to_deployment_id().map_err(|e| e.to_string()) + } + + pub(crate) fn check_fee(&self, fee_microcredits: u64) -> Result<(), String> { + log("Ensuring the fee is sufficient to pay for the deployment"); + + if fee_microcredits < self.minimum_deployment_cost { + return Err(format!( + "Fee is too low to pay for the deployment. The minimum fee is {} credits", + self.minimum_deployment_cost as f64 / 1_000_000.0 + )); + } + + Ok(()) + } +} + +pub(crate) struct ExecuteProgram { + locator: String, + response: Response, + trace: Trace, +} + +impl ExecuteProgram { + pub(crate) fn set_locator(&mut self, locator: String) { + self.locator = locator; + } +} + +pub(crate) struct ProgramState { + rng: StdRng, + process: ProcessNative, + program: ProgramNative, +} + +impl ProgramState { + /// Converts a JS array of strings into a `Vec`. + pub(crate) fn get_inputs(inputs: Array) -> Result, String> { + inputs + .iter() + .map(|input| { + if let Some(input) = input.as_string() { + Ok(input) + } else { + Err("Invalid input - all inputs must be a string specifying the type".to_string()) + } + }) + .collect() + } + + pub(crate) fn new(program_string: String, imports: Option) -> impl Future> { + let imports = get_imports(imports); + + crate::thread_pool::spawn(move || { + let imports = imports?; + + let rng = StdRng::from_entropy(); + + let mut process = ProcessNative::load_web().map_err(|err| err.to_string())?; + + log("Loading program"); + let program = ProgramNative::from_str(&program_string).map_err(|e| e.to_string())?; + + log("Check program imports are valid and add them to the process"); + ProgramManager::resolve_imports(&mut process, &program, imports.as_ref())?; + + Ok(Self { rng, process, program }) + }) + } + + /// Check if a process contains a keypair for a specific function + fn contains_key(&self, program_id: &ProgramIDNative, function_id: &IdentifierNative) -> bool { + self.process.get_stack(program_id).map_or_else( + |_| false, + |stack| stack.contains_proving_key(function_id) && stack.contains_verifying_key(function_id), + ) + } + + pub(crate) fn insert_proving_keys( + self, + fee_record: Option, + fee_proving_key: Option, + fee_verifying_key: Option, + ) -> impl Future, Option, Option), String>> + { + crate::thread_pool::spawn(move || { + let fee_identifier = if fee_record.is_some() { + IdentifierNative::from_str("fee_private").map_err(|e| e.to_string())? + } else { + IdentifierNative::from_str("fee_public").map_err(|e| e.to_string())? + }; + + let stack = self.process.get_stack("credits.aleo").map_err(|e| e.to_string())?; + + if !stack.contains_proving_key(&fee_identifier) && fee_proving_key.is_some() && fee_verifying_key.is_some() + { + let fee_proving_key = fee_proving_key.clone().unwrap(); + let fee_verifying_key = fee_verifying_key.clone().unwrap(); + stack + .insert_proving_key(&fee_identifier, ProvingKeyNative::from(fee_proving_key)) + .map_err(|e| e.to_string())?; + stack + .insert_verifying_key(&fee_identifier, VerifyingKeyNative::from(fee_verifying_key)) + .map_err(|e| e.to_string())?; + } + + Ok((self, fee_record, fee_proving_key, fee_verifying_key)) + }) + } + + pub(crate) fn execute_program( + mut self, + function_id: String, + inputs: Vec, + private_key: PrivateKey, + proving_key: Option, + verifying_key: Option, + ) -> impl Future> { + crate::thread_pool::spawn(move || { + if (proving_key.is_some() && verifying_key.is_none()) || (proving_key.is_none() && verifying_key.is_some()) + { + return Err( + "If specifying a key for a program execution, both the proving and verifying key must be specified" + .to_string(), + ); + } + + log("Loading function"); + let function_name = IdentifierNative::from_str(&function_id) + .map_err(|_| "The function name provided was invalid".to_string())?; + + let id = self.program.id(); + + let program_id = id.to_string(); + + if program_id != "credits.aleo" { + log("Adding program to the process"); + if let Ok(stored_program) = self.process.get_program(id) { + if stored_program != &self.program { + return Err("The program provided does not match the program stored in the cache, please clear the cache before proceeding".to_string()); + } + } else { + self.process.add_program(&self.program).map_err(|e| e.to_string())?; + } + } + + if let Some(proving_key) = proving_key { + if self.contains_key(id, &function_name) { + log(&format!( + "Proving & verifying keys were specified for {program_id} - {function_name:?} but a key already exists in the cache. Using cached keys" + )); + } else { + log(&format!( + "Inserting externally provided proving and verifying keys for {program_id} - {function_name:?}" + )); + + self.process + .insert_proving_key(id, &function_name, ProvingKeyNative::from(proving_key)) + .map_err(|e| e.to_string())?; + + if let Some(verifying_key) = verifying_key { + self.process + .insert_verifying_key(id, &function_name, VerifyingKeyNative::from(verifying_key)) + .map_err(|e| e.to_string())?; + } + } + } + + log("Creating authorization"); + let authorization = self + .process + .authorize::(&private_key, id, function_name, inputs.into_iter(), &mut self.rng) + .map_err(|err| err.to_string())?; + + log("Executing program"); + let (response, trace) = + self.process.execute::(authorization).map_err(|err| err.to_string())?; + + let locator = program_id.add("/").add(&function_id); + + Ok((self, ExecuteProgram { locator, response, trace })) + }) + } + + pub(crate) fn prove_execution( + mut self, + mut execute: ExecuteProgram, + url: String, + ) -> impl Future> { + async move { + log("Preparing inclusion proofs for execution"); + let query = QueryNative::from(url); + execute.trace.prepare_async(query).await.map_err(|err| err.to_string())?; + + crate::thread_pool::spawn(move || { + log("Proving execution"); + let execution = execute + .trace + .prove_execution::(&execute.locator, &mut self.rng) + .map_err(|e| e.to_string())?; + + // Verify the execution + self.process.verify_execution(&execution).map_err(|err| err.to_string())?; + + Ok((self, ProveExecution { execution, execute })) + }) + .await + } + } + + pub(crate) fn execute_response(self, execute: ExecuteProgram, cache: bool) -> Result { + let process = if cache { Some(self.process) } else { None }; + + Ok(ExecutionResponse::from((execute.response, process))) + } + + pub(crate) fn prove_response(self, prove: ProveExecution, cache: bool) -> Result { + let ProveExecution { execute: ExecuteProgram { response, .. }, execution } = prove; + + let process = if cache { Some(self.process) } else { None }; + + Ok(ExecutionResponse::from((response, execution, process))) + } + + pub(crate) fn estimate_fee(self, execution: ProveExecution) -> impl Future> { + crate::thread_pool::spawn(move || { + // Get the storage cost in bytes for the program execution + log("Estimating cost"); + + let storage_cost = execution.execution.size_in_bytes().map_err(|e| e.to_string())?; + + // Compute the finalize cost in microcredits. + let mut finalize_cost = 0u64; + // Iterate over the transitions to accumulate the finalize cost. + for transition in execution.execution.transitions() { + // Retrieve the function name, program id, and program. + let function_name = transition.function_name(); + let program_id = transition.program_id(); + let program = self.process.get_program(program_id).map_err(|e| e.to_string())?; + + // Calculate the finalize cost for the function identified in the transition + let cost = match &program.get_function(function_name).map_err(|e| e.to_string())?.finalize_logic() { + Some(finalize) => cost_in_microcredits(finalize).map_err(|e| e.to_string())?, + None => continue, + }; + + // Accumulate the finalize cost. + finalize_cost = finalize_cost + .checked_add(cost) + .ok_or("The finalize cost computation overflowed for an execution".to_string())?; + } + + Ok(storage_cost + finalize_cost) + }) + } + + pub(crate) fn execute_fee( + mut self, + execution_id: Field, + submission_url: String, + private_key: PrivateKey, + fee_microcredits: u64, + fee_record: Option, + fee_proving_key: Option, + fee_verifying_key: Option, + ) -> impl Future> { + async move { + log("Executing fee"); + + let (mut state, execution_id, private_key, mut trace) = crate::thread_pool::spawn(move || { + if (fee_proving_key.is_some() && fee_verifying_key.is_none()) || + (fee_proving_key.is_none() && fee_verifying_key.is_some()) + { + return Err( + "Missing key - both the proving and verifying key must be specified for a program execution" + .to_string(), + ); + } + + if let Some(fee_proving_key) = fee_proving_key { + let credits = ProgramIDNative::from_str("credits.aleo").unwrap(); + let fee = if fee_record.is_some() { + IdentifierNative::from_str("fee_private").unwrap() + } else { + IdentifierNative::from_str("fee_public").unwrap() + }; + if self.contains_key(&credits, &fee) { + log("Fee proving & verifying keys were specified but a key already exists in the cache. Using cached keys"); + } else { + log("Inserting externally provided fee proving and verifying keys"); + self.process + .insert_proving_key(&credits, &fee, ProvingKeyNative::from(fee_proving_key)).map_err(|e| e.to_string())?; + if let Some(fee_verifying_key) = fee_verifying_key { + self.process + .insert_verifying_key(&credits, &fee, VerifyingKeyNative::from(fee_verifying_key)) + .map_err(|e| e.to_string())?; + } + } + }; + + log("Authorizing Fee"); + let fee_authorization = match fee_record { + Some(fee_record) => { + let fee_record_native = RecordPlaintextNative::from_str(&fee_record.to_string()).unwrap(); + self.process.authorize_fee_private::( + &private_key, + fee_record_native, + fee_microcredits, + 0u64, + execution_id, + &mut self.rng, + ).map_err(|e| e.to_string())? + } + None => { + self.process.authorize_fee_public::(&private_key, fee_microcredits, 0u64, execution_id, &mut self.rng) + .map_err(|e| e.to_string())? + } + }; + + log("Executing fee"); + let (_, trace) = self.process + .execute::(fee_authorization) + .map_err(|err| err.to_string())?; + + Ok((self, execution_id, private_key, trace)) + }).await?; + + let query = QueryNative::from(submission_url); + trace.prepare_async(query).await.map_err(|err| err.to_string())?; + + crate::thread_pool::spawn(move || { + let fee = trace.prove_fee::(&mut state.rng).map_err(|e| e.to_string())?; + + log("Verifying fee execution"); + state.process.verify_fee(&fee, execution_id).map_err(|e| e.to_string())?; + + Ok((state, ExecuteFee { fee, private_key })) + }) + .await + } + } + + pub(crate) fn deploy(mut self) -> impl Future> { + crate::thread_pool::spawn(move || { + log("Creating deployment"); + let deployment = + self.process.deploy::(&self.program, &mut self.rng).map_err(|err| err.to_string())?; + + if deployment.program().functions().is_empty() { + return Err("Attempted to create an empty transaction deployment".to_string()); + } + + log("Estimate the deployment fee"); + let (minimum_deployment_cost, (_, _)) = + deployment_cost::(&deployment).map_err(|err| err.to_string())?; + + Ok((self, Deploy { deployment, minimum_deployment_cost })) + }) + } + + pub(crate) fn deploy_transaction( + mut self, + deploy: Deploy, + fee: ExecuteFee, + ) -> impl Future> { + crate::thread_pool::spawn(move || { + let deployment_id = deploy.execution_id()?; + + // Create the program owner + let owner = ProgramOwnerNative::new(&fee.private_key, deployment_id, &mut self.rng) + .map_err(|err| err.to_string())?; + + log("Verifying the deployment and fees"); + self.process + .verify_deployment::(&deploy.deployment, &mut self.rng) + .map_err(|err| err.to_string())?; + + log("Creating deployment transaction"); + Ok(Transaction::from( + TransactionNative::from_deployment(owner, deploy.deployment, fee.fee).map_err(|err| err.to_string())?, + )) + }) + } +} + #[wasm_bindgen] #[derive(Clone)] pub struct ProgramManager; #[wasm_bindgen] impl ProgramManager { + pub(crate) fn microcredits(fee_credits: f64, fee_record: &Option) -> Result { + match fee_record { + Some(fee_record) => Self::validate_amount(fee_credits, fee_record, true), + None => Ok((fee_credits * 1_000_000.0) as u64), + } + } + /// Validate that an amount being paid from a record is greater than zero and that the record /// has enough credits to pay the amount pub(crate) fn validate_amount(credits: f64, amount: &RecordPlaintext, fee: bool) -> Result { @@ -83,16 +549,16 @@ impl ProgramManager { #[wasm_bindgen(js_name = "synthesizeKeyPair")] pub async fn synthesize_keypair( private_key: &PrivateKey, - program: &str, - function_id: &str, + program: String, + function_id: String, inputs: js_sys::Array, imports: Option, ) -> Result { - let program_id = ProgramNative::from_str(program).map_err(|e| e.to_string())?.id().to_string(); + let program_id = ProgramNative::from_str(&program).map_err(|e| e.to_string())?.id().to_string(); ProgramManager::execute_function_offline( private_key, program, - function_id, + function_id.clone(), inputs, false, true, @@ -101,47 +567,35 @@ impl ProgramManager { None, ) .await? - .get_keys(&program_id, function_id) - } - - /// Check if a process contains a keypair for a specific function - pub(crate) fn contains_key( - process: &ProcessNative, - program_id: &ProgramIDNative, - function_id: &IdentifierNative, - ) -> bool { - process.get_stack(program_id).map_or_else( - |_| false, - |stack| stack.contains_proving_key(function_id) && stack.contains_verifying_key(function_id), - ) + .get_keys(&program_id, &function_id) } /// Resolve imports for a program in depth first search order pub(crate) fn resolve_imports( process: &mut ProcessNative, program: &ProgramNative, - imports: Option, + imports: Option<&HashMap>, ) -> Result<(), String> { if let Some(imports) = imports { program.imports().keys().try_for_each(|program_id| { // Get the program string let program_id = program_id.to_string(); - if let Some(import_string) = Reflect::get(&imports, &program_id.as_str().into()) - .map_err(|_| "Program import not found in imports provided".to_string())? - .as_string() - { + if let Some(import_string) = imports.get(program_id.as_str()) { if &program_id != "credits.aleo" { crate::log(&format!("Importing program: {}", program_id)); let import = ProgramNative::from_str(&import_string).map_err(|err| err.to_string())?; // If the program has imports, add them - Self::resolve_imports(process, &import, Some(imports.clone()))?; + Self::resolve_imports(process, &import, Some(imports))?; // If the process does not already contain the program, add it if !process.contains_program(import.id()) { process.add_program(&import).map_err(|err| err.to_string())?; } } + + Ok::<(), String>(()) + } else { + Err("Program import not found in imports provided".to_string()) } - Ok::<(), String>(()) }) } else { Ok(()) @@ -153,8 +607,6 @@ impl ProgramManager { mod tests { use super::*; - use js_sys::{Object, Reflect}; - use wasm_bindgen::JsValue; use wasm_bindgen_test::*; pub const MULTIPLY_PROGRAM: &str = r#"// The 'multiply_test.aleo' program which is imported by the 'double_test.aleo' program. @@ -205,11 +657,13 @@ function add_and_double: #[wasm_bindgen_test] fn test_import_resolution() { - let imports = Object::new(); - Reflect::set(&imports, &JsValue::from_str("multiply_test.aleo"), &JsValue::from_str(MULTIPLY_PROGRAM)).unwrap(); - Reflect::set(&imports, &JsValue::from_str("addition_test.aleo"), &JsValue::from_str(ADDITION_PROGRAM)).unwrap(); - Reflect::set(&imports, &JsValue::from_str("double_test.aleo"), &JsValue::from_str(MULTIPLY_IMPORT_PROGRAM)) - .unwrap(); + let imports = [ + ("multiply_test.aleo".to_string(), MULTIPLY_PROGRAM.to_string()), + ("addition_test.aleo".to_string(), ADDITION_PROGRAM.to_string()), + ("double_test.aleo".to_string(), MULTIPLY_IMPORT_PROGRAM.to_string()), + ] + .into_iter() + .collect(); let mut process = ProcessNative::load_web().unwrap(); let program = ProgramNative::from_str(NESTED_IMPORT_PROGRAM).unwrap(); @@ -217,7 +671,7 @@ function add_and_double: let multiply_program = ProgramNative::from_str(MULTIPLY_PROGRAM).unwrap(); let double_program = ProgramNative::from_str(MULTIPLY_IMPORT_PROGRAM).unwrap(); - ProgramManager::resolve_imports(&mut process, &program, Some(imports)).unwrap(); + ProgramManager::resolve_imports(&mut process, &program, Some(&imports)).unwrap(); let add_import = process.get_program("addition_test.aleo").unwrap(); let multiply_import = process.get_program("multiply_test.aleo").unwrap(); diff --git a/wasm/src/programs/manager/split.rs b/wasm/src/programs/manager/split.rs index e89fca776..a9bbfff50 100644 --- a/wasm/src/programs/manager/split.rs +++ b/wasm/src/programs/manager/split.rs @@ -16,19 +16,9 @@ use super::*; -use crate::{ - execute_program, - log, - process_inputs, - types::{CurrentAleo, IdentifierNative, ProcessNative, ProgramNative, TransactionNative}, - PrivateKey, - RecordPlaintext, - Transaction, -}; +use crate::{log, types::ProgramNative, PrivateKey, RecordPlaintext, Transaction}; -use js_sys::Array; -use rand::{rngs::StdRng, SeedableRng}; -use std::{ops::Add, str::FromStr}; +use std::ops::Add; #[wasm_bindgen] impl ProgramManager { @@ -48,48 +38,27 @@ impl ProgramManager { private_key: &PrivateKey, split_amount: f64, amount_record: RecordPlaintext, - url: &str, + url: String, split_proving_key: Option, split_verifying_key: Option, ) -> Result { log("Executing split program"); let amount_microcredits = Self::validate_amount(split_amount, &amount_record, false)?; - log("Setup the program and inputs"); let program = ProgramNative::credits().unwrap().to_string(); - let inputs = Array::new_with_length(2u32); - inputs.set(0u32, wasm_bindgen::JsValue::from_str(&amount_record.to_string())); - inputs.set(1u32, wasm_bindgen::JsValue::from_str(&amount_microcredits.to_string().add("u64"))); - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; - let rng = &mut StdRng::from_entropy(); + let state = ProgramState::new(program, None).await?; - log("Executing the split function"); - let (_, mut trace) = execute_program!( - process, - process_inputs!(inputs), - &program, - "split", - private_key, - split_proving_key, - split_verifying_key, - rng - ); + let inputs = vec![amount_record.to_string(), amount_microcredits.to_string().add("u64")]; - log("Preparing the inclusion proof for the split execution"); - let query = QueryNative::from(url); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; + let (state, mut execute) = state + .execute_program("split".to_string(), inputs, private_key.clone(), split_proving_key, split_verifying_key) + .await?; - log("Proving the split execution"); - let execution = - trace.prove_execution::("credits.aleo/split", rng).map_err(|e| e.to_string())?; + execute.set_locator("credits.aleo/split".to_string()); - log("Verifying the split execution"); - process.verify_execution(&execution).map_err(|err| err.to_string())?; + let (_state, execution) = state.prove_execution(execute, url).await?; - log("Creating execution transaction for split"); - let transaction = TransactionNative::from_execution(execution, None).map_err(|err| err.to_string())?; - Ok(Transaction::from(transaction)) + execution.into_transaction(None).await } } diff --git a/wasm/src/programs/manager/transfer.rs b/wasm/src/programs/manager/transfer.rs index c590f96c1..a9b07104d 100644 --- a/wasm/src/programs/manager/transfer.rs +++ b/wasm/src/programs/manager/transfer.rs @@ -16,20 +16,9 @@ use super::*; -use crate::{ - execute_fee, - execute_program, - log, - process_inputs, - types::{CurrentAleo, IdentifierNative, ProcessNative, ProgramNative, RecordPlaintextNative, TransactionNative}, - PrivateKey, - RecordPlaintext, - Transaction, -}; - -use js_sys::Array; -use rand::{rngs::StdRng, SeedableRng}; -use std::{ops::Add, str::FromStr}; +use crate::{log, types::ProgramNative, PrivateKey, RecordPlaintext, Transaction}; + +use std::ops::Add; #[wasm_bindgen] impl ProgramManager { @@ -53,128 +42,98 @@ impl ProgramManager { pub async fn transfer( private_key: &PrivateKey, amount_credits: f64, - recipient: &str, + recipient: String, transfer_type: &str, amount_record: Option, fee_credits: f64, fee_record: Option, - url: &str, + url: String, transfer_proving_key: Option, transfer_verifying_key: Option, fee_proving_key: Option, fee_verifying_key: Option, ) -> Result { log("Executing transfer program"); - let fee_microcredits = match &fee_record { - Some(fee_record) => Self::validate_amount(fee_credits, fee_record, true)?, - None => (fee_credits * 1_000_000.0) as u64, - }; - let amount_microcredits = match &amount_record { - Some(amount_record) => Self::validate_amount(amount_credits, amount_record, true)?, - None => (amount_credits * 1_000_000.0) as u64, - }; + let fee_microcredits = Self::microcredits(fee_credits, &fee_record)?; + let amount_microcredits = Self::microcredits(amount_credits, &amount_record)?; log("Setup the program and inputs"); let program = ProgramNative::credits().unwrap().to_string(); - let rng = &mut StdRng::from_entropy(); log("Transfer Type is:"); log(transfer_type); - let (transfer_type, inputs) = match transfer_type { + let mut inputs = vec![]; + + let transfer_type = match transfer_type { "private" | "transfer_private" | "transferPrivate" => { if amount_record.is_none() { return Err("Amount record must be provided for private transfers".to_string()); } - let inputs = Array::new_with_length(3); - inputs.set(0u32, wasm_bindgen::JsValue::from_str(&amount_record.unwrap().to_string())); - inputs.set(1u32, wasm_bindgen::JsValue::from_str(recipient)); - inputs.set(2u32, wasm_bindgen::JsValue::from_str(&amount_microcredits.to_string().add("u64"))); - ("transfer_private", inputs) + + inputs.push(amount_record.unwrap().to_string()); + inputs.push(recipient); + inputs.push(amount_microcredits.to_string().add("u64")); + + "transfer_private" } "private_to_public" | "privateToPublic" | "transfer_private_to_public" | "transferPrivateToPublic" => { if amount_record.is_none() { return Err("Amount record must be provided for private transfers".to_string()); } - let inputs = Array::new_with_length(3); - inputs.set(0u32, wasm_bindgen::JsValue::from_str(&amount_record.unwrap().to_string())); - inputs.set(1u32, wasm_bindgen::JsValue::from_str(recipient)); - inputs.set(2u32, wasm_bindgen::JsValue::from_str(&amount_microcredits.to_string().add("u64"))); - ("transfer_private_to_public", inputs) + + inputs.push(amount_record.unwrap().to_string()); + inputs.push(recipient); + inputs.push(amount_microcredits.to_string().add("u64")); + + "transfer_private_to_public" } "public" | "transfer_public" | "transferPublic" => { - let inputs = Array::new_with_length(2); - inputs.set(0u32, wasm_bindgen::JsValue::from_str(recipient)); - inputs.set(1u32, wasm_bindgen::JsValue::from_str(&amount_microcredits.to_string().add("u64"))); - ("transfer_public", inputs) + inputs.push(recipient); + inputs.push(amount_microcredits.to_string().add("u64")); + + "transfer_public" } "public_to_private" | "publicToPrivate" | "transfer_public_to_private" | "transferPublicToPrivate" => { - let inputs = Array::new_with_length(2); - inputs.set(0u32, wasm_bindgen::JsValue::from_str(recipient)); - inputs.set(1u32, wasm_bindgen::JsValue::from_str(&amount_microcredits.to_string().add("u64"))); - ("transfer_public_to_private", inputs) + inputs.push(recipient); + inputs.push(amount_microcredits.to_string().add("u64")); + + "transfer_public_to_private" } _ => return Err("Invalid transfer type".to_string()), }; - let mut process_native = ProcessNative::load_web().map_err(|err| err.to_string())?; - let process = &mut process_native; - let fee_identifier = if fee_record.is_some() { - IdentifierNative::from_str("fee_private").map_err(|e| e.to_string())? - } else { - IdentifierNative::from_str("fee_public").map_err(|e| e.to_string())? - }; - let stack = process.get_stack("credits.aleo").map_err(|e| e.to_string())?; - if !stack.contains_proving_key(&fee_identifier) && fee_proving_key.is_some() && fee_verifying_key.is_some() { - let fee_proving_key = fee_proving_key.clone().unwrap(); - let fee_verifying_key = fee_verifying_key.clone().unwrap(); - stack - .insert_proving_key(&fee_identifier, ProvingKeyNative::from(fee_proving_key)) - .map_err(|e| e.to_string())?; - stack - .insert_verifying_key(&fee_identifier, VerifyingKeyNative::from(fee_verifying_key)) - .map_err(|e| e.to_string())?; - } - - log("Executing transfer function"); - let (_, mut trace) = execute_program!( - process, - process_inputs!(inputs), - &program, - transfer_type, - private_key, - transfer_proving_key, - transfer_verifying_key, - rng - ); - - log("Preparing the inclusion proof for the transfer execution"); - let query = QueryNative::from(url); - trace.prepare_async(query).await.map_err(|err| err.to_string())?; - - log("Proving the transfer execution"); - let execution = - trace.prove_execution::("credits.aleo/transfer", rng).map_err(|e| e.to_string())?; - let execution_id = execution.to_execution_id().map_err(|e| e.to_string())?; - - log("Verifying the transfer execution"); - process.verify_execution(&execution).map_err(|err| err.to_string())?; - - log("Executing the fee"); - let fee = execute_fee!( - process, - private_key, - fee_record, - fee_microcredits, - url, - fee_proving_key, - fee_verifying_key, - execution_id, - rng - ); - - log("Creating execution transaction for transfer"); - let transaction = TransactionNative::from_execution(execution, Some(fee)).map_err(|err| err.to_string())?; - Ok(Transaction::from(transaction)) + let state = ProgramState::new(program, None).await?; + + let (state, fee_record, fee_proving_key, fee_verifying_key) = + state.insert_proving_keys(fee_record, fee_proving_key, fee_verifying_key).await?; + + let (state, mut execute) = state + .execute_program( + transfer_type.to_string(), + inputs, + private_key.clone(), + transfer_proving_key, + transfer_verifying_key, + ) + .await?; + + execute.set_locator("credits.aleo/transfer".to_string()); + + let (state, execution) = state.prove_execution(execute, url.clone()).await?; + + let (_state, fee) = state + .execute_fee( + execution.execution_id()?, + url, + private_key.clone(), + fee_microcredits, + fee_record, + fee_proving_key, + fee_verifying_key, + ) + .await?; + + execution.into_transaction(Some(fee)).await } } diff --git a/wasm/src/programs/mod.rs b/wasm/src/programs/mod.rs index abe373b38..6614c6bb6 100644 --- a/wasm/src/programs/mod.rs +++ b/wasm/src/programs/mod.rs @@ -14,8 +14,6 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see . -mod macros; - pub mod key_pair; pub use key_pair::*; diff --git a/wasm/src/thread_pool/mod.rs b/wasm/src/thread_pool/mod.rs index c2c18c4b0..98d45db45 100644 --- a/wasm/src/thread_pool/mod.rs +++ b/wasm/src/thread_pool/mod.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with the Aleo SDK library. If not, see . -use futures::future::try_join_all; +use futures::{channel::oneshot, future::try_join_all}; use rayon::ThreadBuilder; use spmc::{channel, Receiver, Sender}; use std::future::Future; @@ -29,13 +29,17 @@ use wasm_bindgen_futures::JsFuture; }); worker.addEventListener("message", (event) => { - // When running in Node, this allows the process to exit - // even though the Worker is still running. - if (worker.unref) { - worker.unref(); - } - - resolve(worker); + // This is needed in Node to wait one extra tick, so that way + // the Worker can fully initialize before we return. + setTimeout(() => { + resolve(worker); + + // When running in Node, this allows the process to exit + // even though the Worker is still running. + if (worker.unref) { + worker.unref(); + } + }, 0); }, { capture: true, once: true, @@ -48,6 +52,16 @@ use wasm_bindgen_futures::JsFuture; }); }); } + + export function startTimer() { + // Starts a super-long timer in order to keep the Node + // process alive until we manually cancel it. + return setTimeout(() => {}, Math.pow(2, 31) - 1); + } + + export function stopTimer(timer) { + clearTimeout(timer); + } "###)] extern "C" { #[wasm_bindgen(js_name = spawnWorker)] @@ -57,6 +71,51 @@ extern "C" { memory: &JsValue, address: *const Receiver, ) -> js_sys::Promise; + + #[wasm_bindgen(js_name = startTimer)] + fn start_timer() -> f64; + + #[wasm_bindgen(js_name = stopTimer)] + fn stop_timer(timer: f64); +} + +/// Runs a function on the Rayon thread-pool. +/// +/// When the function returns, the Future will resolve +/// with the return value of the function. +/// +/// # NodeJS +/// +/// This will keep the NodeJS process alive until the +/// Future is resolved. +pub fn spawn(f: F) -> impl Future +where + A: Send + 'static, + F: FnOnce() -> A + Send + 'static, +{ + // This is necessary in order to stop the Node process + // from exiting while the spawned task is running. + struct Timer(f64); + + impl Drop for Timer { + fn drop(&mut self) { + stop_timer(self.0); + } + } + + let timer = Timer(start_timer()); + + let (sender, receiver) = oneshot::channel(); + + rayon::spawn(move || { + let _ = sender.send(f()); + }); + + async move { + let output = receiver.await.unwrap_throw(); + drop(timer); + output + } } async fn spawn_workers(url: web_sys::Url, num_threads: usize) -> Result, JsValue> { diff --git a/wasm/tests/offchain.rs b/wasm/tests/offchain.rs index e74af29ef..c65485c30 100644 --- a/wasm/tests/offchain.rs +++ b/wasm/tests/offchain.rs @@ -110,7 +110,10 @@ async fn test_key_synthesis() { inputs.set(0u32, JsValue::from_str(RECORD)); inputs.set(1u32, JsValue::from_str("5u64")); let private_key = PrivateKey::from_string("APrivateKey1zkp3dQx4WASWYQVWKkq14v3RoQDfY2kbLssUj7iifi1VUQ6").unwrap(); - let mut key_pair = ProgramManager::synthesize_keypair(&private_key, &credits, "split", inputs, None).await.unwrap(); + let mut key_pair = + ProgramManager::synthesize_keypair(&private_key, credits.clone(), "split".to_string(), inputs, None) + .await + .unwrap(); let retrieved_proving_key = key_pair.proving_key().unwrap(); let retreived_verifying_key = key_pair.verifying_key().unwrap(); @@ -120,8 +123,8 @@ async fn test_key_synthesis() { inputs.set(1u32, JsValue::from_str("5u64")); let result = ProgramManager::execute_function_offline( &PrivateKey::from_string("APrivateKey1zkp3dQx4WASWYQVWKkq14v3RoQDfY2kbLssUj7iifi1VUQ6").unwrap(), - &credits, - "split", + credits, + "split".to_string(), inputs, false, true, @@ -151,12 +154,12 @@ async fn test_fee_validation() { // Ensure execution fails when fee amount is greater than the balance available in the record let execution = ProgramManager::execute( &private_key, - &Program::get_credits_program().to_string(), - "split", + Program::get_credits_program().to_string(), + "split".to_string(), inputs, 100.0, Some(fee_record.clone()), - "https://vm.aleo.org/api", + "https://vm.aleo.org/api".to_string(), None, None, None, @@ -169,10 +172,10 @@ async fn test_fee_validation() { // Ensure deployment fails when fee amount is greater than the balance available in the record let deployment = ProgramManager::deploy( &private_key, - &Program::get_credits_program().to_string(), + Program::get_credits_program().to_string(), 100.0, Some(fee_record.clone()), - "https://vm.aleo.org/api", + "https://vm.aleo.org/api".to_string(), None, None, None, @@ -184,12 +187,12 @@ async fn test_fee_validation() { let transfer = ProgramManager::transfer( &private_key, 100.00, - "aleo184vuwr5u7u0ha5f5k44067dd2uaqewxx6pe5ltha5pv99wvhfqxqv339h4", + "aleo184vuwr5u7u0ha5f5k44067dd2uaqewxx6pe5ltha5pv99wvhfqxqv339h4".to_string(), "private", Some(fee_record.clone()), 0.9, Some(fee_record.clone()), - "https://vm.aleo.org/api", + "https://vm.aleo.org/api".to_string(), None, None, None, @@ -201,12 +204,12 @@ async fn test_fee_validation() { let transfer = ProgramManager::transfer( &private_key, 0.5, - "aleo184vuwr5u7u0ha5f5k44067dd2uaqewxx6pe5ltha5pv99wvhfqxqv339h4", + "aleo184vuwr5u7u0ha5f5k44067dd2uaqewxx6pe5ltha5pv99wvhfqxqv339h4".to_string(), "private", Some(fee_record.clone()), 100.00, Some(fee_record.clone()), - "https://vm.aleo.org/api", + "https://vm.aleo.org/api".to_string(), None, None, None, @@ -222,7 +225,7 @@ async fn test_fee_validation() { fee_record.clone(), 100.00, Some(fee_record.clone()), - "https://vm.aleo.org/api", + "https://vm.aleo.org/api".to_string(), None, None, None, @@ -241,7 +244,7 @@ async fn test_fee_estimation() { inputs.set(1, wasm_bindgen::JsValue::from_str("15u64")); // Ensure the deployment fee is correct and the cache is used - let deployment_fee = ProgramManager::estimate_deployment_fee(FINALIZE, None).await.unwrap(); + let deployment_fee = ProgramManager::estimate_deployment_fee(FINALIZE.to_string(), None).await.unwrap(); let namespace_fee = ProgramManager::program_name_cost("tencharacters.aleo").unwrap(); assert_eq!(namespace_fee, 1000000); @@ -254,10 +257,10 @@ async fn test_fee_estimation() { let execution_fee = ProgramManager::estimate_execution_fee( &private_key, - FINALIZE, - "integer_key_mapping_update", + FINALIZE.to_string(), + "integer_key_mapping_update".to_string(), inputs, - "https://vm.aleo.org/api", + "https://vm.aleo.org/api".to_string(), None, None, None, @@ -288,8 +291,8 @@ async fn test_import_resolution() { let result = ProgramManager::execute_function_offline( &private_key, - NESTED_IMPORT_PROGRAM, - "add_and_double", + NESTED_IMPORT_PROGRAM.to_string(), + "add_and_double".to_string(), inputs, false, false,