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