leo_compiler/
run.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo 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 Leo 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 Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17//! Utilities for running Leo programs in test environments.
18//!
19//! Currently this is used by:
20//! - the test runner in `test_execution.rs`,
21//! - the interpreter tests in `interpreter/src/test_interpreter.rs`, and
22//! - the `leo test` command in `cli/commands/test.rs`.
23//!
24//! `leo-compiler` is not necessarily the perfect place for it, but
25//! it's the easiest place for now to make it accessible to all of these.
26//!
27//! Provides functions for:
28//! - Running programs without a ledger (`run_without_ledger`). To be used for evaluating non-async code.
29//! - Running programs with a full ledger (`run_with_ledger`), including setup of VM, blocks, and execution tracking.
30//!   To be used for executing async code.
31//!
32//! Also defines types for program configuration, test cases, and outcomes.
33
34use leo_ast::{TEST_PRIVATE_KEY, interpreter_value::Value};
35use leo_errors::Result;
36
37use aleo_std_storage::StorageMode;
38use anyhow::anyhow;
39use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng as _};
40use rayon::prelude::*;
41use serde_json;
42use snarkvm::{
43    circuit::AleoTestnetV0,
44    prelude::{
45        Address,
46        ConsensusVersion,
47        Execution,
48        Identifier,
49        Ledger,
50        Network,
51        PrivateKey,
52        ProgramID,
53        TestnetV0,
54        Transaction,
55        VM,
56        Value as SvmValue,
57        store::{ConsensusStore, helpers::memory::ConsensusMemory},
58    },
59    synthesizer::program::ProgramCore,
60};
61use std::{
62    fmt,
63    panic::{AssertUnwindSafe, catch_unwind},
64    str::FromStr as _,
65};
66
67type CurrentNetwork = TestnetV0;
68
69/// Programs and configuration to run.
70#[derive(Debug)]
71pub struct Config {
72    pub seed: u64,
73    // If `None`, start at the height for the latest consensus version.
74    pub start_height: Option<u32>,
75    pub programs: Vec<Program>,
76}
77
78/// A program to deploy to the ledger.
79#[derive(Clone, Debug, Default)]
80pub struct Program {
81    pub bytecode: String,
82    pub name: String,
83}
84
85/// A particular case to run.
86#[derive(Clone, Debug, Default)]
87pub struct Case {
88    pub program_name: String,
89    pub function: String,
90    pub private_key: Option<String>,
91    pub input: Vec<String>,
92}
93
94/// The status of a case that was run.
95#[derive(Clone, Debug, PartialEq, Eq)]
96pub enum ExecutionStatus {
97    None,
98    Aborted,
99    Accepted,
100    Rejected,
101    Halted(String),
102}
103
104impl fmt::Display for ExecutionStatus {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        match self {
107            Self::Halted(s) => write!(f, "halted ({s})"),
108            Self::None => write!(f, "none"),
109            Self::Aborted => write!(f, "aborted"),
110            Self::Accepted => write!(f, "accepted"),
111            Self::Rejected => write!(f, "rejected"),
112        }
113    }
114}
115
116#[derive(Debug, Clone)]
117pub enum EvaluationStatus {
118    Success,
119    Failed(String),
120}
121
122impl fmt::Display for EvaluationStatus {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::Success => write!(f, "success"),
126            Self::Failed(e) => write!(f, "failed: {e}"),
127        }
128    }
129}
130
131/// Shared fields for all outcome types.
132#[derive(Debug, Clone)]
133pub struct Outcome {
134    pub program_name: String,
135    pub function: String,
136    pub output: Value,
137}
138
139impl Outcome {
140    pub fn output(&self) -> Value {
141        self.output.clone()
142    }
143}
144
145/// Outcome of an evaluation-only run (no execution trace, no verification).
146#[derive(Debug, Clone)]
147pub struct EvaluationOutcome {
148    pub outcome: Outcome,
149    pub status: EvaluationStatus,
150}
151
152impl EvaluationOutcome {
153    pub fn output(&self) -> Value {
154        self.outcome.output()
155    }
156}
157
158/// Outcome that includes execution and verification details.
159#[derive(Debug, Clone)]
160pub struct ExecutionOutcome {
161    pub outcome: Outcome,
162    pub verified: bool,
163    pub execution: String,
164    pub status: ExecutionStatus,
165}
166
167impl ExecutionOutcome {
168    pub fn output(&self) -> Value {
169        self.outcome.output()
170    }
171}
172
173/// Evaluates a set of cases against some programs without using a ledger.
174///
175/// Each case is run in isolation, producing an `EvaluationOutcome` for its
176/// output and success/failure status. Panics and errors in authorization or
177/// evaluation are caught and reported as failures.
178pub fn run_without_ledger(config: &Config, cases: &[Case]) -> Result<Vec<EvaluationOutcome>> {
179    // Nothing to do
180    if cases.is_empty() {
181        return Ok(Vec::new());
182    }
183
184    let programs_and_editions: Vec<(snarkvm::prelude::Program<CurrentNetwork>, u16)> = config
185        .programs
186        .iter()
187        .map(|Program { bytecode, name }| {
188            let program = snarkvm::prelude::Program::<CurrentNetwork>::from_str(bytecode)
189                .map_err(|e| anyhow!("Failed to parse bytecode of program {name}: {e}"))?;
190            // Assume edition 1. We can consider parametrizing this in the future.
191            let edition: u16 = 1;
192            Ok((program, edition))
193        })
194        .collect::<Result<Vec<_>>>()?;
195
196    let outcomes: Vec<EvaluationOutcome> = cases
197        .par_iter()
198        .map(|case| {
199            let rng = &mut ChaCha20Rng::seed_from_u64(config.seed);
200
201            // Helper to produce an EvaluationOutcome with `Failed` status
202            let failed_outcome = |e: String| EvaluationOutcome {
203                outcome: Outcome {
204                    program_name: case.program_name.clone(),
205                    function: case.function.clone(),
206                    output: Value::make_unit(),
207                },
208                status: EvaluationStatus::Failed(e),
209            };
210
211            let vm = match ConsensusStore::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::open(
212                StorageMode::Production,
213            ) {
214                Ok(store) => match VM::from(store) {
215                    Ok(vm) => vm,
216                    Err(e) => return failed_outcome(format!("VM init error: {e}")),
217                },
218                Err(e) => return failed_outcome(format!("Consensus store open error: {e}")),
219            };
220
221            if let Err(e) = vm.process().write().add_programs_with_editions(&programs_and_editions) {
222                return failed_outcome(format!("Failed to add programs: {e}"));
223            }
224
225            let private_key = match PrivateKey::from_str(leo_ast::TEST_PRIVATE_KEY) {
226                Ok(pk) => pk,
227                Err(e) => return failed_outcome(format!("Private key parse error: {e}")),
228            };
229            let program_id = match ProgramID::<CurrentNetwork>::from_str(&case.program_name) {
230                Ok(pid) => pid,
231                Err(e) => return failed_outcome(format!("ProgramID parse error: {e}")),
232            };
233            let function_id = match Identifier::<CurrentNetwork>::from_str(&case.function) {
234                Ok(fid) => fid,
235                Err(e) => return failed_outcome(format!("FunctionID parse error: {e}")),
236            };
237            let inputs = case.input.iter();
238
239            // --- catch panics from authorize ---
240            let authorization = match catch_unwind(AssertUnwindSafe(|| {
241                vm.authorize(&private_key, program_id, function_id, inputs, rng)
242            })) {
243                Ok(Ok(auth)) => auth,
244                Ok(Err(e)) => return failed_outcome(format!("{e}")),
245                Err(e) => return failed_outcome(format!("{e:?}")),
246            };
247
248            // --- catch panics from evaluate ---
249            let response =
250                match catch_unwind(AssertUnwindSafe(|| vm.process().read().evaluate::<AleoTestnetV0>(authorization))) {
251                    Ok(Ok(resp)) => resp,
252                    Ok(Err(e)) => return failed_outcome(format!("{e}")),
253                    Err(e) => return failed_outcome(format!("{e:?}")),
254                };
255
256            let outputs = response.outputs();
257            let output = match outputs.len() {
258                0 => Value::make_unit(),
259                1 => outputs[0].clone().into(),
260                _ => Value::make_tuple(outputs.iter().map(|x| x.clone().into())),
261            };
262
263            EvaluationOutcome {
264                outcome: Outcome { program_name: case.program_name.clone(), function: case.function.clone(), output },
265                status: EvaluationStatus::Success,
266            }
267        })
268        .collect();
269
270    Ok(outcomes)
271}
272
273/// Run the functions indicated by `cases` from the programs in `config`.
274pub fn run_with_ledger(config: &Config, case_sets: &[Vec<Case>]) -> Result<Vec<Vec<ExecutionOutcome>>> {
275    if case_sets.is_empty() {
276        return Ok(Vec::new());
277    }
278
279    // Initialize an rng.
280    let mut rng = ChaCha20Rng::seed_from_u64(config.seed);
281
282    // Initialize a genesis private key.
283    let genesis_private_key = PrivateKey::from_str(TEST_PRIVATE_KEY).unwrap();
284
285    // Store all of the non-genesis blocks created during set up.
286    let mut blocks = Vec::new();
287
288    // Initialize a `VM` and construct the genesis block. This should always succeed.
289    let genesis_block = VM::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::from(ConsensusStore::open(0).unwrap())
290        .unwrap()
291        .genesis_beacon(&genesis_private_key, &mut rng)
292        .unwrap();
293
294    // Initialize a `Ledger`. This should always succeed.
295    let ledger =
296        Ledger::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::load(genesis_block.clone(), StorageMode::Production)
297            .unwrap();
298
299    // Advance the `VM` to the start height, defaulting to the height for the latest consensus version.
300    let latest_consensus_version = ConsensusVersion::latest();
301    let start_height =
302        config.start_height.unwrap_or(CurrentNetwork::CONSENSUS_HEIGHT(latest_consensus_version).unwrap());
303    while ledger.latest_height() < start_height {
304        let block = ledger
305            .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], vec![], &mut rng)
306            .map_err(|_| anyhow!("Failed to prepare advance to next beacon block"))?;
307        ledger.advance_to_next_block(&block).map_err(|_| anyhow!("Failed to advance to next block"))?;
308        blocks.push(block);
309    }
310
311    // Deploy each bytecode separately.
312    for Program { bytecode, name } in &config.programs {
313        // Parse the bytecode as an Aleo program.
314        // Note that this function checks that the bytecode is well-formed.
315        let aleo_program =
316            ProgramCore::from_str(bytecode).map_err(|e| anyhow!("Failed to parse bytecode of program {name}: {e}"))?;
317
318        let mut deploy = || -> Result<()> {
319            // Add the program to the ledger.
320            // Note that this function performs an additional validity check on the bytecode.
321            let deployment = ledger
322                .vm()
323                .deploy(&genesis_private_key, &aleo_program, None, 0, None, &mut rng)
324                .map_err(|e| anyhow!("Failed to deploy program {name}: {e}"))?;
325            let block = ledger
326                .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], vec![deployment], &mut rng)
327                .map_err(|e| anyhow!("Failed to prepare to advance block for program {name}: {e}"))?;
328            ledger
329                .advance_to_next_block(&block)
330                .map_err(|e| anyhow!("Failed to advance block for program {name}: {e}"))?;
331
332            // Check that the deployment transaction was accepted.
333            if block.transactions().num_accepted() != 1 {
334                return Err(anyhow!("Deployment transaction for program {name} not accepted.").into());
335            }
336
337            // Store the block.
338            blocks.push(block);
339
340            Ok(())
341        };
342
343        // Deploy the program.
344        deploy()?;
345        // If the program does not have a constructor, deploy it twice to satisfy the edition requirement.
346        if !aleo_program.contains_constructor() {
347            deploy()?;
348        }
349    }
350
351    // Initialize ledger instances for each case set.
352    let mut indexed_ledgers = vec![(0, ledger)];
353    indexed_ledgers.extend(
354        (1..case_sets.len())
355            .into_par_iter()
356            .map(|i| {
357                // Initialize a `Ledger`. This should always succeed.
358                let l = Ledger::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::load(
359                    genesis_block.clone(),
360                    StorageMode::Production,
361                )
362                .expect("Failed to load copy of ledger");
363                // Add the setup blocks.
364                for block in blocks.iter() {
365                    l.advance_to_next_block(block).expect("Failed to add setup block to ledger");
366                }
367
368                (i, l)
369            })
370            .collect::<Vec<_>>(),
371    );
372
373    // For each of the case sets, run the cases sequentially.
374    let results = indexed_ledgers
375        .into_par_iter()
376        .map(|(index, ledger)| {
377            // Get the cases for this ledger.
378            let cases = &case_sets[index];
379            // Clone the RNG.
380            let mut rng = rng.clone();
381
382            // Fund each private key used in the test cases with 1M ALEO.
383            let transactions: Vec<Transaction<CurrentNetwork>> = cases
384                .iter()
385                .filter_map(|case| case.private_key.as_ref())
386                .map(|key| {
387                    // Parse the private key.
388                    let private_key =
389                        PrivateKey::<CurrentNetwork>::from_str(key).expect("Failed to parse private key.");
390                    // Convert the private key to an address.
391                    let address = Address::try_from(private_key).expect("Failed to convert private key to address.");
392                    // Generate the transaction.
393                    ledger
394                        .vm()
395                        .execute(
396                            &genesis_private_key,
397                            ("credits.aleo", "transfer_public"),
398                            [
399                                SvmValue::from_str(&format!("{address}")).expect("Failed to parse recipient address"),
400                                SvmValue::from_str("1_000_000_000_000u64").expect("Failed to parse amount"),
401                            ]
402                            .iter(),
403                            None,
404                            0u64,
405                            None,
406                            &mut rng,
407                        )
408                        .expect("Failed to generate funding transaction")
409                })
410                .collect();
411
412            // Create a block with the funding transactions.
413            let block = ledger
414                .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], transactions, &mut rng)
415                .expect("Failed to prepare advance to next beacon block");
416            // Assert that no transactions were aborted or rejected.
417            assert!(block.aborted_transaction_ids().is_empty());
418            assert_eq!(block.transactions().num_rejected(), 0);
419            // Advance the ledger to the next block.
420            ledger.advance_to_next_block(&block).expect("Failed to advance to next block");
421
422            let mut case_outcomes = Vec::new();
423
424            for case in cases {
425                assert!(
426                    ledger.vm().contains_program(&ProgramID::from_str(&case.program_name).unwrap()),
427                    "Program {} should exist.",
428                    case.program_name
429                );
430
431                let private_key = case
432                    .private_key
433                    .as_ref()
434                    .map(|key| PrivateKey::from_str(key).expect("Failed to parse private key."))
435                    .unwrap_or(genesis_private_key);
436
437                let mut execution = None;
438                let mut verified = false;
439                let mut status = ExecutionStatus::None;
440
441                // Halts are handled by panics, so we need to catch them.
442                // I'm not thrilled about this usage of `AssertUnwindSafe`, but it seems to be
443                // used frequently in SnarkVM anyway.
444                let execute_output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
445                    ledger.vm().execute_with_response(
446                        &private_key,
447                        (&case.program_name, &case.function),
448                        case.input.iter(),
449                        None,
450                        0,
451                        None,
452                        &mut rng,
453                    )
454                }));
455
456                if let Err(payload) = execute_output {
457                    let s1 = payload.downcast_ref::<&str>().map(|s| s.to_string());
458                    let s2 = payload.downcast_ref::<String>().cloned();
459                    let s = s1.or(s2).unwrap_or_else(|| "Unknown panic payload".to_string());
460
461                    case_outcomes.push(ExecutionOutcome {
462                        outcome: Outcome {
463                            program_name: case.program_name.clone(),
464                            function: case.function.clone(),
465                            output: Value::make_unit(),
466                        },
467                        status: ExecutionStatus::Halted(s),
468                        verified: false,
469                        execution: "".to_string(),
470                    });
471
472                    continue;
473                }
474
475                let result = execute_output.unwrap().and_then(|(transaction, response)| {
476                    verified = ledger.vm().check_transaction(&transaction, None, &mut rng).is_ok();
477                    execution = Some(transaction.clone());
478                    let block = ledger.prepare_advance_to_next_beacon_block(
479                        &private_key,
480                        vec![],
481                        vec![],
482                        vec![transaction],
483                        &mut rng,
484                    )?;
485                    status =
486                        match (block.aborted_transaction_ids().is_empty(), block.transactions().num_accepted() == 1) {
487                            (false, _) => ExecutionStatus::Aborted,
488                            (true, true) => ExecutionStatus::Accepted,
489                            (true, false) => ExecutionStatus::Rejected,
490                        };
491                    ledger.advance_to_next_block(&block)?;
492                    Ok(response)
493                });
494
495                let output = match result {
496                    Ok(response) => {
497                        let outputs = response.outputs();
498                        match outputs.len() {
499                            0 => Value::make_unit(),
500                            1 => outputs[0].clone().into(),
501                            _ => Value::make_tuple(outputs.iter().map(|x| x.clone().into())),
502                        }
503                    }
504                    Err(e) => Value::make_string(format!("Failed to extract output: {e}")),
505                };
506
507                // Extract the execution, removing the global state root and proof.
508                // This is necessary as they are not deterministic across runs, even with RNG fixed.
509                let execution = if let Some(Transaction::Execute(_, _, execution, _)) = execution {
510                    Some(Execution::from(execution.into_transitions(), Default::default(), None).unwrap())
511                } else {
512                    None
513                };
514
515                case_outcomes.push(ExecutionOutcome {
516                    outcome: Outcome {
517                        program_name: case.program_name.clone(),
518                        function: case.function.clone(),
519                        output,
520                    },
521                    status,
522                    verified,
523                    execution: serde_json::to_string_pretty(&execution).expect("Serialization failure"),
524                });
525            }
526
527            Ok((index, case_outcomes))
528        })
529        .collect::<Result<Vec<_>>>()?;
530
531    // Reorder results to match input order.
532    let mut ordered_results: Vec<Vec<ExecutionOutcome>> = vec![Default::default(); case_sets.len()];
533    for (index, outcomes) in results.into_iter() {
534        ordered_results[index] = outcomes;
535    }
536
537    Ok(ordered_results)
538}