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 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#[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 let network = get_network(&self.env_override.network)?;
128 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
147fn 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 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 let endpoint = get_endpoint(&command.env_override.endpoint)?;
165
166 let is_devnet = get_is_devnet(command.consensus_override.devnet);
168
169 let consensus_heights = command
171 .consensus_override
172 .consensus_heights
173 .clone()
174 .unwrap_or_else(|| get_consensus_heights(network, is_devnet));
175 validate_consensus_heights(&consensus_heights)
177 .map_err(|e| crate::errors::custom(format!("Invalid consensus heights: {e}")))?;
178 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 #[allow(unsafe_code)]
186 unsafe {
187 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
194 }
195
196 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 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 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 let program_ids = tasks.iter().map(|task| task.id).collect::<Vec<_>>();
245
246 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
248
249 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 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 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 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_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 if !confirm("Do you want to proceed with upgrade?", command.extra.yes)? {
299 println!("ā Upgrade aborted.");
300 return Ok(DeployOutput::default());
301 }
302
303 let rng = &mut rand::rng();
305
306 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
308
309 let mut programs_and_editions = Vec::with_capacity(program_ids.len());
311 for id in &program_ids {
312 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 let bytecode = Program::<N>::from_str(&bytecode)
330 .map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
331 let edition = program.edition.expect("Edition should be set after successful fetch");
333 programs_and_editions.push((bytecode, edition));
334 }
335
336 check_edition_constructor_requirements(&programs_and_editions, consensus_version, "upgrade")?;
338
339 vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
340
341 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 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 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 !skipped.contains(&id) {
367 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 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 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 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
403 vm.process().lock().add_program(&program)?;
405 let stats =
407 compute_deployment_stats(&vm, deployment, priority_fee, consensus_version, bytecode_size, rng)?;
408 validate_deployment_limits(deployment, &id, &network, consensus_version)?;
410 (transaction, stats)
411 };
412
413 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 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 command.action.print {
430 for (program_name, transaction) in transactions.iter() {
431 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 let Some(path) = &command.action.save {
442 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 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 command.action.broadcast {
457 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
458 println!("š” Broadcasting upgrade for {program_id}...");
459 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 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 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 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 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
601fn 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 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 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 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 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 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 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 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 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 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 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 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
711impl 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}