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 }