/ compiler / compiler / src / run.rs
run.rs
  1  // Copyright (C) 2019-2025 ADnet Contributors
  2  // This file is part of the ADL library.
  3  
  4  // The ADL 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 ADL 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 ADL library. If not, see <https://www.gnu.org/licenses/>.
 16  
 17  //! Utilities for running ADL 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  
 34  use adl_ast::{TEST_PRIVATE_KEY, interpreter_value::Value};
 35  use adl_errors::Result;
 36  
 37  use alpha_std_storage::StorageMode;
 38  use anyhow::anyhow;
 39  use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng as _};
 40  use rayon::prelude::*;
 41  use serde_json;
 42  use 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  };
 61  use std::{
 62      fmt,
 63      panic::{AssertUnwindSafe, catch_unwind},
 64      str::FromStr as _,
 65  };
 66  
 67  type CurrentNetwork = TestnetV0;
 68  
 69  /// Programs and configuration to run.
 70  #[derive(Debug)]
 71  pub 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)]
 80  pub struct Program {
 81      pub bytecode: String,
 82      pub name: String,
 83  }
 84  
 85  /// A particular case to run.
 86  #[derive(Clone, Debug, Default)]
 87  pub 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)]
 96  pub enum ExecutionStatus {
 97      None,
 98      Aborted,
 99      Accepted,
100      Rejected,
101      Halted(String),
102  }
103  
104  impl 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)]
117  pub enum EvaluationStatus {
118      Success,
119      Failed(String),
120  }
121  
122  impl 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)]
133  pub struct Outcome {
134      pub program_name: String,
135      pub function: String,
136      pub output: Value,
137  }
138  
139  impl 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)]
147  pub struct EvaluationOutcome {
148      pub outcome: Outcome,
149      pub status: EvaluationStatus,
150  }
151  
152  impl 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)]
160  pub struct ExecutionOutcome {
161      pub outcome: Outcome,
162      pub verified: bool,
163      pub execution: String,
164      pub status: ExecutionStatus,
165  }
166  
167  impl 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.
178  pub 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(adl_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`.
274  pub 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 alpha_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, &alpha_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 !alpha_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.alpha", "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  }