Skip to main content

leo_lang/cli/commands/
upgrade.rs

1// Copyright (C) 2019-2026 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    circuit::{Aleo, AleoTestnetV0},
27    ledger::query::Query as SnarkVMQuery,
28    prelude::{
29        Program,
30        TestnetV0,
31        VM,
32        store::{ConsensusStore, helpers::memory::ConsensusMemory},
33    },
34};
35
36use crate::cli::{
37    check_transaction::TransactionStatus,
38    commands::deploy::{consensus_limit, deploy_with_placeholder_certificate, validate_deployment_limits},
39};
40use aleo_std::StorageMode;
41use colored::*;
42use itertools::Itertools;
43use leo_span::Symbol;
44use snarkvm::{
45    prelude::{ConsensusVersion, ProgramID, Stack, check_program_plaintext_sizes, store::helpers::memory::BlockMemory},
46    synthesizer::program::StackTrait,
47};
48use std::path::PathBuf;
49
50/// Upgrades an Aleo program.
51#[derive(Parser, Debug)]
52pub struct LeoUpgrade {
53    #[clap(flatten)]
54    pub(crate) fee_options: FeeOptions,
55    #[clap(flatten)]
56    pub(crate) action: TransactionAction,
57    #[clap(flatten)]
58    pub(crate) env_override: EnvOptions,
59    #[clap(flatten)]
60    pub(crate) key_override: PrivateKeyOptions,
61    #[clap(flatten)]
62    pub(crate) consensus_override: ConsensusOptions,
63    #[clap(flatten)]
64    pub(crate) extra: ExtraOptions,
65    #[clap(long, help = "Skips the upgrade of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
66    pub(crate) skip: Vec<String>,
67    #[clap(flatten)]
68    pub(crate) build_options: BuildOptions,
69    #[clap(
70        long,
71        value_name = "DIR",
72        help = "Directory containing local .aleo imports when --path points to an Aleo bytecode file; other imports are fetched from the network"
73    )]
74    pub(crate) imports_dir: Option<PathBuf>,
75    #[clap(long, help = "Skips deployment certificate generation.")]
76    pub(crate) skip_deploy_certificate: bool,
77}
78
79impl Command for LeoUpgrade {
80    type Input = Package;
81    type Output = DeployOutput;
82
83    fn log_span(&self) -> Span {
84        tracing::span!(tracing::Level::INFO, "Leo")
85    }
86
87    fn prelude(&self, context: Context) -> Result<Self::Input> {
88        let path = context.dir()?;
89        if path.extension().and_then(|extension| extension.to_str()) == Some("aleo") {
90            if context.package_filter.is_some() {
91                return Err(crate::errors::custom("`--package` cannot be used with an Aleo bytecode file.").into());
92            }
93            let home_path = context.home()?;
94            let network = get_network(&self.env_override.network)?;
95            let endpoint = get_endpoint(&self.env_override.endpoint)?;
96            return Package::from_aleo_file(
97                path,
98                home_path,
99                self.imports_dir.as_deref(),
100                true,
101                self.build_options.no_local,
102                Some(network),
103                Some(&endpoint),
104                self.env_override.network_retries,
105            );
106        }
107        if self.imports_dir.is_some() {
108            return Err(
109                crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
110            );
111        }
112
113        LeoBuild {
114            env_override: self.env_override.clone(),
115            options: {
116                let mut options = self.build_options.clone();
117                options.no_cache = true;
118                options
119            },
120            rename: None,
121        }
122        .execute(context)
123    }
124
125    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
126        // Get the network, accounting for overrides.
127        let network = get_network(&self.env_override.network)?;
128        // Handle each network with the appropriate parameterization.
129        match network {
130            NetworkName::TestnetV0 => handle_upgrade::<TestnetV0, AleoTestnetV0>(&self, context, network, input),
131            NetworkName::MainnetV0 => {
132                #[cfg(feature = "only_testnet")]
133                panic!("Mainnet chosen with only_testnet feature");
134                #[cfg(not(feature = "only_testnet"))]
135                handle_upgrade::<MainnetV0, snarkvm::circuit::AleoV0>(&self, context, network, input)
136            }
137            NetworkName::CanaryV0 => {
138                #[cfg(feature = "only_testnet")]
139                panic!("Canary chosen with only_testnet feature");
140                #[cfg(not(feature = "only_testnet"))]
141                handle_upgrade::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(&self, context, network, input)
142            }
143        }
144    }
145}
146
147// A helper function to handle upgrade logic.
148fn handle_upgrade<N: Network, A: Aleo<Network = N>>(
149    command: &LeoUpgrade,
150    context: Context,
151    network: NetworkName,
152    package: Package,
153) -> Result<<LeoDeploy as Command>::Output> {
154    if package.compilation_units.last().map(|p| p.kind.is_library()).unwrap_or(false) {
155        return Err(crate::errors::custom("`leo upgrade` is not supported for library packages.").into());
156    }
157
158    // Get the private key and associated address, accounting for overrides.
159    let private_key = get_private_key(&command.key_override.private_key)?;
160    let address =
161        Address::try_from(&private_key).map_err(|e| crate::errors::custom(format!("Failed to parse address: {e}")))?;
162
163    // Get the endpoint, accounting for overrides.
164    let endpoint = get_endpoint(&command.env_override.endpoint)?;
165
166    // Get whether the network is a devnet, accounting for overrides.
167    let is_devnet = get_is_devnet(command.consensus_override.devnet);
168
169    // If the consensus heights are provided, use them; otherwise, use the default heights for the network.
170    let consensus_heights = command
171        .consensus_override
172        .consensus_heights
173        .clone()
174        .unwrap_or_else(|| get_consensus_heights(network, is_devnet));
175    // Validate the provided consensus heights.
176    validate_consensus_heights(&consensus_heights)
177        .map_err(|e| crate::errors::custom(format!("Invalid consensus heights: {e}")))?;
178    // Print the consensus heights being used.
179    let consensus_heights_string = consensus_heights.iter().format(",").to_string();
180    println!(
181        "\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"
182    );
183
184    // Set the consensus heights in the environment.
185    #[allow(unsafe_code)]
186    unsafe {
187        // SAFETY:
188        //  - `CONSENSUS_VERSION_HEIGHTS` is only set once and is only read in `snarkvm::prelude::load_consensus_heights`.
189        //  - There are no concurrent threads running at this point in the execution.
190        // WHY:
191        //  - This is needed because there is no way to set the desired consensus heights for a particular `VM` instance
192        //    without using the environment variable `CONSENSUS_VERSION_HEIGHTS`. Which is itself read once, and stored in a `OnceLock`.
193        std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
194    }
195
196    // Get all the programs but tests and libraries (libraries have no AVM bytecode).
197    let programs = package.compilation_units.iter().filter(|unit| unit.kind.is_program()).cloned();
198
199    let programs_and_bytecode: Vec<(leo_package::CompilationUnit, String)> = programs
200        .into_iter()
201        .map(|program| {
202            let bytecode = match &program.data {
203                ProgramData::Bytecode(s) => s.clone(),
204                ProgramData::SourcePath { .. } => {
205                    // We need to read the bytecode from its own build directory.
206                    let aleo_path = package.unit_bytecode_path(&program.name.to_string());
207                    fs::read_to_string(aleo_path.clone()).map_err(|e| {
208                        crate::errors::custom(format!("Failed to read file {}: {e}", aleo_path.display()))
209                    })?
210                }
211            };
212
213            Ok((program, bytecode))
214        })
215        .collect::<Result<_>>()?;
216
217    // Parse the fee options.
218    let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_bytecode.len())?;
219
220    let tasks: Vec<Task<N>> = programs_and_bytecode
221        .into_iter()
222        .zip(fee_options)
223        .map(|((program, bytecode), (priority_fee, record))| {
224            let id_str = format!("{}", program.name);
225            let id = id_str
226                .parse()
227                .map_err(|e| crate::errors::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
228            let bytecode_size = bytecode.len();
229            let parsed_program =
230                bytecode.parse().map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
231            Ok(Task {
232                id,
233                program: parsed_program,
234                edition: program.edition,
235                is_local: program.is_local,
236                priority_fee,
237                record,
238                bytecode_size,
239            })
240        })
241        .collect::<Result<_>>()?;
242
243    // Get the program IDs.
244    let program_ids = tasks.iter().map(|task| task.id).collect::<Vec<_>>();
245
246    // Split the tasks into local and remote dependencies.
247    let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
248
249    // Get the skipped programs.
250    let skipped: HashSet<ProgramID<N>> = local
251        .iter()
252        .filter_map(|task| {
253            let id_string = task.id.to_string();
254            command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
255        })
256        .collect();
257
258    // Get the consensus version.
259    let consensus_version = get_consensus_version(
260        &command.extra.consensus_version,
261        &endpoint,
262        network,
263        &consensus_heights,
264        &context,
265        command.env_override.network_retries,
266    )?;
267
268    // Fail closed for proven-invalid upgrades by using the stricter active endpoint rules; fetch uncertainty remains a warning below.
269    let validation_consensus_version =
270        get_endpoint_consensus_version(&endpoint, network, command.env_override.network_retries)
271            .map_or(consensus_version, |network_version| consensus_version.max(network_version));
272    let remote_programs =
273        validate_upgrade_tasks(&endpoint, network, &local, &skipped, validation_consensus_version, command)?;
274
275    // Build the config for JSON output.
276    let config = Some(Config {
277        address: address.to_string(),
278        network: network.to_string(),
279        endpoint: Some(endpoint.clone()),
280        consensus_version: Some(consensus_version as u8),
281    });
282
283    // Print a summary of the deployment plan.
284    print_deployment_plan(
285        &private_key,
286        &address,
287        &endpoint,
288        &network,
289        &local,
290        &skipped,
291        &remote,
292        &check_tasks_for_warnings(&endpoint, network, &local, &remote_programs, consensus_version, command),
293        consensus_version,
294        &command.into(),
295    );
296
297    // Prompt the user to confirm the plan.
298    if !confirm("Do you want to proceed with upgrade?", command.extra.yes)? {
299        println!("āŒ Upgrade aborted.");
300        return Ok(DeployOutput::default());
301    }
302
303    // Initialize an RNG.
304    let rng = &mut rand::rng();
305
306    // Initialize a new VM.
307    let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
308
309    // Load all the programs from the network into the VM.
310    let mut programs_and_editions = Vec::with_capacity(program_ids.len());
311    for id in &program_ids {
312        // Load the program from the network.
313        let Ok(program) = leo_package::CompilationUnit::fetch(
314            Symbol::intern(&id.name().to_string()),
315            None,
316            context.home()?,
317            network,
318            &endpoint,
319            true,
320            command.env_override.network_retries,
321        ) else {
322            warn_and_confirm(&format!("Failed to fetch program {id} from the network."), command.extra.yes)?;
323            continue;
324        };
325        let ProgramData::Bytecode(bytecode) = program.data else {
326            panic!("Expected bytecode when fetching a remote program");
327        };
328        // Parse the program bytecode.
329        let bytecode = Program::<N>::from_str(&bytecode)
330            .map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
331        // Program::fetch should always set the edition after a successful fetch.
332        let edition = program.edition.expect("Edition should be set after successful fetch");
333        programs_and_editions.push((bytecode, edition));
334    }
335
336    // Check for programs that violate edition/constructor requirements.
337    check_edition_constructor_requirements(&programs_and_editions, consensus_version, "upgrade")?;
338
339    vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
340
341    // Print the programs and their editions in the VM.
342    println!("Loaded the following programs into the VM:");
343    for program_id in vm.process().program_ids() {
344        let edition = *vm.process().get_stack(program_id)?.program_edition();
345        if program_id.to_string() == "credits.aleo" {
346            println!(" - credits.aleo (default)");
347        } else {
348            println!(" - {program_id} (edition {edition})");
349        }
350    }
351    println!();
352
353    // Specify the query.
354    let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
355        endpoint
356            .parse::<Uri>()
357            .map_err(|e| crate::errors::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
358    );
359
360    // For each of the programs, generate a deployment transaction.
361    let mut transactions = Vec::new();
362    let mut all_stats = Vec::new();
363    let mut all_broadcasts = Vec::new();
364    for Task { id, program, priority_fee, record, bytecode_size, .. } in local {
365        // If the program is a local dependency that is not skipped, generate a deployment transaction.
366        if !skipped.contains(&id) {
367            // Check plaintext declarations before deployment synthesis.
368            if validation_consensus_version >= ConsensusVersion::V20 {
369                let max_bits = consensus_limit(
370                    &N::MAX_PLAINTEXT_TYPE_SIZE_IN_BITS,
371                    validation_consensus_version,
372                    N::LATEST_MAX_PLAINTEXT_TYPE_SIZE_IN_BITS(),
373                );
374                let stack = Stack::new(vm.process(), &program)?;
375                check_program_plaintext_sizes(&program, &stack, max_bits)?;
376            }
377            let (transaction, stats) = if command.skip_deploy_certificate {
378                println!("āš ļø  Skipping deployment certificate generation as per user request.\n");
379                // Increment the edition from the existing on-chain program.
380                let edition = *vm.process().get_stack(id)?.program_edition() + 1;
381                println!("edition for deployed program: {}", edition);
382                deploy_with_placeholder_certificate::<N, A, _>(
383                    &vm,
384                    &private_key,
385                    &program,
386                    edition,
387                    record,
388                    priority_fee,
389                    bytecode_size,
390                    consensus_version,
391                    &query,
392                    command.env_override.network_retries,
393                    rng,
394                )?
395            } else {
396                println!("šŸ“¦ Creating deployment transaction for '{}'...\n", id.to_string().bold());
397                // Generate the transaction.
398                let transaction = vm
399                    .deploy(&private_key, &program, record, priority_fee.unwrap_or(0), Some(&query), rng)
400                    .map_err(|e| crate::errors::custom(format!("Failed to generate deployment transaction: {e}")))?;
401                // Get the deployment.
402                let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
403                // Add the program to the VM before calculating function costs.
404                vm.process().lock().add_program(&program)?;
405                // Compute the deployment stats.
406                let stats =
407                    compute_deployment_stats(&vm, deployment, priority_fee, consensus_version, bytecode_size, rng)?;
408                // Validate the deployment limits.
409                validate_deployment_limits(deployment, &id, &network, consensus_version)?;
410                (transaction, stats)
411            };
412
413            // Print the deployment summary and save the transaction and stats.
414            print_deployment_summary(&id.to_string(), &stats);
415            if command.action.broadcast {
416                warn_if_transaction_oversized(&id, &transaction, consensus_version, "upgrade");
417            }
418            transactions.push((id, transaction));
419            all_stats.push(stats);
420        }
421        // Add the program to the VM (idempotent; ensures skipped programs are available for later imports).
422        if let Err(e) = vm.process().lock().add_program(&program) {
423            warn_and_confirm(&format!("Failed to add program {id} to the VM. Error: {e}"), command.extra.yes)?;
424        }
425    }
426
427    // If the `print` option is set, print the deployment transaction to the console.
428    // The transaction is printed in JSON format.
429    if command.action.print {
430        for (program_name, transaction) in transactions.iter() {
431            // Pretty-print the transaction.
432            let transaction_json = serde_json::to_string_pretty(transaction)
433                .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
434            println!("šŸ–Øļø Printing deployment for {program_name}\n{transaction_json}")
435        }
436    }
437
438    // If the `save` option is set, save each deployment transaction to a file in the specified directory.
439    // The file format is `program_name.deployment.json`.
440    // The directory is created if it doesn't exist.
441    if let Some(path) = &command.action.save {
442        // Create the directory if it doesn't exist.
443        std::fs::create_dir_all(path).map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
444        for (program_name, transaction) in transactions.iter() {
445            // Save the transaction to a file.
446            let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
447            println!("šŸ’¾ Saving deployment for {program_name} at {}", file_path.display());
448            let transaction_json = serde_json::to_string_pretty(transaction)
449                .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
450            std::fs::write(file_path, transaction_json)
451                .map_err(|e| crate::errors::custom(format!("Failed to write transaction to file: {e}")))?;
452        }
453    }
454
455    // If the `broadcast` option is set, broadcast each upgrade transaction to the network.
456    if command.action.broadcast {
457        for (i, (program_id, transaction)) in transactions.iter().enumerate() {
458            println!("šŸ“” Broadcasting upgrade for {program_id}...");
459            // Get and confirm the fee with the user.
460            let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
461            if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
462                println!("ā© Upgrade skipped.");
463                continue;
464            }
465            let fee_id = fee.id().to_string();
466            let fee_transaction_id = Transaction::from_fee(fee.clone())?.id().to_string();
467            let id = transaction.id().to_string();
468            let height_before =
469                check_transaction::current_height(&endpoint, network, command.env_override.network_retries)?;
470            // Broadcast the transaction to the network.
471            let (message, status) = handle_broadcast(
472                &format!("{endpoint}/{network}/transaction/broadcast"),
473                transaction,
474                &program_id.to_string(),
475            )?;
476
477            let fail_and_prompt = |msg| {
478                println!("āŒ Failed to upgrade program {program_id}: {msg}.");
479                let count = transactions.len() - i - 1;
480                // Check if the user wants to continue with the next upgrade.
481                if count > 0 {
482                    confirm("Do you want to continue with the next upgrade?", command.extra.yes)
483                } else {
484                    Ok(false)
485                }
486            };
487
488            match status {
489                200..=299 => {
490                    let tx_status = check_transaction::check_transaction_with_message(
491                        &id,
492                        Some(&fee_id),
493                        &endpoint,
494                        network,
495                        height_before + 1,
496                        command.extra.max_wait,
497                        command.extra.blocks_to_check,
498                        command.env_override.network_retries,
499                    )?;
500                    let confirmed = tx_status == Some(TransactionStatus::Accepted);
501                    if confirmed {
502                        println!("āœ… Upgrade confirmed!");
503                    } else if fail_and_prompt("could not find the transaction on the network")? {
504                        continue;
505                    } else {
506                        return Ok(build_deploy_output(config.clone(), &transactions, &all_stats, &all_broadcasts));
507                    }
508                    all_broadcasts.push(BroadcastStats {
509                        fee_id: fee_id.clone(),
510                        fee_transaction_id: fee_transaction_id.clone(),
511                        confirmed,
512                    });
513                }
514                _ => {
515                    if fail_and_prompt(&message)? {
516                        continue;
517                    } else {
518                        return Ok(build_deploy_output(config.clone(), &transactions, &all_stats, &all_broadcasts));
519                    }
520                }
521            }
522        }
523    }
524
525    Ok(build_deploy_output(config, &transactions, &all_stats, &all_broadcasts))
526}
527
528fn validate_upgrade_tasks<N: Network>(
529    endpoint: &str,
530    network: NetworkName,
531    tasks: &[Task<N>],
532    skipped: &HashSet<ProgramID<N>>,
533    consensus_version: ConsensusVersion,
534    command: &LeoUpgrade,
535) -> Result<Vec<(ProgramID<N>, Program<N>)>> {
536    let mut remote_programs = Vec::with_capacity(tasks.len());
537
538    for Task { id, program, is_local, .. } in tasks {
539        // A proven-invalid upgrade is rejected before transaction construction in every output mode.
540        if !is_local || skipped.contains(id) {
541            continue;
542        }
543
544        let Ok(remote_program) =
545            fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries)
546        else {
547            // Fetch uncertainty remains in `check_tasks_for_warnings` so transient network state does not block planning.
548            continue;
549        };
550        let Ok(remote_program) = Program::<N>::from_str(&remote_program) else {
551            continue;
552        };
553        reject_invalid_upgrade(id, &remote_program, program, consensus_version)?;
554        remote_programs.push((*id, remote_program));
555    }
556
557    Ok(remote_programs)
558}
559
560fn get_endpoint_consensus_version(
561    endpoint: &str,
562    network: NetworkName,
563    network_retries: u32,
564) -> Option<ConsensusVersion> {
565    let response = leo_package::fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries)
566        .ok()?
567        .parse::<u8>()
568        .ok()?;
569
570    number_to_consensus_version(response as usize).ok()
571}
572
573fn reject_invalid_upgrade<N: Network>(
574    id: &ProgramID<N>,
575    remote_program: &Program<N>,
576    new_program: &Program<N>,
577    consensus_version: ConsensusVersion,
578) -> Result<()> {
579    if !remote_program.contains_constructor() {
580        return Ok(());
581    }
582
583    Stack::check_upgrade_is_valid(remote_program, new_program)
584        .and_then(|_| {
585            if consensus_version >= ConsensusVersion::V10 {
586                snarkvm::synthesizer::vm::check_output_register_indices_unchanged(remote_program, new_program)
587            } else {
588                Ok(())
589            }
590        })
591        .map_err(|e| {
592            crate::errors::custom(format!("program '{id}' is not a valid upgrade: {e}"))
593                .with_help(
594                    "Try preserving the original interface and output registers, adding a new function for the changed \
595                    interface, or deploying a new program.",
596                )
597                .into()
598        })
599}
600
601/// Check the tasks to warn the user about any potential issues.
602/// The following properties are checked:
603/// - If the transaction is to be broadcast:
604///     - The program exists on the network and the new program is a valid upgrade.
605///     - If the consensus version is less than V9, the program does not use V9 features.
606///     - If the consensus version is V9 or greater, the program contains a constructor.
607fn check_tasks_for_warnings<N: Network>(
608    endpoint: &str,
609    network: NetworkName,
610    tasks: &[Task<N>],
611    remote_programs: &[(ProgramID<N>, Program<N>)],
612    consensus_version: ConsensusVersion,
613    command: &LeoUpgrade,
614) -> Vec<String> {
615    let mut warnings = Vec::new();
616    for Task { id, program, is_local, bytecode_size, .. } in tasks {
617        if !is_local || !command.action.broadcast {
618            continue;
619        }
620
621        // Check if the program exists on the network.
622        if let Some((_, remote_program)) = remote_programs.iter().find(|(remote_id, _)| remote_id == id) {
623            push_remote_upgrade_warnings(id, remote_program, program, consensus_version, &mut warnings);
624        } else if let Ok(remote_program) =
625            fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries)
626        {
627            // Parse the program.
628            let remote_program = match Program::<N>::from_str(&remote_program) {
629                Ok(program) => program,
630                Err(e) => {
631                    warnings.push(format!("Could not parse '{id}' from the network. Error: {e}",));
632                    continue;
633                }
634            };
635            push_remote_upgrade_warnings(id, &remote_program, program, consensus_version, &mut warnings);
636        } else {
637            warnings.push(format!("The program '{id}' does not exist on the network. The upgrade will likely fail.",));
638        }
639        // Check if the program has a valid naming scheme.
640        if consensus_version >= ConsensusVersion::V7
641            && let Err(e) = program.check_program_naming_structure()
642        {
643            warnings.push(format!(
644                "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
645            ));
646        }
647        // Check if the program contains restricted keywords.
648        if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
649            warnings.push(format!(
650                "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
651                consensus_version as u8
652            ));
653        }
654        // Check if the program uses V9 features.
655        if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
656            warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The upgrade will likely fail"));
657        }
658        // Check if the program uses V15 features (e.g., `view` blocks).
659        if consensus_version < ConsensusVersion::V15 && program.contains_v15_syntax() {
660            warnings.push(format!("The program '{id}' uses V15 features (e.g., `view fn`) but the consensus version is less than V15. The upgrade will likely fail"));
661        }
662        // Check if the program uses V16 features (e.g., `std::prog::function_checksum`).
663        if consensus_version < ConsensusVersion::V16 && program.contains_v16_syntax() {
664            warnings.push(format!("The program '{id}' uses V16 features (e.g., `std::prog::function_checksum`) but the consensus version is less than V16. The upgrade will likely fail"));
665        }
666        // Check if the program contains a constructor.
667        if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
668            warnings.push(format!("The program '{id}' does not contain a constructor. The upgrade will likely fail",));
669        }
670        // An upgrade redeploys bytecode, so it must fit the target version's size limit.
671        let max_size = max_program_size_for_consensus_version::<N>(consensus_version);
672        if *bytecode_size > max_size {
673            warnings.push(format!(
674                "The program '{id}' is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The upgrade will likely fail.",
675                *bytecode_size as f64 / 1024.0,
676                max_size as f64 / 1024.0,
677                consensus_version as u8,
678            ));
679        } else if let (_, _, Some(msg)) = format_program_size(*bytecode_size, max_size) {
680            warnings.push(format!("The program '{id}' is {msg}."));
681        }
682        // Check for a consensus version mismatch.
683        if let Err(e) =
684            check_consensus_version_mismatch(consensus_version, endpoint, network, command.env_override.network_retries)
685        {
686            warnings.push(format!("{e}. In some cases, the deployment may fail"));
687        }
688    }
689    warnings
690}
691
692fn push_remote_upgrade_warnings<N: Network>(
693    id: &ProgramID<N>,
694    remote_program: &Program<N>,
695    program: &Program<N>,
696    consensus_version: ConsensusVersion,
697    warnings: &mut Vec<String>,
698) {
699    // Check if the program is a valid upgrade.
700    if remote_program.contains_constructor() {
701        if let Err(e) = reject_invalid_upgrade(id, remote_program, program, consensus_version) {
702            warnings.push(e.to_string());
703        }
704    } else if consensus_version >= ConsensusVersion::V8 {
705        warnings.push(format!("The program '{id}' can only ever be upgraded once and its contents cannot be changed. Otherwise, the upgrade will likely fail."));
706    } else {
707        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."));
708    }
709}
710
711// Convert the `LeoUpgrade` into a `LeoDeploy` command.
712impl From<&LeoUpgrade> for LeoDeploy {
713    fn from(upgrade: &LeoUpgrade) -> Self {
714        Self {
715            fee_options: upgrade.fee_options.clone(),
716            action: upgrade.action.clone(),
717            env_override: upgrade.env_override.clone(),
718            key_override: upgrade.key_override.clone(),
719            consensus_override: upgrade.consensus_override.clone(),
720            extra: upgrade.extra.clone(),
721            skip: upgrade.skip.clone(),
722            rename: None,
723            build_options: upgrade.build_options.clone(),
724            imports_dir: upgrade.imports_dir.clone(),
725            skip_deploy_certificate: upgrade.skip_deploy_certificate,
726        }
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733    use snarkvm::prelude::TestnetV0;
734
735    fn parse_program(source: &str) -> Program<TestnetV0> {
736        Program::<TestnetV0>::from_str(source).unwrap()
737    }
738
739    #[test]
740    fn rejects_upgrade_that_changes_record_output_register() {
741        let original = parse_program(
742            r"program upgrade_check.aleo;
743
744record Token:
745    owner as address.private;
746    amount as u64.private;
747
748function mint:
749    input r0 as address.private;
750    input r1 as u64.private;
751    cast r0 r1 into r2 as Token.record;
752    output r2 as Token.record;
753
754constructor:
755    assert.eq edition 0u16;
756",
757        );
758        let changed_output_register = parse_program(
759            r"program upgrade_check.aleo;
760
761record Token:
762    owner as address.private;
763    amount as u64.private;
764
765function mint:
766    input r0 as address.private;
767    input r1 as u64.private;
768    add r1 0u64 into r2;
769    cast r0 r2 into r3 as Token.record;
770    output r3 as Token.record;
771
772constructor:
773    assert.eq edition 0u16;
774",
775        );
776        let id = original.id();
777
778        let error = reject_invalid_upgrade(id, &original, &changed_output_register, ConsensusVersion::V10)
779            .unwrap_err()
780            .to_string();
781
782        assert!(error.contains("not a valid upgrade"));
783        assert!(error.contains("output register"));
784    }
785
786    #[test]
787    fn accepts_upgrade_that_preserves_record_output_register() {
788        let original = parse_program(
789            r"program upgrade_check.aleo;
790
791record Token:
792    owner as address.private;
793    amount as u64.private;
794
795function mint:
796    input r0 as address.private;
797    input r1 as u64.private;
798    cast r0 r1 into r2 as Token.record;
799    output r2 as Token.record;
800
801constructor:
802    assert.eq edition 0u16;
803",
804        );
805        let same_interface = parse_program(
806            r"program upgrade_check.aleo;
807
808record Token:
809    owner as address.private;
810    amount as u64.private;
811
812function mint:
813    input r0 as address.private;
814    input r1 as u64.private;
815    add r1 1u64 into r3;
816    cast r0 r1 into r2 as Token.record;
817    output r2 as Token.record;
818
819constructor:
820    assert.eq edition 0u16;
821",
822        );
823
824        assert!(reject_invalid_upgrade(original.id(), &original, &same_interface, ConsensusVersion::V10).is_ok());
825    }
826
827    #[test]
828    fn warns_for_upgrade_that_changes_record_output_register() {
829        let original = parse_program(
830            r"program upgrade_check.aleo;
831
832record Token:
833    owner as address.private;
834    amount as u64.private;
835
836function mint:
837    input r0 as address.private;
838    input r1 as u64.private;
839    cast r0 r1 into r2 as Token.record;
840    output r2 as Token.record;
841
842constructor:
843    assert.eq edition 0u16;
844",
845        );
846        let changed_output_register = parse_program(
847            r"program upgrade_check.aleo;
848
849record Token:
850    owner as address.private;
851    amount as u64.private;
852
853function mint:
854    input r0 as address.private;
855    input r1 as u64.private;
856    add r1 0u64 into r2;
857    cast r0 r2 into r3 as Token.record;
858    output r3 as Token.record;
859
860constructor:
861    assert.eq edition 0u16;
862",
863        );
864        let mut warnings = Vec::new();
865
866        push_remote_upgrade_warnings(
867            original.id(),
868            &original,
869            &changed_output_register,
870            ConsensusVersion::V10,
871            &mut warnings,
872        );
873
874        assert_eq!(warnings.len(), 1);
875        assert!(warnings[0].contains("not a valid upgrade"));
876        assert!(warnings[0].contains("Try preserving the original interface and output registers"));
877    }
878}