leo_lang/cli/commands/
upgrade.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 super::*;
18use std::{collections::HashSet, fs};
19
20use leo_ast::NetworkName;
21use leo_package::{Package, ProgramData, fetch_program_from_network};
22
23#[cfg(not(feature = "only_testnet"))]
24use snarkvm::prelude::{CanaryV0, MainnetV0};
25use snarkvm::{
26    ledger::query::Query as SnarkVMQuery,
27    prelude::{
28        Program,
29        TestnetV0,
30        VM,
31        store::{ConsensusStore, helpers::memory::ConsensusMemory},
32    },
33};
34
35use crate::cli::{check_transaction::TransactionStatus, commands::deploy::validate_deployment_limits};
36use aleo_std::StorageMode;
37use colored::*;
38use itertools::Itertools;
39use leo_span::Symbol;
40use snarkvm::{
41    prelude::{ConsensusVersion, ProgramID, Stack, store::helpers::memory::BlockMemory},
42    synthesizer::program::StackTrait,
43};
44use std::path::PathBuf;
45
46/// Upgrades an Aleo program.
47#[derive(Parser, Debug)]
48pub struct LeoUpgrade {
49    #[clap(flatten)]
50    pub(crate) fee_options: FeeOptions,
51    #[clap(flatten)]
52    pub(crate) action: TransactionAction,
53    #[clap(flatten)]
54    pub(crate) env_override: EnvOptions,
55    #[clap(flatten)]
56    pub(crate) extra: ExtraOptions,
57    #[clap(long, help = "Skips the upgrade of any program that contains one of the given substrings.")]
58    pub(crate) skip: Vec<String>,
59    #[clap(flatten)]
60    pub(crate) build_options: BuildOptions,
61}
62
63impl Command for LeoUpgrade {
64    type Input = Package;
65    type Output = ();
66
67    fn log_span(&self) -> Span {
68        tracing::span!(tracing::Level::INFO, "Leo")
69    }
70
71    fn prelude(&self, context: Context) -> Result<Self::Input> {
72        LeoBuild {
73            env_override: self.env_override.clone(),
74            options: {
75                let mut options = self.build_options.clone();
76                options.no_cache = true;
77                options
78            },
79        }
80        .execute(context)
81    }
82
83    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
84        // Get the network, accounting for overrides.
85        let network = get_network(&self.env_override.network)?;
86        // Handle each network with the appropriate parameterization.
87        match network {
88            NetworkName::TestnetV0 => handle_upgrade::<TestnetV0>(&self, context, network, input),
89            NetworkName::MainnetV0 => {
90                #[cfg(feature = "only_testnet")]
91                panic!("Mainnet chosen with only_testnet feature");
92                #[cfg(not(feature = "only_testnet"))]
93                handle_upgrade::<MainnetV0>(&self, context, network, input)
94            }
95            NetworkName::CanaryV0 => {
96                #[cfg(feature = "only_testnet")]
97                panic!("Canary chosen with only_testnet feature");
98                #[cfg(not(feature = "only_testnet"))]
99                handle_upgrade::<CanaryV0>(&self, context, network, input)
100            }
101        }
102    }
103}
104
105// A helper function to handle upgrade logic.
106fn handle_upgrade<N: Network>(
107    command: &LeoUpgrade,
108    context: Context,
109    network: NetworkName,
110    package: Package,
111) -> Result<<LeoDeploy as Command>::Output> {
112    // Get the private key and associated address, accounting for overrides.
113    let private_key = get_private_key(&command.env_override.private_key)?;
114    let address =
115        Address::try_from(&private_key).map_err(|e| CliError::custom(format!("Failed to parse address: {e}")))?;
116
117    // Get the endpoint, accounting for overrides.
118    let endpoint = get_endpoint(&command.env_override.endpoint)?;
119
120    // Get whether the network is a devnet, accounting for overrides.
121    let is_devnet = get_is_devnet(command.env_override.devnet);
122
123    // If the consensus heights are provided, use them; otherwise, use the default heights for the network.
124    let consensus_heights =
125        command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
126    // Validate the provided consensus heights.
127    validate_consensus_heights(&consensus_heights)
128        .map_err(|e| CliError::custom(format!("Invalid consensus heights: {e}")))?;
129    // Print the consensus heights being used.
130    let consensus_heights_string = consensus_heights.iter().format(",").to_string();
131    println!(
132        "\nšŸ“¢ Using the following consensus heights: {consensus_heights_string}\n  To override, pass in `--consensus-heights` or override the environment variable `CONSENSUS_VERSION_HEIGHTS`.\n"
133    );
134
135    // Set the consensus heights in the environment.
136    #[allow(unsafe_code)]
137    unsafe {
138        // SAFETY:
139        //  - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `snarkvm::prelude::load_consensus_heights`.
140        //  - There are no concurrent threads running at this point in the execution.
141        // WHY:
142        //  - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance
143        //    without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`.
144        std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
145    }
146
147    // Get all the programs but tests.
148    let programs = package.programs.iter().filter(|program| !program.is_test).cloned();
149
150    let programs_and_bytecode: Vec<(leo_package::Program, String)> = programs
151        .into_iter()
152        .map(|program| {
153            let bytecode = match &program.data {
154                ProgramData::Bytecode(s) => s.clone(),
155                ProgramData::SourcePath { .. } => {
156                    // We need to read the bytecode from the filesystem.
157                    let aleo_name = format!("{}.aleo", program.name);
158                    let aleo_path = if package.manifest.program == aleo_name {
159                        // The main program in the package, so its .aleo file
160                        // will be in the build directory.
161                        package.build_directory().join("main.aleo")
162                    } else {
163                        // Some other dependency, so look in `imports`.
164                        package.imports_directory().join(aleo_name)
165                    };
166                    fs::read_to_string(aleo_path.clone())
167                        .map_err(|e| CliError::custom(format!("Failed to read file {}: {e}", aleo_path.display())))?
168                }
169            };
170
171            Ok((program, bytecode))
172        })
173        .collect::<Result<_>>()?;
174
175    // Parse the fee options.
176    let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_bytecode.len())?;
177
178    let tasks: Vec<Task<N>> = programs_and_bytecode
179        .into_iter()
180        .zip(fee_options)
181        .map(|((program, bytecode), (_base_fee, priority_fee, record))| {
182            let id_str = format!("{}.aleo", program.name);
183            let id =
184                id_str.parse().map_err(|e| CliError::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
185            let bytecode_size = bytecode.len();
186            let parsed_program =
187                bytecode.parse().map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
188            Ok(Task {
189                id,
190                program: parsed_program,
191                edition: program.edition,
192                is_local: program.is_local,
193                priority_fee,
194                record,
195                bytecode_size,
196            })
197        })
198        .collect::<Result<_>>()?;
199
200    // Get the program IDs.
201    let program_ids = tasks.iter().map(|task| task.id).collect::<Vec<_>>();
202
203    // Split the tasks into local and remote dependencies.
204    let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
205
206    // Get the skipped programs.
207    let skipped: HashSet<ProgramID<N>> = local
208        .iter()
209        .filter_map(|task| {
210            let id_string = task.id.to_string();
211            command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
212        })
213        .collect();
214
215    // Get the consensus version.
216    let consensus_version =
217        get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
218
219    // Print a summary of the deployment plan.
220    print_deployment_plan(
221        &private_key,
222        &address,
223        &endpoint,
224        &network,
225        &local,
226        &skipped,
227        &remote,
228        &check_tasks_for_warnings(&endpoint, network, &local, consensus_version, command),
229        consensus_version,
230        &command.into(),
231    );
232
233    // Prompt the user to confirm the plan.
234    if !confirm("Do you want to proceed with upgrade?", command.extra.yes)? {
235        println!("āŒ Upgrade aborted.");
236        return Ok(());
237    }
238
239    // Initialize an RNG.
240    let rng = &mut rand::thread_rng();
241
242    // Initialize a new VM.
243    let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
244
245    // Load all the programs from the network into the VM.
246    let programs_and_editions = program_ids
247        .iter()
248        .map(|id| {
249            // Load the program from the network.
250            let program = leo_package::Program::fetch(
251                Symbol::intern(&id.name().to_string()),
252                None,
253                context.home()?,
254                network,
255                &endpoint,
256                true,
257            )?;
258            let ProgramData::Bytecode(bytecode) = program.data else {
259                panic!("Expected bytecode when fetching a remote program");
260            };
261            // Parse the program bytecode.
262            let bytecode = Program::<N>::from_str(&bytecode)
263                .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
264            // Return the bytecode and edition.
265            // Program::fetch should always set the edition after a successful fetch.
266            let edition = program.edition.expect("Edition should be set after successful fetch");
267            Ok((bytecode, edition))
268        })
269        .collect::<Result<Vec<_>>>()?;
270
271    // Check for programs that violate edition/constructor requirements.
272    check_edition_constructor_requirements(&programs_and_editions, consensus_version, "upgrade")?;
273
274    vm.process().write().add_programs_with_editions(&programs_and_editions)?;
275
276    // Print the programs and their editions in the VM.
277    println!("Loaded the following programs into the VM:");
278    for program_id in vm.process().read().program_ids() {
279        let edition = *vm.process().read().get_stack(program_id)?.program_edition();
280        if program_id.to_string() == "credits.aleo" {
281            println!(" - credits.aleo (default)");
282        } else {
283            println!(" - {program_id} (edition {edition})");
284        }
285    }
286    println!();
287
288    // Remove version suffixes from the endpoint.
289    let re = regex::Regex::new(r"v\d+$").unwrap();
290    let query_endpoint = re.replace(&endpoint, "").to_string();
291
292    // Specify the query.
293    let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
294        query_endpoint
295            .parse::<Uri>()
296            .map_err(|e| CliError::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
297    );
298
299    // For each of the programs, generate a deployment transaction.
300    let mut transactions = Vec::new();
301    for Task { id, program, priority_fee, record, bytecode_size, .. } in local {
302        // If the program is a local dependency that is not skipped, generate a deployment transaction.
303        if !skipped.contains(&id) {
304            println!("šŸ“¦ Creating deployment transaction for '{}'...\n", id.to_string().bold());
305            // Generate the transaction.
306            let transaction =
307                vm.deploy(&private_key, &program, record, priority_fee.unwrap_or(0), Some(&query), rng)
308                    .map_err(|e| CliError::custom(format!("Failed to generate deployment transaction: {e}")))?;
309            // Get the deployment.
310            let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
311            // Print the deployment stats.
312            print_deployment_stats(&vm, &id.to_string(), deployment, priority_fee, consensus_version, bytecode_size)?;
313            // Validate the deployment limits.
314            validate_deployment_limits(deployment, &id, &network)?;
315            // Save the transaction.
316            transactions.push((id, transaction));
317        }
318        // Add the program to the VM.
319        vm.process().write().add_program(&program)?;
320    }
321
322    // If the `print` option is set, print the deployment transaction to the console.
323    // The transaction is printed in JSON format.
324    if command.action.print {
325        for (program_name, transaction) in transactions.iter() {
326            // Pretty-print the transaction.
327            let transaction_json = serde_json::to_string_pretty(transaction)
328                .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
329            println!("šŸ–Øļø Printing deployment for {program_name}\n{transaction_json}")
330        }
331    }
332
333    // If the `save` option is set, save each deployment transaction to a file in the specified directory.
334    // The file format is `program_name.deployment.json`.
335    // The directory is created if it doesn't exist.
336    if let Some(path) = &command.action.save {
337        // Create the directory if it doesn't exist.
338        std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
339        for (program_name, transaction) in transactions.iter() {
340            // Save the transaction to a file.
341            let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
342            println!("šŸ’¾ Saving deployment for {program_name} at {}", file_path.display());
343            let transaction_json = serde_json::to_string_pretty(transaction)
344                .map_err(|e| CliError::custom(format!("Failed to serialize transaction: {e}")))?;
345            std::fs::write(file_path, transaction_json)
346                .map_err(|e| CliError::custom(format!("Failed to write transaction to file: {e}")))?;
347        }
348    }
349
350    // If the `broadcast` option is set, broadcast each upgrade transaction to the network.
351    if command.action.broadcast {
352        for (i, (program_id, transaction)) in transactions.iter().enumerate() {
353            println!("šŸ“” Broadcasting upgrade for {program_id}...");
354            // Get and confirm the fee with the user.
355            let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
356            if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
357                println!("ā© Upgrade skipped.");
358                continue;
359            }
360            let fee_id = fee.id().to_string();
361            let id = transaction.id().to_string();
362            let height_before = check_transaction::current_height(&endpoint, network)?;
363            // Broadcast the transaction to the network.
364            let (message, status) = handle_broadcast(
365                &format!("{endpoint}/{network}/transaction/broadcast"),
366                transaction,
367                &program_id.to_string(),
368            )?;
369
370            let fail_and_prompt = |msg| {
371                println!("āŒ Failed to upgrade program {program_id}: {msg}.");
372                let count = transactions.len() - i - 1;
373                // Check if the user wants to continue with the next upgrade.
374                if count > 0 {
375                    confirm("Do you want to continue with the next upgrade?", command.extra.yes)
376                } else {
377                    Ok(false)
378                }
379            };
380
381            match status {
382                200..=299 => {
383                    let status = check_transaction::check_transaction_with_message(
384                        &id,
385                        Some(&fee_id),
386                        &endpoint,
387                        network,
388                        height_before + 1,
389                        command.extra.max_wait,
390                        command.extra.blocks_to_check,
391                    )?;
392                    if status == Some(TransactionStatus::Accepted) {
393                        println!("āœ… Upgrade confirmed!");
394                    } else if fail_and_prompt("could not find the transaction on the network")? {
395                        continue;
396                    } else {
397                        return Ok(());
398                    }
399                }
400                _ => {
401                    if fail_and_prompt(&message)? {
402                        continue;
403                    } else {
404                        return Ok(());
405                    }
406                }
407            }
408        }
409    }
410
411    Ok(())
412}
413
414/// Check the tasks to warn the user about any potential issues.
415/// The following properties are checked:
416/// - If the transaction is to be broadcast:
417///     - The program exists on the network and the new program is a valid upgrade.
418///     - If the consensus version is less than V9, the program does not use V9 features.
419///     - If the consensus version is V9 or greater, the program contains a constructor.
420fn check_tasks_for_warnings<N: Network>(
421    endpoint: &str,
422    network: NetworkName,
423    tasks: &[Task<N>],
424    consensus_version: ConsensusVersion,
425    command: &LeoUpgrade,
426) -> Vec<String> {
427    let mut warnings = Vec::new();
428    for Task { id, program, is_local, .. } in tasks {
429        if !is_local || !command.action.broadcast {
430            continue;
431        }
432
433        // Check if the program exists on the network.
434        if let Ok(remote_program) = fetch_program_from_network(&id.to_string(), endpoint, network) {
435            // Parse the program.
436            let remote_program = match Program::<N>::from_str(&remote_program) {
437                Ok(program) => program,
438                Err(e) => {
439                    warnings.push(format!("Could not parse '{id}' from the network. Error: {e}",));
440                    continue;
441                }
442            };
443            // Check if the program is a valid upgrade.
444            if remote_program.contains_constructor() {
445                if let Err(e) = Stack::check_upgrade_is_valid(&remote_program, program) {
446                    warnings.push(format!(
447                        "The program '{id}' is not a valid upgrade. The upgrade will likely fail. Error: {e}",
448                    ));
449                }
450            } else if consensus_version >= ConsensusVersion::V8 {
451                warnings.push(format!("The program '{id}' can only ever be upgraded once and its contents cannot be changed. Otherwise, the upgrade will likely fail."));
452            } else {
453                warnings.push(format!("The program '{id}' does not have a constructor and is not eligible for a one-time upgrade (>= `ConsensusVersion::V8`). The upgrade will likely fail."));
454            }
455        } else {
456            warnings.push(format!("The program '{id}' does not exist on the network. The upgrade will likely fail.",));
457        }
458        // Check if the program has a valid naming scheme.
459        if consensus_version >= ConsensusVersion::V7
460            && let Err(e) = program.check_program_naming_structure()
461        {
462            warnings.push(format!(
463                "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
464            ));
465        }
466        // Check if the program contains restricted keywords.
467        if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
468            warnings.push(format!(
469                "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
470                consensus_version as u8
471            ));
472        }
473        // Check if the program uses V9 features.
474        if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
475            warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The upgrade will likely fail"));
476        }
477        // Check if the program contains a constructor.
478        if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
479            warnings.push(format!("The program '{id}' does not contain a constructor. The upgrade will likely fail",));
480        }
481        // Check for a consensus version mismatch.
482        if let Err(e) = check_consensus_version_mismatch(consensus_version, endpoint, network) {
483            warnings.push(format!("{e}. In some cases, the deployment may fail"));
484        }
485    }
486    warnings
487}
488
489// Convert the `LeoUpgrade` into a `LeoDeploy` command.
490impl From<&LeoUpgrade> for LeoDeploy {
491    fn from(upgrade: &LeoUpgrade) -> Self {
492        Self {
493            fee_options: upgrade.fee_options.clone(),
494            action: upgrade.action.clone(),
495            env_override: upgrade.env_override.clone(),
496            extra: upgrade.extra.clone(),
497            skip: upgrade.skip.clone(),
498            build_options: upgrade.build_options.clone(),
499        }
500    }
501}