use super::*;
use std::{collections::HashSet, fs};
use leo_ast::NetworkName;
use leo_package::{Package, ProgramData, fetch_program_from_network};
#[cfg(not(feature = "only_testnet"))]
use snarkvm::prelude::{CanaryV0, MainnetV0};
use snarkvm::{
circuit::{Aleo, AleoTestnetV0},
ledger::query::Query as SnarkVMQuery,
prelude::{
Program,
TestnetV0,
VM,
store::{ConsensusStore, helpers::memory::ConsensusMemory},
},
};
use crate::cli::{
check_transaction::TransactionStatus,
commands::deploy::{deploy_with_placeholder_certificate, validate_deployment_limits},
};
use aleo_std::StorageMode;
use colored::*;
use itertools::Itertools;
use leo_span::Symbol;
use snarkvm::{
prelude::{ConsensusVersion, ProgramID, Stack, store::helpers::memory::BlockMemory},
synthesizer::program::StackTrait,
};
use std::path::PathBuf;
#[derive(Parser, Debug)]
pub struct LeoUpgrade {
#[clap(flatten)]
pub(crate) fee_options: FeeOptions,
#[clap(flatten)]
pub(crate) action: TransactionAction,
#[clap(flatten)]
pub(crate) env_override: EnvOptions,
#[clap(flatten)]
pub(crate) key_override: PrivateKeyOptions,
#[clap(flatten)]
pub(crate) consensus_override: ConsensusOptions,
#[clap(flatten)]
pub(crate) extra: ExtraOptions,
#[clap(long, help = "Skips the upgrade of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
pub(crate) skip: Vec<String>,
#[clap(flatten)]
pub(crate) build_options: BuildOptions,
#[clap(long, help = "Skips deployment certificate generation.")]
pub(crate) skip_deploy_certificate: bool,
}
impl Command for LeoUpgrade {
type Input = Package;
type Output = DeployOutput;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, context: Context) -> Result<Self::Input> {
LeoBuild {
env_override: self.env_override.clone(),
options: {
let mut options = self.build_options.clone();
options.no_cache = true;
options
},
rename: None,
}
.execute(context)
}
fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
let network = get_network(&self.env_override.network)?;
match network {
NetworkName::TestnetV0 => handle_upgrade::<TestnetV0, AleoTestnetV0>(&self, context, network, input),
NetworkName::MainnetV0 => {
#[cfg(feature = "only_testnet")]
panic!("Mainnet chosen with only_testnet feature");
#[cfg(not(feature = "only_testnet"))]
handle_upgrade::<MainnetV0, snarkvm::circuit::AleoV0>(&self, context, network, input)
}
NetworkName::CanaryV0 => {
#[cfg(feature = "only_testnet")]
panic!("Canary chosen with only_testnet feature");
#[cfg(not(feature = "only_testnet"))]
handle_upgrade::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(&self, context, network, input)
}
}
}
}
fn handle_upgrade<N: Network, A: Aleo<Network = N>>(
command: &LeoUpgrade,
context: Context,
network: NetworkName,
package: Package,
) -> Result<<LeoDeploy as Command>::Output> {
if package.compilation_units.last().map(|p| p.kind.is_library()).unwrap_or(false) {
return Err(crate::errors::custom("`leo upgrade` is not supported for library packages.").into());
}
let private_key = get_private_key(&command.key_override.private_key)?;
let address =
Address::try_from(&private_key).map_err(|e| crate::errors::custom(format!("Failed to parse address: {e}")))?;
let endpoint = get_endpoint(&command.env_override.endpoint)?;
let is_devnet = get_is_devnet(command.consensus_override.devnet);
let consensus_heights = command
.consensus_override
.consensus_heights
.clone()
.unwrap_or_else(|| get_consensus_heights(network, is_devnet));
validate_consensus_heights(&consensus_heights)
.map_err(|e| crate::errors::custom(format!("Invalid consensus heights: {e}")))?;
let consensus_heights_string = consensus_heights.iter().format(",").to_string();
println!(
"\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"
);
#[allow(unsafe_code)]
unsafe {
std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
}
let programs = package.compilation_units.iter().filter(|unit| unit.kind.is_program()).cloned();
let programs_and_bytecode: Vec<(leo_package::CompilationUnit, String)> = programs
.into_iter()
.map(|program| {
let bytecode = match &program.data {
ProgramData::Bytecode(s) => s.clone(),
ProgramData::SourcePath { .. } => {
let aleo_path = package.unit_bytecode_path(&program.name.to_string());
fs::read_to_string(aleo_path.clone()).map_err(|e| {
crate::errors::custom(format!("Failed to read file {}: {e}", aleo_path.display()))
})?
}
};
Ok((program, bytecode))
})
.collect::<Result<_>>()?;
let fee_options = parse_fee_options(&private_key, &command.fee_options, programs_and_bytecode.len())?;
let tasks: Vec<Task<N>> = programs_and_bytecode
.into_iter()
.zip(fee_options)
.map(|((program, bytecode), (priority_fee, record))| {
let id_str = format!("{}", program.name);
let id = id_str
.parse()
.map_err(|e| crate::errors::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
let bytecode_size = bytecode.len();
let parsed_program =
bytecode.parse().map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
Ok(Task {
id,
program: parsed_program,
edition: program.edition,
is_local: program.is_local,
priority_fee,
record,
bytecode_size,
})
})
.collect::<Result<_>>()?;
let program_ids = tasks.iter().map(|task| task.id).collect::<Vec<_>>();
let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
let skipped: HashSet<ProgramID<N>> = local
.iter()
.filter_map(|task| {
let id_string = task.id.to_string();
command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
})
.collect();
let consensus_version = get_consensus_version(
&command.extra.consensus_version,
&endpoint,
network,
&consensus_heights,
&context,
command.env_override.network_retries,
)?;
let validation_consensus_version =
get_endpoint_consensus_version(&endpoint, network, command.env_override.network_retries)
.map_or(consensus_version, |network_version| consensus_version.max(network_version));
let remote_programs =
validate_upgrade_tasks(&endpoint, network, &local, &skipped, validation_consensus_version, command)?;
let config = Some(Config {
address: address.to_string(),
network: network.to_string(),
endpoint: Some(endpoint.clone()),
consensus_version: Some(consensus_version as u8),
});
print_deployment_plan(
&private_key,
&address,
&endpoint,
&network,
&local,
&skipped,
&remote,
&check_tasks_for_warnings(&endpoint, network, &local, &remote_programs, consensus_version, command),
consensus_version,
&command.into(),
);
if !confirm("Do you want to proceed with upgrade?", command.extra.yes)? {
println!("❌ Upgrade aborted.");
return Ok(DeployOutput::default());
}
let rng = &mut rand::rng();
let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
let mut programs_and_editions = Vec::with_capacity(program_ids.len());
for id in &program_ids {
let Ok(program) = leo_package::CompilationUnit::fetch(
Symbol::intern(&id.name().to_string()),
None,
context.home()?,
network,
&endpoint,
true,
command.env_override.network_retries,
) else {
warn_and_confirm(&format!("Failed to fetch program {id} from the network."), command.extra.yes)?;
continue;
};
let ProgramData::Bytecode(bytecode) = program.data else {
panic!("Expected bytecode when fetching a remote program");
};
let bytecode = Program::<N>::from_str(&bytecode)
.map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
let edition = program.edition.expect("Edition should be set after successful fetch");
programs_and_editions.push((bytecode, edition));
}
check_edition_constructor_requirements(&programs_and_editions, consensus_version, "upgrade")?;
vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
println!("Loaded the following programs into the VM:");
for program_id in vm.process().program_ids() {
let edition = *vm.process().get_stack(program_id)?.program_edition();
if program_id.to_string() == "credits.aleo" {
println!(" - credits.aleo (default)");
} else {
println!(" - {program_id} (edition {edition})");
}
}
println!();
let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
endpoint
.parse::<Uri>()
.map_err(|e| crate::errors::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
);
let mut transactions = Vec::new();
let mut all_stats = Vec::new();
let mut all_broadcasts = Vec::new();
for Task { id, program, priority_fee, record, bytecode_size, .. } in local {
if !skipped.contains(&id) {
let (transaction, stats) = if command.skip_deploy_certificate {
println!("⚠️ Skipping deployment certificate generation as per user request.\n");
let edition = *vm.process().get_stack(id)?.program_edition() + 1;
println!("edition for deployed program: {}", edition);
deploy_with_placeholder_certificate::<N, A, _>(
&vm,
&private_key,
&program,
edition,
record,
priority_fee,
bytecode_size,
consensus_version,
&query,
command.env_override.network_retries,
rng,
)?
} else {
println!("📦 Creating deployment transaction for '{}'...\n", id.to_string().bold());
let transaction = vm
.deploy(&private_key, &program, record, priority_fee.unwrap_or(0), Some(&query), rng)
.map_err(|e| crate::errors::custom(format!("Failed to generate deployment transaction: {e}")))?;
let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
vm.process().lock().add_program(&program)?;
let stats =
compute_deployment_stats(&vm, deployment, priority_fee, consensus_version, bytecode_size, rng)?;
validate_deployment_limits(deployment, &id, &network)?;
(transaction, stats)
};
print_deployment_summary(&id.to_string(), &stats);
if command.action.broadcast {
warn_if_transaction_oversized(&id, &transaction, consensus_version, "upgrade");
}
transactions.push((id, transaction));
all_stats.push(stats);
}
if let Err(e) = vm.process().lock().add_program(&program) {
warn_and_confirm(&format!("Failed to add program {id} to the VM. Error: {e}"), command.extra.yes)?;
}
}
if command.action.print {
for (program_name, transaction) in transactions.iter() {
let transaction_json = serde_json::to_string_pretty(transaction)
.map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
println!("🖨️ Printing deployment for {program_name}\n{transaction_json}")
}
}
if let Some(path) = &command.action.save {
std::fs::create_dir_all(path).map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
for (program_name, transaction) in transactions.iter() {
let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
println!("💾 Saving deployment for {program_name} at {}", file_path.display());
let transaction_json = serde_json::to_string_pretty(transaction)
.map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
std::fs::write(file_path, transaction_json)
.map_err(|e| crate::errors::custom(format!("Failed to write transaction to file: {e}")))?;
}
}
if command.action.broadcast {
for (i, (program_id, transaction)) in transactions.iter().enumerate() {
println!("📡 Broadcasting upgrade for {program_id}...");
let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
if !confirm_fee(&fee, &private_key, &address, &endpoint, network, &context, command.extra.yes)? {
println!("⏩ Upgrade skipped.");
continue;
}
let fee_id = fee.id().to_string();
let fee_transaction_id = Transaction::from_fee(fee.clone())?.id().to_string();
let id = transaction.id().to_string();
let height_before =
check_transaction::current_height(&endpoint, network, command.env_override.network_retries)?;
let (message, status) = handle_broadcast(
&format!("{endpoint}/{network}/transaction/broadcast"),
transaction,
&program_id.to_string(),
)?;
let fail_and_prompt = |msg| {
println!("❌ Failed to upgrade program {program_id}: {msg}.");
let count = transactions.len() - i - 1;
if count > 0 {
confirm("Do you want to continue with the next upgrade?", command.extra.yes)
} else {
Ok(false)
}
};
match status {
200..=299 => {
let tx_status = check_transaction::check_transaction_with_message(
&id,
Some(&fee_id),
&endpoint,
network,
height_before + 1,
command.extra.max_wait,
command.extra.blocks_to_check,
command.env_override.network_retries,
)?;
let confirmed = tx_status == Some(TransactionStatus::Accepted);
if confirmed {
println!("✅ Upgrade confirmed!");
} else if fail_and_prompt("could not find the transaction on the network")? {
continue;
} else {
return Ok(build_deploy_output(config.clone(), &transactions, &all_stats, &all_broadcasts));
}
all_broadcasts.push(BroadcastStats {
fee_id: fee_id.clone(),
fee_transaction_id: fee_transaction_id.clone(),
confirmed,
});
}
_ => {
if fail_and_prompt(&message)? {
continue;
} else {
return Ok(build_deploy_output(config.clone(), &transactions, &all_stats, &all_broadcasts));
}
}
}
}
}
Ok(build_deploy_output(config, &transactions, &all_stats, &all_broadcasts))
}
fn validate_upgrade_tasks<N: Network>(
endpoint: &str,
network: NetworkName,
tasks: &[Task<N>],
skipped: &HashSet<ProgramID<N>>,
consensus_version: ConsensusVersion,
command: &LeoUpgrade,
) -> Result<Vec<(ProgramID<N>, Program<N>)>> {
let mut remote_programs = Vec::with_capacity(tasks.len());
for Task { id, program, is_local, .. } in tasks {
if !is_local || skipped.contains(id) {
continue;
}
let Ok(remote_program) =
fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries)
else {
continue;
};
let Ok(remote_program) = Program::<N>::from_str(&remote_program) else {
continue;
};
reject_invalid_upgrade(id, &remote_program, program, consensus_version)?;
remote_programs.push((*id, remote_program));
}
Ok(remote_programs)
}
fn get_endpoint_consensus_version(
endpoint: &str,
network: NetworkName,
network_retries: u32,
) -> Option<ConsensusVersion> {
let response = leo_package::fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries)
.ok()?
.parse::<u8>()
.ok()?;
number_to_consensus_version(response as usize).ok()
}
fn reject_invalid_upgrade<N: Network>(
id: &ProgramID<N>,
remote_program: &Program<N>,
new_program: &Program<N>,
consensus_version: ConsensusVersion,
) -> Result<()> {
if !remote_program.contains_constructor() {
return Ok(());
}
Stack::check_upgrade_is_valid(remote_program, new_program)
.and_then(|_| {
if consensus_version >= ConsensusVersion::V10 {
snarkvm::synthesizer::vm::check_output_register_indices_unchanged(remote_program, new_program)
} else {
Ok(())
}
})
.map_err(|e| {
crate::errors::custom(format!("program '{id}' is not a valid upgrade: {e}"))
.with_help(
"Try preserving the original interface and output registers, adding a new function for the changed \
interface, or deploying a new program.",
)
.into()
})
}
fn check_tasks_for_warnings<N: Network>(
endpoint: &str,
network: NetworkName,
tasks: &[Task<N>],
remote_programs: &[(ProgramID<N>, Program<N>)],
consensus_version: ConsensusVersion,
command: &LeoUpgrade,
) -> Vec<String> {
let mut warnings = Vec::new();
for Task { id, program, is_local, bytecode_size, .. } in tasks {
if !is_local || !command.action.broadcast {
continue;
}
if let Some((_, remote_program)) = remote_programs.iter().find(|(remote_id, _)| remote_id == id) {
push_remote_upgrade_warnings(id, remote_program, program, consensus_version, &mut warnings);
} else if let Ok(remote_program) =
fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries)
{
let remote_program = match Program::<N>::from_str(&remote_program) {
Ok(program) => program,
Err(e) => {
warnings.push(format!("Could not parse '{id}' from the network. Error: {e}",));
continue;
}
};
push_remote_upgrade_warnings(id, &remote_program, program, consensus_version, &mut warnings);
} else {
warnings.push(format!("The program '{id}' does not exist on the network. The upgrade will likely fail.",));
}
if consensus_version >= ConsensusVersion::V7
&& let Err(e) = program.check_program_naming_structure()
{
warnings.push(format!(
"The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
));
}
if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
warnings.push(format!(
"The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
consensus_version as u8
));
}
if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The upgrade will likely fail"));
}
if consensus_version < ConsensusVersion::V15 && program.contains_v15_syntax() {
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"));
}
if consensus_version < ConsensusVersion::V16 && program.contains_v16_syntax() {
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"));
}
if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
warnings.push(format!("The program '{id}' does not contain a constructor. The upgrade will likely fail",));
}
let max_size = max_program_size_for_consensus_version::<N>(consensus_version);
if *bytecode_size > max_size {
warnings.push(format!(
"The program '{id}' is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The upgrade will likely fail.",
*bytecode_size as f64 / 1024.0,
max_size as f64 / 1024.0,
consensus_version as u8,
));
} else if let (_, _, Some(msg)) = format_program_size(*bytecode_size, max_size) {
warnings.push(format!("The program '{id}' is {msg}."));
}
if let Err(e) =
check_consensus_version_mismatch(consensus_version, endpoint, network, command.env_override.network_retries)
{
warnings.push(format!("{e}. In some cases, the deployment may fail"));
}
}
warnings
}
fn push_remote_upgrade_warnings<N: Network>(
id: &ProgramID<N>,
remote_program: &Program<N>,
program: &Program<N>,
consensus_version: ConsensusVersion,
warnings: &mut Vec<String>,
) {
if remote_program.contains_constructor() {
if let Err(e) = reject_invalid_upgrade(id, remote_program, program, consensus_version) {
warnings.push(e.to_string());
}
} else if consensus_version >= ConsensusVersion::V8 {
warnings.push(format!("The program '{id}' can only ever be upgraded once and its contents cannot be changed. Otherwise, the upgrade will likely fail."));
} else {
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."));
}
}
impl From<&LeoUpgrade> for LeoDeploy {
fn from(upgrade: &LeoUpgrade) -> Self {
Self {
fee_options: upgrade.fee_options.clone(),
action: upgrade.action.clone(),
env_override: upgrade.env_override.clone(),
key_override: upgrade.key_override.clone(),
consensus_override: upgrade.consensus_override.clone(),
extra: upgrade.extra.clone(),
skip: upgrade.skip.clone(),
rename: None,
build_options: upgrade.build_options.clone(),
skip_deploy_certificate: upgrade.skip_deploy_certificate,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm::prelude::TestnetV0;
fn parse_program(source: &str) -> Program<TestnetV0> {
Program::<TestnetV0>::from_str(source).unwrap()
}
#[test]
fn rejects_upgrade_that_changes_record_output_register() {
let original = parse_program(
r"program upgrade_check.aleo;
record Token:
owner as address.private;
amount as u64.private;
function mint:
input r0 as address.private;
input r1 as u64.private;
cast r0 r1 into r2 as Token.record;
output r2 as Token.record;
constructor:
assert.eq edition 0u16;
",
);
let changed_output_register = parse_program(
r"program upgrade_check.aleo;
record Token:
owner as address.private;
amount as u64.private;
function mint:
input r0 as address.private;
input r1 as u64.private;
add r1 0u64 into r2;
cast r0 r2 into r3 as Token.record;
output r3 as Token.record;
constructor:
assert.eq edition 0u16;
",
);
let id = original.id();
let error = reject_invalid_upgrade(id, &original, &changed_output_register, ConsensusVersion::V10)
.unwrap_err()
.to_string();
assert!(error.contains("not a valid upgrade"));
assert!(error.contains("output register"));
}
#[test]
fn accepts_upgrade_that_preserves_record_output_register() {
let original = parse_program(
r"program upgrade_check.aleo;
record Token:
owner as address.private;
amount as u64.private;
function mint:
input r0 as address.private;
input r1 as u64.private;
cast r0 r1 into r2 as Token.record;
output r2 as Token.record;
constructor:
assert.eq edition 0u16;
",
);
let same_interface = parse_program(
r"program upgrade_check.aleo;
record Token:
owner as address.private;
amount as u64.private;
function mint:
input r0 as address.private;
input r1 as u64.private;
add r1 1u64 into r3;
cast r0 r1 into r2 as Token.record;
output r2 as Token.record;
constructor:
assert.eq edition 0u16;
",
);
assert!(reject_invalid_upgrade(original.id(), &original, &same_interface, ConsensusVersion::V10).is_ok());
}
#[test]
fn warns_for_upgrade_that_changes_record_output_register() {
let original = parse_program(
r"program upgrade_check.aleo;
record Token:
owner as address.private;
amount as u64.private;
function mint:
input r0 as address.private;
input r1 as u64.private;
cast r0 r1 into r2 as Token.record;
output r2 as Token.record;
constructor:
assert.eq edition 0u16;
",
);
let changed_output_register = parse_program(
r"program upgrade_check.aleo;
record Token:
owner as address.private;
amount as u64.private;
function mint:
input r0 as address.private;
input r1 as u64.private;
add r1 0u64 into r2;
cast r0 r2 into r3 as Token.record;
output r3 as Token.record;
constructor:
assert.eq edition 0u16;
",
);
let mut warnings = Vec::new();
push_remote_upgrade_warnings(
original.id(),
&original,
&changed_output_register,
ConsensusVersion::V10,
&mut warnings,
);
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("not a valid upgrade"));
assert!(warnings[0].contains("Try preserving the original interface and output registers"));
}
}