From 33571fb7cb407d86a3aa4f6ab76b1db5dd090392 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" <165563006+Kuhai9801@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:29:32 +0800 Subject: [PATCH 1/3] Preflight invalid program upgrades --- crates/leo/src/cli/commands/upgrade.rs | 267 +++++++++++++++++++-- documentation/leo_by_example/battleship.md | 2 +- 2 files changed, 254 insertions(+), 15 deletions(-) diff --git a/crates/leo/src/cli/commands/upgrade.rs b/crates/leo/src/cli/commands/upgrade.rs index d1b4a2bdcda..ce2792fb109 100644 --- a/crates/leo/src/cli/commands/upgrade.rs +++ b/crates/leo/src/cli/commands/upgrade.rs @@ -227,6 +227,13 @@ fn handle_upgrade>( command.env_override.network_retries, )?; + // Use the stricter active endpoint rules for proven-invalid upgrades; fetch uncertainty remains a warning below. + 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)?; + // Build the config for JSON output. let config = Some(Config { address: address.to_string(), @@ -244,7 +251,7 @@ fn handle_upgrade>( &local, &skipped, &remote, - &check_tasks_for_warnings(&endpoint, network, &local, consensus_version, command), + &check_tasks_for_warnings(&endpoint, network, &local, &remote_programs, consensus_version, command), consensus_version, &command.into(), ); @@ -467,6 +474,77 @@ fn handle_upgrade>( Ok(build_deploy_output(config, &transactions, &all_stats, &all_broadcasts)) } +fn validate_upgrade_tasks( + endpoint: &str, + network: NetworkName, + tasks: &[Task], + skipped: &HashSet>, + consensus_version: ConsensusVersion, + command: &LeoUpgrade, +) -> Result, Program)>> { + 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::::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 { + let response = leo_package::fetch_from_network(&format!("{endpoint}/{network}/consensus_version"), network_retries) + .ok()? + .parse::() + .ok()?; + + number_to_consensus_version(response as usize).ok() +} + +fn reject_invalid_upgrade( + id: &ProgramID, + remote_program: &Program, + new_program: &Program, + 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!( + "The program '{id}' is not a valid upgrade: {e}\n\n\ + Try preserving the original interface and output registers, adding a new function for the changed \ + interface, or deploying a new program." + )) + .into() + }) +} + /// Check the tasks to warn the user about any potential issues. /// The following properties are checked: /// - If the transaction is to be broadcast: @@ -477,6 +555,7 @@ fn check_tasks_for_warnings( endpoint: &str, network: NetworkName, tasks: &[Task], + remote_programs: &[(ProgramID, Program)], consensus_version: ConsensusVersion, command: &LeoUpgrade, ) -> Vec { @@ -487,7 +566,9 @@ fn check_tasks_for_warnings( } // Check if the program exists on the network. - if let Ok(remote_program) = + 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) { // Parse the program. @@ -498,18 +579,7 @@ fn check_tasks_for_warnings( continue; } }; - // Check if the program is a valid upgrade. - if remote_program.contains_constructor() { - if let Err(e) = Stack::check_upgrade_is_valid(&remote_program, program) { - warnings.push(format!( - "The program '{id}' is not a valid upgrade. The upgrade will likely fail. Error: {e}", - )); - } - } 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.")); - } + 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.",)); } @@ -550,6 +620,25 @@ fn check_tasks_for_warnings( warnings } +fn push_remote_upgrade_warnings( + id: &ProgramID, + remote_program: &Program, + program: &Program, + consensus_version: ConsensusVersion, + warnings: &mut Vec, +) { + // Check if the program is a valid upgrade. + if remote_program.contains_constructor() { + if let Err(e) = reject_invalid_upgrade(id, remote_program, program, consensus_version) { + warnings.push(format!("{e}\n\nThe upgrade will likely fail.")); + } + } 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.")); + } +} + // Convert the `LeoUpgrade` into a `LeoDeploy` command. impl From<&LeoUpgrade> for LeoDeploy { fn from(upgrade: &LeoUpgrade) -> Self { @@ -565,3 +654,153 @@ impl From<&LeoUpgrade> for LeoDeploy { } } } + +#[cfg(test)] +mod tests { + use super::*; + use snarkvm::prelude::TestnetV0; + + fn parse_program(source: &str) -> Program { + Program::::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("mismatched record output registers")); + assert!(warnings[0].contains("Try preserving the original interface and output registers")); + } +} diff --git a/documentation/leo_by_example/battleship.md b/documentation/leo_by_example/battleship.md index 88c12a24370..86954583f3d 100644 --- a/documentation/leo_by_example/battleship.md +++ b/documentation/leo_by_example/battleship.md @@ -664,7 +664,7 @@ If a ship is valid vertically or horizontally, then we know the ship is valid. W ### Bit Counting -See the "c_bitcount" closure to follow along with the code. 50 years ago, MIT AI Laboratory published HAKMEM, which was a series of tricks and hacks to speed up processing for bitwise operations. We turned to HAKMEM 169 for bitcounting inspiration, although we've tweaked our implementation to be (hopefully) easier to understand. Before diving into details, let's build some intuition. +See the "c_bitcount" closure to follow along with the code. 50 years ago, MIT AI Laboratory published HAKMEM, which was a series of tricks and hacks to speed up processing for bitwise operations. We turned to HAKMEM 169 for bitcounting inspiration, although we've tweaked our implementation to be (hopefully) easier to understand. Before diving into details, let's build some intuition. Let a,b,c,d be either 0 or 1. Given a polynomial 8a + 4b + 2c + d, how do we find the summation of a + b + c + d? If we subtract subsets of this polynomial, we'll be left with the summation. From 098237aed408d1780dfae4a6c47b0e29949ed45b Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" <165563006+Kuhai9801@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:23:25 +0800 Subject: [PATCH 2/3] Refine upgrade preflight diagnostics --- crates/leo/src/cli/commands/upgrade.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/leo/src/cli/commands/upgrade.rs b/crates/leo/src/cli/commands/upgrade.rs index ce2792fb109..bdd9c4dcc5f 100644 --- a/crates/leo/src/cli/commands/upgrade.rs +++ b/crates/leo/src/cli/commands/upgrade.rs @@ -227,7 +227,7 @@ fn handle_upgrade>( command.env_override.network_retries, )?; - // Use the stricter active endpoint rules for proven-invalid upgrades; fetch uncertainty remains a warning below. + // Fail closed for proven-invalid upgrades by using the stricter active endpoint rules; fetch uncertainty remains a warning below. 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)); @@ -536,12 +536,12 @@ fn reject_invalid_upgrade( } }) .map_err(|e| { - crate::errors::custom(format!( - "The program '{id}' is not a valid upgrade: {e}\n\n\ - Try preserving the original interface and output registers, adding a new function for the changed \ - interface, or deploying a new program." - )) - .into() + 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() }) } @@ -630,7 +630,7 @@ fn push_remote_upgrade_warnings( // Check if the program is a valid upgrade. if remote_program.contains_constructor() { if let Err(e) = reject_invalid_upgrade(id, remote_program, program, consensus_version) { - warnings.push(format!("{e}\n\nThe upgrade will likely fail.")); + 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.")); @@ -800,7 +800,7 @@ constructor: ); assert_eq!(warnings.len(), 1); - assert!(warnings[0].contains("mismatched record output registers")); + assert!(warnings[0].contains("not a valid upgrade")); assert!(warnings[0].contains("Try preserving the original interface and output registers")); } } From 5e4111c478c2e2b1a60056ad7bd38b6494213bb5 Mon Sep 17 00:00:00 2001 From: "Cyne Jarvis J. Zarceno" <165563006+Kuhai9801@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:51:51 +0800 Subject: [PATCH 3/3] Clarify upgrade preflight handling --- crates/leo/src/cli/commands/upgrade.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/leo/src/cli/commands/upgrade.rs b/crates/leo/src/cli/commands/upgrade.rs index bdd9c4dcc5f..18d5a02423c 100644 --- a/crates/leo/src/cli/commands/upgrade.rs +++ b/crates/leo/src/cli/commands/upgrade.rs @@ -485,6 +485,7 @@ fn validate_upgrade_tasks( let mut remote_programs = Vec::with_capacity(tasks.len()); for Task { id, program, is_local, .. } in tasks { + // A proven-invalid upgrade is rejected before transaction construction in every output mode. if !is_local || skipped.contains(id) { continue; } @@ -492,6 +493,7 @@ fn validate_upgrade_tasks( let Ok(remote_program) = fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries) else { + // Fetch uncertainty remains in `check_tasks_for_warnings` so transient network state does not block planning. continue; }; let Ok(remote_program) = Program::::from_str(&remote_program) else {