1use 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#[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 let network = get_network(&self.env_override.network)?;
86 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
105fn handle_upgrade<N: Network>(
107 command: &LeoUpgrade,
108 context: Context,
109 network: NetworkName,
110 package: Package,
111) -> Result<<LeoDeploy as Command>::Output> {
112 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 let endpoint = get_endpoint(&command.env_override.endpoint)?;
119
120 let is_devnet = get_is_devnet(command.env_override.devnet);
122
123 let consensus_heights =
125 command.env_override.consensus_heights.clone().unwrap_or_else(|| get_consensus_heights(network, is_devnet));
126 validate_consensus_heights(&consensus_heights)
128 .map_err(|e| CliError::custom(format!("Invalid consensus heights: {e}")))?;
129 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 #[allow(unsafe_code)]
137 unsafe {
138 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
145 }
146
147 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 let aleo_name = format!("{}.aleo", program.name);
158 let aleo_path = if package.manifest.program == aleo_name {
159 package.build_directory().join("main.aleo")
162 } else {
163 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 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 let program_ids = tasks.iter().map(|task| task.id).collect::<Vec<_>>();
202
203 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
205
206 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 let consensus_version =
217 get_consensus_version(&command.extra.consensus_version, &endpoint, network, &consensus_heights, &context)?;
218
219 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 if !confirm("Do you want to proceed with upgrade?", command.extra.yes)? {
235 println!("ā Upgrade aborted.");
236 return Ok(());
237 }
238
239 let rng = &mut rand::thread_rng();
241
242 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
244
245 let programs_and_editions = program_ids
247 .iter()
248 .map(|id| {
249 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 let bytecode = Program::<N>::from_str(&bytecode)
263 .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
264 let edition = program.edition.expect("Edition should be set after successful fetch");
267 Ok((bytecode, edition))
268 })
269 .collect::<Result<Vec<_>>>()?;
270
271 check_edition_constructor_requirements(&programs_and_editions, consensus_version, "upgrade")?;
273
274 vm.process().write().add_programs_with_editions(&programs_and_editions)?;
275
276 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 let re = regex::Regex::new(r"v\d+$").unwrap();
290 let query_endpoint = re.replace(&endpoint, "").to_string();
291
292 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 let mut transactions = Vec::new();
301 for Task { id, program, priority_fee, record, bytecode_size, .. } in local {
302 if !skipped.contains(&id) {
304 println!("š¦ Creating deployment transaction for '{}'...\n", id.to_string().bold());
305 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 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
311 print_deployment_stats(&vm, &id.to_string(), deployment, priority_fee, consensus_version, bytecode_size)?;
313 validate_deployment_limits(deployment, &id, &network)?;
315 transactions.push((id, transaction));
317 }
318 vm.process().write().add_program(&program)?;
320 }
321
322 if command.action.print {
325 for (program_name, transaction) in transactions.iter() {
326 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 let Some(path) = &command.action.save {
337 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 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 command.action.broadcast {
352 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
353 println!("š” Broadcasting upgrade for {program_id}...");
354 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 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 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
414fn 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 if let Ok(remote_program) = fetch_program_from_network(&id.to_string(), endpoint, network) {
435 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 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 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 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 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 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 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
489impl 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}