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::interpreter_value::Value;
18use leo_errors::{BufferEmitter, ErrBuffer, Handler, LeoError, Result, WarningBuffer};
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 serde_json;
40use snarkvm::prelude::{ConsensusVersion, Network};
41use std::{fmt, str::FromStr as _};
42
43type CurrentNetwork = TestnetV0;
44
45/// Programs and configuration to run.
46#[derive(Debug)]
47pub struct Config {
48    pub seed: u64,
49    pub start_height: Option<u32>,
50    pub programs: Vec<Program>,
51}
52
53/// A program to deploy to the ledger.
54#[derive(Clone, Debug, Default)]
55pub struct Program {
56    pub bytecode: String,
57    pub name: String,
58}
59
60/// A particular case to run.
61#[derive(Clone, Debug, Default)]
62pub struct Case {
63    pub program_name: String,
64    pub function: String,
65    pub private_key: Option<String>,
66    pub input: Vec<String>,
67}
68
69/// The status of a case that was run.
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum Status {
72    None,
73    Aborted,
74    Accepted,
75    Rejected,
76    Halted(String),
77}
78
79impl fmt::Display for Status {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Status::Halted(s) => write!(f, "halted ({s})"),
83            Status::None => "none".fmt(f),
84            Status::Aborted => "aborted".fmt(f),
85            Status::Accepted => "accepted".fmt(f),
86            Status::Rejected => "rejected".fmt(f),
87        }
88    }
89}
90
91/// All details about the result of a case that was run.
92#[derive(Debug)]
93pub struct CaseOutcome {
94    pub status: Status,
95    pub verified: bool,
96    pub errors: ErrBuffer,
97    pub warnings: WarningBuffer,
98    pub execution: String,
99    pub output: Value,
100}
101
102/// Run the functions indicated by `cases` from the programs in `config`.
103// Currently this is used both by the test runner in `test_execution.rs`
104// as well as the Leo test in `cli/commands/test.rs`.
105// `leo-compiler` is not necessarily the perfect place for it, but
106// it's the easiest place for now to make it accessible to both of those.
107pub fn run_with_ledger(
108    config: &Config,
109    cases: &[Case],
110    handler: &Handler,
111    buf: &BufferEmitter,
112) -> Result<Vec<CaseOutcome>> {
113    if cases.is_empty() {
114        return Ok(Vec::new());
115    }
116
117    // Initialize an rng.
118    let mut rng = ChaCha20Rng::seed_from_u64(config.seed);
119
120    // Initialize a genesis private key.
121    let genesis_private_key = PrivateKey::new(&mut rng).unwrap();
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, 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    }
144
145    // Deploy each bytecode separately.
146    for Program { bytecode, name } in &config.programs {
147        // Parse the bytecode as an Aleo program.
148        // Note that this function checks that the bytecode is well-formed.
149        let aleo_program =
150            ProgramCore::from_str(bytecode).map_err(|e| anyhow!("Failed to parse bytecode of program {name}: {e}"))?;
151
152        let mut deploy = || -> Result<()> {
153            // Add the program to the ledger.
154            // Note that this function performs an additional validity check on the bytecode.
155            let deployment = ledger
156                .vm()
157                .deploy(&genesis_private_key, &aleo_program, None, 0, None, &mut rng)
158                .map_err(|e| anyhow!("Failed to deploy program {name}: {e}"))?;
159            let block = ledger
160                .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], vec![deployment], &mut rng)
161                .map_err(|e| anyhow!("Failed to prepare to advance block for program {name}: {e}"))?;
162            ledger
163                .advance_to_next_block(&block)
164                .map_err(|e| anyhow!("Failed to advance block for program {name}: {e}"))?;
165
166            // Check that the deployment transaction was accepted.
167            if block.transactions().num_accepted() != 1 {
168                return Err(anyhow!("Deployment transaction for program {name} not accepted.").into());
169            }
170            Ok(())
171        };
172
173        // Deploy the program.
174        deploy()?;
175        // If the program does not have a constructor, deploy it twice to satisfy the edition requirement.
176        if !aleo_program.contains_constructor() {
177            deploy()?;
178        }
179    }
180
181    // Fund each private key used in the test cases with 1M ALEO.
182    let transactions: Vec<Transaction<CurrentNetwork>> = cases
183        .iter()
184        .filter_map(|case| case.private_key.as_ref())
185        .map(|key| {
186            // Parse the private key.
187            let private_key = PrivateKey::<CurrentNetwork>::from_str(key).expect("Failed to parse private key.");
188            // Convert the private key to an address.
189            let address = Address::try_from(private_key).expect("Failed to convert private key to address.");
190            // Generate the transaction.
191            ledger
192                .vm()
193                .execute(
194                    &genesis_private_key,
195                    ("credits.aleo", "transfer_public"),
196                    [
197                        SvmValue::from_str(&format!("{address}")).expect("Failed to parse recipient address"),
198                        SvmValue::from_str("1_000_000_000_000u64").expect("Failed to parse amount"),
199                    ]
200                    .iter(),
201                    None,
202                    0u64,
203                    None,
204                    &mut rng,
205                )
206                .expect("Failed to generate funding transaction")
207        })
208        .collect();
209
210    // Create a block with the funding transactions.
211    let block = ledger
212        .prepare_advance_to_next_beacon_block(&genesis_private_key, vec![], vec![], transactions, &mut rng)
213        .expect("Failed to prepare advance to next beacon block");
214    // Assert that no transactions were aborted or rejected.
215    assert!(block.aborted_transaction_ids().is_empty());
216    assert_eq!(block.transactions().num_rejected(), 0);
217    // Advance the ledger to the next block.
218    ledger.advance_to_next_block(&block).expect("Failed to advance to next block");
219
220    let mut case_outcomes = Vec::new();
221
222    for case in cases {
223        assert!(
224            ledger.vm().contains_program(&ProgramID::from_str(&case.program_name).unwrap()),
225            "Program {} should exist.",
226            case.program_name
227        );
228
229        let private_key = case
230            .private_key
231            .as_ref()
232            .map(|key| PrivateKey::from_str(key).expect("Failed to parse private key."))
233            .unwrap_or(genesis_private_key);
234
235        let mut execution = None;
236        let mut verified = false;
237        let mut status = Status::None;
238
239        // Halts are handled by panics, so we need to catch them.
240        // I'm not thrilled about this usage of `AssertUnwindSafe`, but it seems to be
241        // used frequently in SnarkVM anyway.
242        let execute_output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
243            ledger.vm().execute_with_response(
244                &private_key,
245                (&case.program_name, &case.function),
246                case.input.iter(),
247                None,
248                0,
249                None,
250                &mut rng,
251            )
252        }));
253
254        if let Err(payload) = execute_output {
255            let s1 = payload.downcast_ref::<&str>().map(|s| s.to_string());
256            let s2 = payload.downcast_ref::<String>().cloned();
257            let s = s1.or(s2).unwrap_or_else(|| "Unknown panic payload".to_string());
258
259            case_outcomes.push(CaseOutcome {
260                status: Status::Halted(s),
261                verified: false,
262                errors: buf.extract_errs(),
263                warnings: buf.extract_warnings(),
264                execution: "".to_string(),
265                output: Value::make_unit(),
266            });
267            continue;
268        }
269
270        let result = execute_output.unwrap().and_then(|(transaction, response)| {
271            verified = ledger.vm().check_transaction(&transaction, None, &mut rng).is_ok();
272            execution = Some(transaction.clone());
273            let block = ledger.prepare_advance_to_next_beacon_block(
274                &private_key,
275                vec![],
276                vec![],
277                vec![transaction],
278                &mut rng,
279            )?;
280            status = match (block.aborted_transaction_ids().is_empty(), block.transactions().num_accepted() == 1) {
281                (false, _) => Status::Aborted,
282                (true, true) => Status::Accepted,
283                (true, false) => Status::Rejected,
284            };
285            ledger.advance_to_next_block(&block)?;
286            Ok(response)
287        });
288
289        let output = match result {
290            Ok(response) => {
291                let outputs = response.outputs();
292                match outputs.len() {
293                    0 => Value::make_unit(),
294                    1 => outputs[0].clone().into(),
295                    _ => Value::make_tuple(outputs.iter().map(|x| x.clone().into())),
296                }
297            }
298            Err(e) => {
299                handler.emit_err(LeoError::Anyhow(e));
300                Value::make_unit()
301            }
302        };
303
304        // Extract the execution, removing the global state root and proof.
305        // This is necessary as they are not deterministic across runs, even with RNG fixed.
306        let execution = if let Some(Transaction::Execute(_, _, execution, _)) = execution {
307            Some(Execution::from(execution.into_transitions(), Default::default(), None).unwrap())
308        } else {
309            None
310        };
311
312        case_outcomes.push(CaseOutcome {
313            status,
314            verified,
315            errors: buf.extract_errs(),
316            warnings: buf.extract_warnings(),
317            execution: serde_json::to_string_pretty(&execution).expect("Serialization failure"),
318            output,
319        });
320    }
321
322    Ok(case_outcomes)
323}