aleo_rust/program/
execute.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the Aleo SDK library.
3
4// The Aleo SDK library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Aleo SDK library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18
19impl<N: Network> ProgramManager<N> {
20    /// Create an offline execution of a program to share with a third party.
21    ///
22    /// DISCLAIMER: Offline executions will not interact with the Aleo network and cannot use all
23    /// of the features of the Leo programming language or Aleo instructions. Any code written
24    /// inside finalize blocks will not be executed, mappings cannot be initialized, updated or read,
25    /// and a chain of records cannot be created.
26    ///
27    /// Offline executions however can be used to verify that program outputs follow from program
28    /// inputs and that the program was executed correctly. If this is the aim and no chain
29    /// interaction is desired, this function can be used.
30    #[allow(clippy::too_many_arguments)]
31    pub fn execute_program_offline<A: Aleo<Network = N>>(
32        &self,
33        private_key: &PrivateKey<N>,
34        program: &Program<N>,
35        function: impl TryInto<Identifier<N>>,
36        imports: &[Program<N>],
37        inputs: impl ExactSizeIterator<Item = impl TryInto<Value<N>>>,
38        include_outputs: bool,
39        url: &str,
40    ) -> Result<OfflineExecution<N>> {
41        // Initialize an RNG and query object for the transaction
42        let rng = &mut rand::thread_rng();
43        let query = Query::<N, BlockMemory<N>>::from(url);
44
45        // Check that the function exists in the program
46        let function_name = function.try_into().map_err(|_| anyhow!("Invalid function name"))?;
47        let program_id = program.id();
48        println!("Checking function {function_name:?} exists in {program_id:?}");
49        ensure!(
50            program.contains_function(&function_name),
51            "Program {program_id:?} does not contain function {function_name:?}, aborting execution"
52        );
53
54        // Create an ephemeral SnarkVM to store the programs
55        let store = ConsensusStore::<N, ConsensusMemory<N>>::open(None)?;
56        let vm = VM::<N, ConsensusMemory<N>>::from(store)?;
57        let credits_id = ProgramID::<N>::from_str("credits.aleo")?;
58        imports.iter().try_for_each(|program| {
59            if &credits_id != program.id() {
60                vm.process().write().add_program(program)?
61            }
62            Ok::<(), Error>(())
63        })?;
64        let _ = vm.process().write().add_program(program);
65
66        // Compute the authorization.
67        let authorization = vm.authorize(private_key, program_id, function_name, inputs, rng)?;
68
69        // Compute the trace
70        let locator = Locator::new(*program_id, function_name);
71        let (response, mut trace) = vm.process().write().execute::<A, _>(authorization, rng)?;
72        trace.prepare(query)?;
73        let execution = trace.prove_execution::<A, _>(&locator.to_string(), &mut rand::thread_rng())?;
74
75        // Get the public outputs
76        let mut public_outputs = vec![];
77        response.outputs().iter().zip(response.output_ids().iter()).for_each(|(output, output_id)| {
78            if let OutputID::Public(_) = output_id {
79                public_outputs.push(output.clone());
80            }
81        });
82
83        // If all outputs are requested, include them
84        let response = if include_outputs { Some(response) } else { None };
85
86        // Return the execution
87        Ok(OfflineExecution::new(execution, response, trace, Some(public_outputs)))
88    }
89
90    /// Execute a program function on the Aleo Network.
91    ///
92    /// To run this function successfully, the program must already be deployed on the Aleo Network
93    pub fn execute_program(
94        &mut self,
95        program_id: impl TryInto<ProgramID<N>>,
96        function: impl TryInto<Identifier<N>>,
97        inputs: impl ExactSizeIterator<Item = impl TryInto<Value<N>>>,
98        priority_fee: u64,
99        fee_record: Option<Record<N, Plaintext<N>>>,
100        password: Option<&str>,
101        private_key: Option<&PrivateKey<N>>,
102    ) -> Result<String> {
103        // Ensure a network client is set, otherwise online execution is not possible
104        ensure!(
105            self.api_client.is_some(),
106            "❌ Network client not set. A network client must be set before execution in order to send an execution transaction to the Aleo network"
107        );
108
109        // Check program and function have valid names
110        let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
111        let function_id = function.try_into().map_err(|_| anyhow!("Invalid function name"))?;
112        let function_name = function_id.to_string();
113        let api_client = self.api_client()?;
114
115        // Get the program from chain, error if it doesn't exist
116        let program = self
117            .get_program(program_id)
118            .or_else(|_| api_client.get_program(program_id))?;
119
120
121        // Create the execution transaction
122        let private_key = private_key.map_or_else(
123            || { self.get_private_key(password) },
124            |private_key| Ok(*private_key),
125        )?;
126        let node_url = self.api_client.as_ref().unwrap().base_url().to_string();
127        let transaction = Self::create_execute_transaction(
128            &private_key,
129            priority_fee,
130            inputs,
131            fee_record,
132            &program,
133            function_id,
134            node_url,
135            self.api_client()?,
136            &self.vm,
137        )?;
138
139        // Broadcast the execution transaction to the network
140        println!("Attempting to broadcast execution transaction for {program_id:?}");
141        let execution = self.broadcast_transaction(transaction);
142
143        // Tell the user about the result of the execution before returning it
144        if execution.is_ok() {
145            println!("✅ Execution of function {function_name:?} from program {program_id:?}' broadcast successfully");
146        } else {
147            println!("❌ Execution of function {function_name:?} from program {program_id:?} failed to broadcast");
148        }
149
150        execution
151    }
152
153    /// Create an execute transaction without initializing a program manager instance
154    #[allow(clippy::too_many_arguments)]
155    pub fn create_execute_transaction(
156        private_key: &PrivateKey<N>,
157        priority_fee: u64,
158        inputs: impl ExactSizeIterator<Item = impl TryInto<Value<N>>>,
159        fee_record: Option<Record<N, Plaintext<N>>>,
160        program: &Program<N>,
161        function: impl TryInto<Identifier<N>>,
162        node_url: String,
163        api_client: &AleoAPIClient<N>,
164        vm: &Option<VM<N, ConsensusMemory<N>>>,
165    ) -> Result<Transaction<N>> {
166        // Initialize an RNG and query object for the transaction
167        let rng = &mut rand::thread_rng();
168        let query = Query::from(node_url);
169
170        // Check that the function exists in the program
171        let function_name = function.try_into().map_err(|_| anyhow!("Invalid function name"))?;
172        let program_id = program.id();
173        println!("Checking function {function_name:?} exists in {program_id:?}");
174        ensure!(
175            program.contains_function(&function_name),
176            "Program {program_id:?} does not contain function {function_name:?}, aborting execution"
177        );
178
179        // Initialize the VM
180        if let Some(vm) = vm {
181            let credits_id = ProgramID::<N>::from_str("credits.aleo")?;
182            api_client.get_program_imports_from_source(program)?.iter().try_for_each(|(_, import)| {
183                if import.id() != &credits_id && !vm.process().read().contains_program(import.id()) {
184                    vm.process().write().add_program(import)?
185                }
186                Ok::<_, Error>(())
187            })?;
188
189            // If the initialization is for an execution, add the program. Otherwise, don't add it as
190            // it will be added during the deployment process
191            if !vm.process().read().contains_program(program.id()) {
192                vm.process().write().add_program(program)?;
193            }
194            vm.execute(private_key, (program_id, function_name), inputs, fee_record, priority_fee, Some(query), rng)
195        } else {
196            let vm = Self::initialize_vm(api_client, program, true)?;
197            vm.execute(private_key, (program_id, function_name), inputs, fee_record, priority_fee, Some(query), rng)
198        }
199    }
200
201    /// Estimate the cost of executing a program with the given inputs in microcredits. The response
202    /// will be in the form of (total_cost, (storage_cost, finalize_cost))
203    ///
204    /// Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network
205    pub fn estimate_execution_fee<A: Aleo<Network = N>>(
206        &self,
207        program: &Program<N>,
208        function: impl TryInto<Identifier<N>>,
209        inputs: impl ExactSizeIterator<Item = impl TryInto<Value<N>>>,
210    ) -> Result<(u64, (u64, u64))> {
211        let url = self.api_client.as_ref().map_or_else(
212            || bail!("A network client must be configured to estimate a program execution fee"),
213            |api_client| Ok(api_client.base_url()),
214        )?;
215
216        // Check that the function exists in the program
217        let function_name = function.try_into().map_err(|_| anyhow!("Invalid function name"))?;
218        let program_id = program.id();
219        println!("Checking function {function_name:?} exists in {program_id:?}");
220        ensure!(
221            program.contains_function(&function_name),
222            "Program {program_id:?} does not contain function {function_name:?}, aborting execution"
223        );
224
225        // Create an ephemeral SnarkVM to store the programs
226        // Initialize an RNG and query object for the transaction
227        let rng = &mut rand::thread_rng();
228        let query = Query::<N, BlockMemory<N>>::from(url);
229        let vm = Self::initialize_vm(self.api_client()?, program, true)?;
230
231        // Create an ephemeral private key for the sample execution
232        let private_key = PrivateKey::<N>::new(rng)?;
233
234        // Compute the authorization.
235        let authorization = vm.authorize(&private_key, program_id, function_name, inputs, rng)?;
236
237        let locator = Locator::new(*program_id, function_name);
238        let (_, mut trace) = vm.process().write().execute::<A, _>(authorization, rng)?;
239        trace.prepare(query)?;
240        let execution = trace.prove_execution::<A, _>(&locator.to_string(), &mut rand::thread_rng())?;
241        execution_cost(&vm, &execution)
242    }
243
244    /// Estimate the finalize fee component for executing a function. This fee is additional to the
245    /// size of the execution of the program in bytes. If the function does not have a finalize
246    /// step, then the finalize fee is 0.
247    ///
248    /// Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network
249    pub fn estimate_finalize_fee(&self, program: &Program<N>, function: impl TryInto<Identifier<N>>) -> Result<u64> {
250        let function_name = function.try_into().map_err(|_| anyhow!("Invalid function name"))?;
251        match program.get_function(&function_name)?.finalize_logic() {
252            Some(finalize) => cost_in_microcredits(finalize),
253            None => Ok(0u64),
254        }
255    }
256}
257
258#[cfg(test)]
259#[cfg(not(feature = "wasm"))]
260mod tests {
261    use super::*;
262    use crate::{random_program, random_program_id, AleoAPIClient, RECORD_5_MICROCREDITS};
263    use snarkvm::circuit::AleoV0;
264    use snarkvm_console::network::Testnet3;
265
266    #[test]
267    fn test_fee_estimation() {
268        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
269        let api_client = AleoAPIClient::<Testnet3>::testnet3();
270        let program_manager =
271            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client.clone()), None, false).unwrap();
272
273        let finalize_program = program_manager.api_client.as_ref().unwrap().get_program("credits.aleo").unwrap();
274        let hello_hello = program_manager.api_client.as_ref().unwrap().get_program("hello_hello.aleo").unwrap();
275        // Ensure a finalize scope program execution fee is estimated correctly
276        let (total, (storage, finalize)) = program_manager
277            .estimate_execution_fee::<AleoV0>(
278                &finalize_program,
279                "transfer_public",
280                vec!["aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px", "5u64"].into_iter(),
281            )
282            .unwrap();
283        let finalize_only = program_manager.estimate_finalize_fee(&finalize_program, "transfer_public").unwrap();
284        assert!(finalize_only > 0);
285        assert!(finalize > storage);
286        assert_eq!(finalize, finalize_only);
287        assert_eq!(total, finalize_only + storage);
288        assert_eq!(storage, total - finalize_only);
289
290        // Ensure a non-finalize scope program execution fee is estimated correctly
291        let (total, (storage, finalize)) = program_manager
292            .estimate_execution_fee::<AleoV0>(&hello_hello, "hello", vec!["5u32", "5u32"].into_iter())
293            .unwrap();
294        let finalize_only = program_manager.estimate_finalize_fee(&hello_hello, "hello").unwrap();
295        assert!(storage > 0);
296        assert_eq!(finalize_only, 0);
297        assert_eq!(finalize, finalize_only);
298        assert_eq!(total, finalize_only + storage);
299        assert_eq!(storage, total - finalize_only);
300
301        // Ensure a deployment fee is estimated correctly
302        let random = random_program();
303        let (total, (storage, namespace)) = program_manager.estimate_deployment_fee::<AleoV0>(&random).unwrap();
304        let namespace_only = ProgramManager::estimate_namespace_fee(random.id()).unwrap();
305        assert_eq!(namespace, 1000000);
306        assert_eq!(namespace, namespace_only);
307        assert_eq!(total, namespace_only + storage);
308        assert_eq!(storage, total - namespace_only);
309
310        // Ensure a program with imports is estimated correctly
311        let nested_import_program = api_client.get_program("imported_add_mul.aleo").unwrap();
312        let finalize_only = program_manager.estimate_finalize_fee(&nested_import_program, "add_and_double").unwrap();
313        let (total, (storage, finalize)) = program_manager
314            .estimate_execution_fee::<AleoV0>(
315                &nested_import_program,
316                "add_and_double",
317                vec!["5u32", "5u32"].into_iter(),
318            )
319            .unwrap();
320        assert!(storage > 0);
321        assert_eq!(finalize_only, 0);
322        assert_eq!(finalize, finalize_only);
323        assert_eq!(total, finalize_only + storage);
324        assert_eq!(storage, total - finalize_only);
325
326        let (total, (storage, namespace)) =
327            program_manager.estimate_deployment_fee::<AleoV0>(&nested_import_program).unwrap();
328        let namespace_only = ProgramManager::estimate_namespace_fee(nested_import_program.id()).unwrap();
329        assert_eq!(namespace, 1000000);
330        assert_eq!(namespace, namespace_only);
331        assert_eq!(total, namespace_only + storage);
332        assert_eq!(storage, total - namespace_only);
333    }
334
335    #[test]
336    #[ignore]
337    fn test_execution() {
338        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
339        let encrypted_private_key =
340            crate::Encryptor::encrypt_private_key_with_secret(&private_key, "password").unwrap();
341        let api_client = AleoAPIClient::<Testnet3>::local_testnet3("3033");
342        let record_finder = RecordFinder::new(api_client.clone());
343        let mut program_manager =
344            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client.clone()), None, false).unwrap();
345
346        let fee = 2_500_000;
347        let finalize_fee = 8_000_000;
348
349        // Test execution of an on chain program is successful
350        for i in 0..5 {
351            let fee_record = record_finder.find_one_record(&private_key, fee, None).unwrap();
352            // Test execution of a on chain program is successful
353            let execution = program_manager.execute_program(
354                "credits_import_test.aleo",
355                "test",
356                ["1312u32", "62131112u32"].into_iter(),
357                fee,
358                Some(fee_record),
359                None,
360                None,
361            );
362            println!("{:?}", execution);
363
364            if execution.is_ok() {
365                break;
366            } else if i == 4 {
367                panic!("{}", format!("Execution failed after 5 attempts with error: {:?}", execution));
368            }
369        }
370
371        // Test programs can be executed with an encrypted private key
372        let mut program_manager =
373            ProgramManager::<Testnet3>::new(None, Some(encrypted_private_key), Some(api_client), None, false).unwrap();
374
375        for i in 0..5 {
376            let fee_record = record_finder.find_one_record(&private_key, fee, None).unwrap();
377            // Test execution of an on chain program is successful using an encrypted private key
378            let execution = program_manager.execute_program(
379                "credits_import_test.aleo",
380                "test",
381                ["1337u32", "42u32"].into_iter(),
382                fee,
383                Some(fee_record),
384                Some("password"),
385                None,
386            );
387            if execution.is_ok() {
388                break;
389            } else if i == 4 {
390                panic!("{}", format!("Execution failed after 5 attempts with error: {:?}", execution));
391            }
392        }
393
394        // Test execution with a finalize scope can be done
395        for i in 0..5 {
396            let fee_record = record_finder.find_one_record(&private_key, finalize_fee, None).unwrap();
397            // Test execution of an on chain program is successful using an encrypted private key
398            let execution = program_manager.execute_program(
399                "finalize_test.aleo",
400                "increase_counter",
401                ["0u32", "42u32"].into_iter(),
402                finalize_fee,
403                Some(fee_record),
404                Some("password"),
405                None,
406            );
407            if execution.is_ok() {
408                break;
409            } else if i == 4 {
410                panic!("{}", format!("Execution failed after 5 attempts with error: {:?}", execution));
411            }
412        }
413
414        // Test execution of a program with imports other than credits.aleo is successful
415        for i in 0..5 {
416            let fee_record = record_finder.find_one_record(&private_key, finalize_fee, None).unwrap();
417            // Test execution of an on chain program is successful using an encrypted private key
418            let execution = program_manager.execute_program(
419                "double_test.aleo",
420                "double_it",
421                ["42u32"].into_iter(),
422                finalize_fee,
423                Some(fee_record),
424                Some("password"),
425                None,
426            );
427            if execution.is_ok() {
428                break;
429            } else if i == 4 {
430                panic!("{}", format!("Execution failed after 5 attempts with error: {:?}", execution));
431            }
432        }
433    }
434
435    #[test]
436    fn test_execution_failure_modes() {
437        let rng = &mut rand::thread_rng();
438        let recipient_private_key = PrivateKey::<Testnet3>::new(rng).unwrap();
439        let api_client = AleoAPIClient::<Testnet3>::testnet3();
440        let record_5_microcredits = Record::<Testnet3, Plaintext<Testnet3>>::from_str(RECORD_5_MICROCREDITS).unwrap();
441        let record_2000000001_microcredits =
442            Record::<Testnet3, Plaintext<Testnet3>>::from_str(RECORD_2000000001_MICROCREDITS).unwrap();
443
444        // Ensure that program manager creation fails if no key is provided
445        let mut program_manager =
446            ProgramManager::<Testnet3>::new(Some(recipient_private_key), None, Some(api_client), None, false).unwrap();
447
448        // Assert that execution fails if record's available microcredits are below the fee
449        let execution = program_manager.execute_program(
450            "hello.aleo",
451            "hello",
452            ["5u32", "6u32"].into_iter(),
453            500000,
454            Some(record_5_microcredits),
455            None,
456            None,
457        );
458
459        assert!(execution.is_err());
460
461        // Assert that execution fails if a fee is specified but no records are
462        let execution = program_manager.execute_program(
463            "hello.aleo",
464            "hello",
465            ["5u32", "6u32"].into_iter(),
466            200,
467            Some(record_2000000001_microcredits.clone()),
468            None,
469            None,
470        );
471
472        assert!(execution.is_err());
473
474        // Assert that execution fails if the program is not found
475        let randomized_program_id = random_program_id(16);
476        let execution = program_manager.execute_program(
477            &randomized_program_id,
478            "hello",
479            ["5u32", "6u32"].into_iter(),
480            500000,
481            Some(record_2000000001_microcredits.clone()),
482            None,
483            None,
484        );
485
486        assert!(execution.is_err());
487
488        // Assert that execution fails if the function is not found
489        let execution = program_manager.execute_program(
490            "hello.aleo",
491            "random_function",
492            ["5u32", "6u32"].into_iter(),
493            500000,
494            Some(record_2000000001_microcredits.clone()),
495            None,
496            None,
497        );
498
499        assert!(execution.is_err());
500
501        // Assert that execution fails if the function is not found
502        let execution = program_manager.execute_program(
503            "hello.aleo",
504            "random_function",
505            ["5u32", "6u32"].into_iter(),
506            500000,
507            Some(record_2000000001_microcredits),
508            None,
509            None,
510        );
511
512        assert!(execution.is_err());
513    }
514}