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