leo_compiler/
run_with_ledger.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
17use leo_ast::{TEST_PRIVATE_KEY, interpreter_value::Value};
18use leo_errors::Result;
19
20use aleo_std_storage::StorageMode;
21use anyhow::anyhow;
22use snarkvm::{
23    prelude::{
24        Address,
25        Execution,
26        Ledger,
27        PrivateKey,
28        ProgramID,
29        TestnetV0,
30        Transaction,
31        VM,
32        Value as SvmValue,
33        store::{ConsensusStore, helpers::memory::ConsensusMemory},
34    },
35    synthesizer::program::ProgramCore,
36};
37
38use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng as _};
39use rayon::prelude::*;
40use serde_json;
41use snarkvm::prelude::{ConsensusVersion, Network};
42use std::{fmt, str::FromStr as _};
43
44type CurrentNetwork = TestnetV0;
45
46/// Programs and configuration to run.
47#[derive(Debug)]
48pub struct Config {
49    pub seed: u64,
50    // If `None`, start at the height for the latest consensus version.
51    pub start_height: Option<u32>,
52    pub programs: Vec<Program>,
53}
54
55/// A program to deploy to the ledger.
56#[derive(Clone, Debug, Default)]
57pub struct Program {
58    pub bytecode: String,
59    pub name: String,
60}
61
62/// A particular case to run.
63#[derive(Clone, Debug, Default)]
64pub struct Case {
65    pub program_name: String,
66    pub function: String,
67    pub private_key: Option<String>,
68    pub input: Vec<String>,
69}
70
71/// The status of a case that was run.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub enum Status {
74    None,
75    Aborted,
76    Accepted,
77    Rejected,
78    Halted(String),
79}
80
81impl fmt::Display for Status {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Status::Halted(s) => write!(f, "halted ({s})"),
85            Status::None => "none".fmt(f),
86            Status::Aborted => "aborted".fmt(f),
87            Status::Accepted => "accepted".fmt(f),
88            Status::Rejected => "rejected".fmt(f),
89        }
90    }
91}
92
93/// All details about the result of a case that was run.
94#[derive(Debug, Clone)]
95pub struct CaseOutcome {
96    pub program_name: String,
97    pub function: String,
98    pub status: Status,
99    pub verified: bool,
100    pub execution: String,
101    pub output: Value,
102}
103
104/// Run the functions indicated by `cases` from the programs in `config`.
105// Currently this is used both by the test runner in `test_execution.rs`
106// as well as the Leo test in `cli/commands/test.rs`.
107// `leo-compiler` is not necessarily the perfect place for it, but
108// it's the easiest place for now to make it accessible to both of those.
109pub fn run_with_ledger(config: &Config, case_sets: &[Vec<Case>]) -> Result<Vec<Vec<CaseOutcome>>> {
110    if case_sets.is_empty() {
111        return Ok(Vec::new());
112    }
113
114    // Initialize an rng.
115    let mut rng = ChaCha20Rng::seed_from_u64(config.seed);
116
117    // Initialize a genesis private key.
118    let genesis_private_key = PrivateKey::from_str(TEST_PRIVATE_KEY).unwrap();
119
120    // Store all of the non-genesis blocks created during set up.
121    let mut blocks = Vec::new();
122
123    // Initialize a `VM` and construct the genesis block. This should always succeed.
124    let genesis_block = VM::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::from(ConsensusStore::open(0).unwrap())
125        .unwrap()
126        .genesis_beacon(&genesis_private_key, &mut rng)
127        .unwrap();
128
129    // Initialize a `Ledger`. This should always succeed.
130    let ledger =
131        Ledger::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::load(genesis_block.clone(), StorageMode::Production)
132            .unwrap();
133
134    // Advance the `VM` to the start height, defaulting to the height for the latest consensus version.
135    let latest_consensus_version = ConsensusVersion::latest();
136    let start_height =
137        config.start_height.unwrap_or(CurrentNetwork::CONSENSUS_HEIGHT(latest_consensus_version).unwrap());
138    while ledger.latest_height() < start_height {
139        let block = ledger
140            .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], vec![], &mut rng)
141            .map_err(|_| anyhow!("Failed to prepare advance to next beacon block"))?;
142        ledger.advance_to_next_block(&block).map_err(|_| anyhow!("Failed to advance to next block"))?;
143        blocks.push(block);
144    }
145
146    // Deploy each bytecode separately.
147    for Program { bytecode, name } in &config.programs {
148        // Parse the bytecode as an Aleo program.
149        // Note that this function checks that the bytecode is well-formed.
150        let aleo_program =
151            ProgramCore::from_str(bytecode).map_err(|e| anyhow!("Failed to parse bytecode of program {name}: {e}"))?;
152
153        let mut deploy = || -> Result<()> {
154            // Add the program to the ledger.
155            // Note that this function performs an additional validity check on the bytecode.
156            let deployment = ledger
157                .vm()
158                .deploy(&genesis_private_key, &aleo_program, None, 0, None, &mut rng)
159                .map_err(|e| anyhow!("Failed to deploy program {name}: {e}"))?;
160            let block = ledger
161                .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], vec![deployment], &mut rng)
162                .map_err(|e| anyhow!("Failed to prepare to advance block for program {name}: {e}"))?;
163            ledger
164                .advance_to_next_block(&block)
165                .map_err(|e| anyhow!("Failed to advance block for program {name}: {e}"))?;
166
167            // Check that the deployment transaction was accepted.
168            if block.transactions().num_accepted() != 1 {
169                return Err(anyhow!("Deployment transaction for program {name} not accepted.").into());
170            }
171
172            // Store the block.
173            blocks.push(block);
174
175            Ok(())
176        };
177
178        // Deploy the program.
179        deploy()?;
180        // If the program does not have a constructor, deploy it twice to satisfy the edition requirement.
181        if !aleo_program.contains_constructor() {
182            deploy()?;
183        }
184    }
185
186    // Initialize ledger instances for each case set.
187    let mut indexed_ledgers = vec![(0, ledger)];
188    indexed_ledgers.extend(
189        (1..case_sets.len())
190            .into_par_iter()
191            .map(|i| {
192                // Initialize a `Ledger`. This should always succeed.
193                let l = Ledger::<CurrentNetwork, ConsensusMemory<CurrentNetwork>>::load(
194                    genesis_block.clone(),
195                    StorageMode::Production,
196                )
197                .expect("Failed to load copy of ledger");
198                // Add the setup blocks.
199                for block in blocks.iter() {
200                    l.advance_to_next_block(block).expect("Failed to add setup block to ledger");
201                }
202
203                (i, l)
204            })
205            .collect::<Vec<_>>(),
206    );
207
208    // For each of the case sets, run the cases sequentially.
209    let results = indexed_ledgers
210        .into_par_iter()
211        .map(|(index, ledger)| {
212            // Get the cases for this ledger.
213            let cases = &case_sets[index];
214            // Clone the RNG.
215            let mut rng = rng.clone();
216
217            // Fund each private key used in the test cases with 1M ALEO.
218            let transactions: Vec<Transaction<CurrentNetwork>> = cases
219                .iter()
220                .filter_map(|case| case.private_key.as_ref())
221                .map(|key| {
222                    // Parse the private key.
223                    let private_key =
224                        PrivateKey::<CurrentNetwork>::from_str(key).expect("Failed to parse private key.");
225                    // Convert the private key to an address.
226                    let address = Address::try_from(private_key).expect("Failed to convert private key to address.");
227                    // Generate the transaction.
228                    ledger
229                        .vm()
230                        .execute(
231                            &genesis_private_key,
232                            ("credits.aleo", "transfer_public"),
233                            [
234                                SvmValue::from_str(&format!("{address}")).expect("Failed to parse recipient address"),
235                                SvmValue::from_str("1_000_000_000_000u64").expect("Failed to parse amount"),
236                            ]
237                            .iter(),
238                            None,
239                            0u64,
240                            None,
241                            &mut rng,
242                        )
243                        .expect("Failed to generate funding transaction")
244                })
245                .collect();
246
247            // Create a block with the funding transactions.
248            let block = ledger
249                .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], transactions, &mut rng)
250                .expect("Failed to prepare advance to next beacon block");
251            // Assert that no transactions were aborted or rejected.
252            assert!(block.aborted_transaction_ids().is_empty());
253            assert_eq!(block.transactions().num_rejected(), 0);
254            // Advance the ledger to the next block.
255            ledger.advance_to_next_block(&block).expect("Failed to advance to next block");
256
257            let mut case_outcomes = Vec::new();
258
259            for case in cases {
260                assert!(
261                    ledger.vm().contains_program(&ProgramID::from_str(&case.program_name).unwrap()),
262                    "Program {} should exist.",
263                    case.program_name
264                );
265
266                let private_key = case
267                    .private_key
268                    .as_ref()
269                    .map(|key| PrivateKey::from_str(key).expect("Failed to parse private key."))
270                    .unwrap_or(genesis_private_key);
271
272                let mut execution = None;
273                let mut verified = false;
274                let mut status = Status::None;
275
276                // Halts are handled by panics, so we need to catch them.
277                // I'm not thrilled about this usage of `AssertUnwindSafe`, but it seems to be
278                // used frequently in SnarkVM anyway.
279                let execute_output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
280                    ledger.vm().execute_with_response(
281                        &private_key,
282                        (&case.program_name, &case.function),
283                        case.input.iter(),
284                        None,
285                        0,
286                        None,
287                        &mut rng,
288                    )
289                }));
290
291                if let Err(payload) = execute_output {
292                    let s1 = payload.downcast_ref::<&str>().map(|s| s.to_string());
293                    let s2 = payload.downcast_ref::<String>().cloned();
294                    let s = s1.or(s2).unwrap_or_else(|| "Unknown panic payload".to_string());
295
296                    case_outcomes.push(CaseOutcome {
297                        program_name: case.program_name.clone(),
298                        function: case.function.clone(),
299                        status: Status::Halted(s),
300                        verified: false,
301                        execution: "".to_string(),
302                        output: Value::make_unit(),
303                    });
304                    continue;
305                }
306
307                let result = execute_output.unwrap().and_then(|(transaction, response)| {
308                    verified = ledger.vm().check_transaction(&transaction, None, &mut rng).is_ok();
309                    execution = Some(transaction.clone());
310                    let block = ledger.prepare_advance_to_next_beacon_block(
311                        &private_key,
312                        vec![],
313                        vec![],
314                        vec![transaction],
315                        &mut rng,
316                    )?;
317                    status =
318                        match (block.aborted_transaction_ids().is_empty(), block.transactions().num_accepted() == 1) {
319                            (false, _) => Status::Aborted,
320                            (true, true) => Status::Accepted,
321                            (true, false) => Status::Rejected,
322                        };
323                    ledger.advance_to_next_block(&block)?;
324                    Ok(response)
325                });
326
327                let output = match result {
328                    Ok(response) => {
329                        let outputs = response.outputs();
330                        match outputs.len() {
331                            0 => Value::make_unit(),
332                            1 => outputs[0].clone().into(),
333                            _ => Value::make_tuple(outputs.iter().map(|x| x.clone().into())),
334                        }
335                    }
336                    Err(e) => Value::make_string(format!("Failed to extract output: {e}")),
337                };
338
339                // Extract the execution, removing the global state root and proof.
340                // This is necessary as they are not deterministic across runs, even with RNG fixed.
341                let execution = if let Some(Transaction::Execute(_, _, execution, _)) = execution {
342                    Some(Execution::from(execution.into_transitions(), Default::default(), None).unwrap())
343                } else {
344                    None
345                };
346
347                case_outcomes.push(CaseOutcome {
348                    program_name: case.program_name.clone(),
349                    function: case.function.clone(),
350                    status,
351                    verified,
352                    execution: serde_json::to_string_pretty(&execution).expect("Serialization failure"),
353                    output,
354                });
355            }
356
357            Ok((index, case_outcomes))
358        })
359        .collect::<Result<Vec<_>>>()?;
360
361    // Reorder results to match input order.
362    let mut ordered_results: Vec<Vec<CaseOutcome>> = vec![Default::default(); case_sets.len()];
363    for (index, outcomes) in results.into_iter() {
364        ordered_results[index] = outcomes;
365    }
366
367    Ok(ordered_results)
368}