Skip to content

Preflight invalid program upgrades#29505

Draft
Kuhai9801 wants to merge 11 commits into
ProvableHQ:masterfrom
Kuhai9801:upgrade-invalid-preflight
Draft

Preflight invalid program upgrades#29505
Kuhai9801 wants to merge 11 commits into
ProvableHQ:masterfrom
Kuhai9801:upgrade-invalid-preflight

Conversation

@Kuhai9801

Copy link
Copy Markdown
Contributor

Motivation

Fixes #29040.

leo upgrade already checks whether a local program is a valid upgrade, but that result is only surfaced as a warning. For upgrades that can be proven invalid before transaction generation, the CLI should stop before showing the deployment plan and confirmation prompt.

This adds a hard preflight for local, non-skipped upgrade targets and catches V10+ record output register changes with the same snarkVM helper used by verification.

Test Plan

Added regression coverage for rejecting an upgrade that changes a record output register and accepting a logic-only change that preserves the record output register.

Related PRs

N/A

@Kuhai9801 Kuhai9801 force-pushed the upgrade-invalid-preflight branch 2 times, most recently from 7da8c95 to cefab61 Compare June 9, 2026 04:15
@Kuhai9801 Kuhai9801 force-pushed the upgrade-invalid-preflight branch from cefab61 to 3757b06 Compare June 9, 2026 04:28
@Kuhai9801 Kuhai9801 marked this pull request as ready for review June 9, 2026 06:41
@Kuhai9801 Kuhai9801 marked this pull request as draft June 9, 2026 10:21
@Kuhai9801 Kuhai9801 marked this pull request as ready for review June 9, 2026 10:34
let mut remote_programs = Vec::new();

for Task { id, program, is_local, .. } in tasks {
if !is_local || skipped.contains(id) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warnings loop still gates on !command.action.broadcast, but this preflight runs for every upgrade mode. Should a non-broadcast (save-to-file / offline) upgrade hard-fail here too, or only the broadcast path?

}

let remote_program =
fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously a failed fetch was just a warning (does not exist on the network), which also covered transient network errors and not-yet-deployed programs. Here it hard-aborts the command before the plan. Is that intended, or should a fetch failure stay a warning and reserve the hard error for a proven-invalid upgrade?

Comment thread crates/leo/src/cli/commands/upgrade.rs Outdated
consensus_version: ConsensusVersion,
command: &LeoUpgrade,
) -> Result<Vec<(ProgramID<N>, Program<N>)>> {
let mut remote_programs = Vec::new();

@mohammadfawaz mohammadfawaz Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Vec::with_capacity(tasks.len()) here since the size is bounded by the task count.

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)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hard check uses max(local, network) for the consensus version while check_tasks_for_warnings uses the plain local consensus_version. Is the asymmetry intentional (fail-closed)? A one-line comment on the rationale would help.

@Kuhai9801

Copy link
Copy Markdown
Contributor Author

Thanks, agreed on fetch failures. I updated this so fetch/parse failures stay in the warning path; only a fetched program that is proven invalid fails early.

Also added the capacity hint and consensus-version comment.

For non-broadcast, do you prefer proven-invalid upgrades to fail for --save / --print too, or should hard preflight be broadcast-only?

@mohammadfawaz

mohammadfawaz commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Non-broadcast: fail in all modes. Proven-invalid depends only on the program pair, not on how the tx leaves the CLI, so a saved/printed tx will just get rejected on broadcast later. Keep the !broadcast gate for the network checks (not-found, transient fetch) as warnings, but make the structural check a hard error everywhere.

Also, the new consensus-version comment explains the max() but not the asymmetry I flagged: preflight uses max(local, network), warnings use plain local. A line saying preflight is intentionally fail-closed (so it can hard-error) would cover it.

Rest looks good.

@Kuhai9801 Kuhai9801 marked this pull request as draft June 9, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error out on illegal upgrade when a record output register changes

2 participants