1use super::*;
18
19use check_transaction::TransactionStatus;
20use leo_ast::NetworkName;
21use leo_package::{Package, ProgramData, fetch_program_from_network, retry_network_call};
22
23use aleo_std::StorageMode;
24use rand::CryptoRng;
25#[cfg(not(feature = "only_testnet"))]
26use snarkvm::prelude::{CanaryV0, MainnetV0};
27use snarkvm::{
28 circuit::{Aleo, AleoTestnetV0},
29 ledger::{
30 query::{Query as SnarkVMQuery, QueryTrait},
31 store::helpers::memory::BlockMemory,
32 },
33 prelude::{
34 Certificate,
35 ConsensusVersion,
36 Deployment,
37 Fee,
38 Program,
39 ProgramID,
40 ProgramOwner,
41 Rng,
42 Stack,
43 TestnetV0,
44 ToBytes,
45 Transaction,
46 VM,
47 VerifyingKey,
48 check_program_plaintext_sizes,
49 deployment_cost,
50 execution_cost_for_authorization,
51 minimum_cost_in_microcredits_v1,
52 minimum_cost_in_microcredits_v2,
53 minimum_cost_in_microcredits_v3,
54 store::{ConsensusStore, helpers::memory::ConsensusMemory},
55 },
56 synthesizer::program::StackTrait,
57};
58
59use colored::*;
60use itertools::Itertools;
61use std::{
62 collections::{HashMap, HashSet},
63 fs,
64 path::PathBuf,
65};
66
67#[derive(Parser, Debug)]
69pub struct LeoDeploy {
70 #[clap(flatten)]
71 pub(crate) fee_options: FeeOptions,
72 #[clap(flatten)]
73 pub(crate) action: TransactionAction,
74 #[clap(flatten)]
75 pub(crate) env_override: EnvOptions,
76 #[clap(flatten)]
77 pub(crate) key_override: PrivateKeyOptions,
78 #[clap(flatten)]
79 pub(crate) consensus_override: ConsensusOptions,
80 #[clap(flatten)]
81 pub(crate) extra: ExtraOptions,
82 #[clap(long, help = "Skips deployment of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
83 pub(crate) skip: Vec<String>,
84 #[clap(
85 long,
86 help = "Deploy the program under a different name, producing a genuinely distinct on-chain deployment. Programs importing the original name are not redirected to the renamed copy."
87 )]
88 pub(crate) rename: Option<String>,
89 #[clap(flatten)]
90 pub(crate) build_options: BuildOptions,
91 #[clap(
92 long,
93 value_name = "DIR",
94 help = "Directory containing local .aleo imports when --path points to an Aleo bytecode file; other imports are fetched from the network"
95 )]
96 pub(crate) imports_dir: Option<PathBuf>,
97 #[clap(long, help = "Use placeholder certificate and verifying keys during deployment.", default_value = "false")]
98 pub(crate) skip_deploy_certificate: bool,
99}
100
101pub struct Task<N: Network> {
102 pub id: ProgramID<N>,
103 pub program: Program<N>,
104 pub edition: Option<u16>,
105 pub is_local: bool,
106 pub priority_fee: Option<u64>,
107 pub record: Option<Record<N, Plaintext<N>>>,
108 pub bytecode_size: usize,
109}
110
111type PreparedTasks<N> = (Vec<Task<N>>, Vec<Task<N>>, HashSet<ProgramID<N>>);
113
114type DeployTransactions<N> = (Vec<(ProgramID<N>, Transaction<N>)>, Vec<DeploymentStats>);
116
117type MemberTasks<N> = (String, Vec<Task<N>>, HashSet<ProgramID<N>>);
119
120struct DeploySetup<N: Network> {
122 private_key: PrivateKey<N>,
123 address: Address<N>,
124 endpoint: String,
125 consensus_version: ConsensusVersion,
126 network: NetworkName,
127 vm: VM<N, ConsensusMemory<N>>,
128 query: SnarkVMQuery<N, BlockMemory<N>>,
129}
130
131impl Command for LeoDeploy {
132 type Input = Package;
133 type Output = DeployOutput;
134
135 fn log_span(&self) -> Span {
136 tracing::span!(tracing::Level::INFO, "Leo")
137 }
138
139 fn prelude(&self, context: Context) -> Result<Self::Input> {
140 let path = context.dir()?;
141 if path.extension().and_then(|extension| extension.to_str()) == Some("aleo") {
142 if context.package_filter.is_some() {
143 return Err(crate::errors::custom("`--package` cannot be used with an Aleo bytecode file.").into());
144 }
145 if self.rename.is_some() {
146 return Err(crate::errors::custom("`--rename` is not supported when deploying Aleo bytecode.").into());
147 }
148 let home_path = context.home()?;
149 let network = get_network(&self.env_override.network)?;
150 let endpoint = get_endpoint(&self.env_override.endpoint)?;
151 return Package::from_aleo_file(
152 path,
153 home_path,
154 self.imports_dir.as_deref(),
155 true,
156 self.build_options.no_local,
157 Some(network),
158 Some(&endpoint),
159 self.env_override.network_retries,
160 );
161 }
162 if self.imports_dir.is_some() {
163 return Err(
164 crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
165 );
166 }
167
168 LeoBuild {
169 env_override: self.env_override.clone(),
170 options: {
171 let mut options = self.build_options.clone();
172 options.no_cache = true;
173 options
174 },
175 rename: self.rename.clone(),
176 }
177 .execute(context)
178 }
179
180 fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
181 if input.compilation_units.last().is_some_and(|p| p.kind.is_library()) {
183 return Err(crate::errors::custom("Cannot deploy a library package. Only programs can be deployed.").into());
184 }
185
186 let network = get_network(&self.env_override.network)?;
188 match network {
190 NetworkName::TestnetV0 => handle_deploy::<TestnetV0, AleoTestnetV0>(&self, context, network, input),
191 NetworkName::MainnetV0 => {
192 #[cfg(feature = "only_testnet")]
193 panic!("Mainnet chosen with only_testnet feature");
194 #[cfg(not(feature = "only_testnet"))]
195 handle_deploy::<MainnetV0, snarkvm::circuit::AleoV0>(&self, context, network, input)
196 }
197 NetworkName::CanaryV0 => {
198 #[cfg(feature = "only_testnet")]
199 panic!("Canary chosen with only_testnet feature");
200 #[cfg(not(feature = "only_testnet"))]
201 handle_deploy::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(&self, context, network, input)
202 }
203 }
204 }
205
206 fn execute(self, context: Context) -> Result<Self::Output> {
207 let path = context.dir()?;
209 let is_aleo_file = path.extension().and_then(|extension| extension.to_str()) == Some("aleo");
210 if self.imports_dir.is_some() && !is_aleo_file {
211 return Err(
212 crate::errors::custom("`--imports-dir` requires `--path` to point to an Aleo bytecode file.").into()
213 );
214 }
215 let workspace_targets = if is_aleo_file { None } else { context.resolve_targets()? };
216 match workspace_targets {
217 Some((_, targets)) if targets.len() > 1 => handle_workspace_deploy(self, context, targets),
218 _ => {
219 let input = self.prelude(context.clone())?;
221 let span = self.log_span();
222 let span = span.enter();
223 let out = self.apply(context, input);
224 drop(span);
225 out
226 }
227 }
228 }
229}
230
231fn create_deploy_setup<N: Network>(
235 command: &LeoDeploy,
236 context: &Context,
237 network: NetworkName,
238) -> Result<DeploySetup<N>> {
239 let private_key = get_private_key(&command.key_override.private_key)?;
241 let address =
242 Address::try_from(&private_key).map_err(|e| crate::errors::custom(format!("Failed to parse address: {e}")))?;
243
244 let endpoint = get_endpoint(&command.env_override.endpoint)?;
246
247 let is_devnet = get_is_devnet(command.consensus_override.devnet);
249
250 let consensus_heights = command
252 .consensus_override
253 .consensus_heights
254 .clone()
255 .unwrap_or_else(|| get_consensus_heights(network, is_devnet));
256 validate_consensus_heights(&consensus_heights)
258 .map_err(|e| crate::errors::custom(format!("⚠️ Invalid consensus heights: {e}")))?;
259 let consensus_heights_string = consensus_heights.iter().format(",").to_string();
261 println!(
262 "\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"
263 );
264
265 #[allow(unsafe_code)]
267 unsafe {
268 std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
275 }
276
277 let consensus_version = get_consensus_version(
279 &command.extra.consensus_version,
280 &endpoint,
281 network,
282 &consensus_heights,
283 context,
284 command.env_override.network_retries,
285 )?;
286
287 let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
289
290 let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
292 endpoint
293 .parse::<Uri>()
294 .map_err(|e| crate::errors::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
295 );
296
297 Ok(DeploySetup { private_key, address, endpoint, consensus_version, network, vm, query })
298}
299
300fn prepare_package_tasks<N: Network>(
302 command: &LeoDeploy,
303 setup: &DeploySetup<N>,
304 package: &Package,
305) -> Result<PreparedTasks<N>> {
306 let programs = package.compilation_units.iter().filter(|unit| unit.kind.is_program()).cloned();
308
309 let programs_and_bytecode: Vec<(leo_package::CompilationUnit, String)> = programs
310 .into_iter()
311 .map(|program| {
312 let bytecode = match &program.data {
313 ProgramData::Bytecode(s) => s.clone(),
314 ProgramData::SourcePath { .. } => {
315 let aleo_path = package.unit_bytecode_path(&program.name.to_string());
317 fs::read_to_string(aleo_path.clone()).map_err(|e| {
318 crate::errors::custom(format!("Failed to read file {}: {e}", aleo_path.display()))
319 })?
320 }
321 };
322
323 Ok((program, bytecode))
324 })
325 .collect::<Result<_>>()?;
326
327 let fee_options = parse_fee_options(&setup.private_key, &command.fee_options, programs_and_bytecode.len())?;
329
330 let tasks: Vec<Task<N>> = programs_and_bytecode
331 .into_iter()
332 .zip(fee_options)
333 .map(|((program, bytecode), (priority_fee, record))| {
334 let id_str = format!("{}", program.name);
335 let id = id_str
336 .parse()
337 .map_err(|e| crate::errors::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
338 let bytecode_size = bytecode.len();
339 let parsed_program =
340 bytecode.parse().map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
341 Ok(Task {
342 id,
343 program: parsed_program,
344 edition: program.edition,
345 is_local: program.is_local,
346 priority_fee,
347 record,
348 bytecode_size,
349 })
350 })
351 .collect::<Result<_>>()?;
352
353 let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
355
356 let skipped: HashSet<ProgramID<N>> = local
358 .iter()
359 .filter_map(|task| {
360 let id_string = task.id.to_string();
361 command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
362 })
363 .collect();
364
365 Ok((local, remote, skipped))
366}
367
368fn load_remote_deps<N: Network>(command: &LeoDeploy, setup: &DeploySetup<N>, remote: Vec<Task<N>>) -> Result<()> {
370 if remote.is_empty() {
371 return Ok(());
372 }
373
374 let programs_and_editions = remote
375 .into_iter()
376 .map(|task| {
377 let edition = match task.edition {
379 Some(e) => e,
380 None => leo_package::fetch_latest_edition(
381 &task.id.to_string(),
382 &setup.endpoint,
383 setup.network,
384 command.env_override.network_retries,
385 )?,
386 };
387 Ok((task.program, edition))
388 })
389 .collect::<Result<Vec<_>>>()?;
390
391 check_edition_constructor_requirements(&programs_and_editions, setup.consensus_version, "deploy")?;
393 setup.vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
394 Ok(())
395}
396
397fn generate_deploy_transactions<N: Network, A: Aleo<Network = N>>(
401 command: &LeoDeploy,
402 setup: &DeploySetup<N>,
403 local: Vec<Task<N>>,
404 skipped: &HashSet<ProgramID<N>>,
405 already_deployed: &mut HashSet<ProgramID<N>>,
406) -> Result<Option<DeployTransactions<N>>> {
407 let rng = &mut rand::rng();
408 let mut transactions = Vec::new();
409 let mut all_stats = Vec::new();
410
411 for Task { id, program, edition, priority_fee, record, bytecode_size, .. } in local {
412 if !skipped.contains(&id) && !already_deployed.contains(&id) {
414 if setup.consensus_version >= ConsensusVersion::V20 {
416 let max_bits = consensus_limit(
417 &N::MAX_PLAINTEXT_TYPE_SIZE_IN_BITS,
418 setup.consensus_version,
419 N::LATEST_MAX_PLAINTEXT_TYPE_SIZE_IN_BITS(),
420 );
421 let stack = Stack::new(setup.vm.process(), &program)?;
422 check_program_plaintext_sizes(&program, &stack, max_bits)?;
423 }
424 if let Some(constructor) = program.constructor() {
426 println!(
427 r"
428🔧 Your program '{}' has the following constructor.
429──────────────────────────────────────────────
430{constructor}
431──────────────────────────────────────────────
432Once it is deployed, it CANNOT be changed.
433",
434 id.to_string().bold()
435 );
436 if !confirm("Would you like to proceed?", command.extra.yes)? {
437 println!("❌ Deployment aborted.");
438 return Ok(None);
439 }
440 }
441 println!("📦 Creating deployment transaction for '{}'...\n", id.to_string().bold());
442 let (transaction, stats) = if command.skip_deploy_certificate {
443 println!("⚠️ Skipping deployment certificate and verifier key generation as per user request.\n");
444 deploy_with_placeholder_certificate::<N, A, _>(
445 &setup.vm,
446 &setup.private_key,
447 &program,
448 edition.unwrap_or(0),
449 record,
450 priority_fee,
451 bytecode_size,
452 setup.consensus_version,
453 &setup.query,
454 command.env_override.network_retries,
455 rng,
456 )?
457 } else {
458 let transaction = setup
460 .vm
461 .deploy(&setup.private_key, &program, record, priority_fee.unwrap_or(0), Some(&setup.query), rng)
462 .map_err(|e| crate::errors::custom(format!("Failed to generate deployment transaction: {e}")))?;
463 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
465 setup.vm.process().lock().add_program(&program)?;
467 let stats = compute_deployment_stats(
469 &setup.vm,
470 deployment,
471 priority_fee,
472 setup.consensus_version,
473 bytecode_size,
474 rng,
475 )?;
476 (transaction, stats)
477 };
478
479 print_deployment_summary(&id.to_string(), &stats);
481 if command.action.broadcast {
482 warn_if_transaction_oversized(&id, &transaction, setup.consensus_version, "deployment");
483 }
484 transactions.push((id, transaction));
485 all_stats.push(stats);
486 already_deployed.insert(id);
487
488 if !command.skip_deploy_certificate {
489 let (program_id, transaction) = transactions.last().expect("Transaction was just pushed");
491 let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
492 validate_deployment_limits(deployment, program_id, &setup.network, setup.consensus_version)?;
493 }
494 }
495
496 if let Err(e) = setup.vm.process().lock().add_program(&program) {
498 tracing::debug!("Program {id} already in VM: {e}");
499 }
500 }
501
502 Ok(Some((transactions, all_stats)))
503}
504
505fn output_and_broadcast<N: Network>(
507 command: &LeoDeploy,
508 setup: &DeploySetup<N>,
509 context: &Context,
510 config: Option<Config>,
511 transactions: &[(ProgramID<N>, Transaction<N>)],
512 all_stats: &[DeploymentStats],
513) -> Result<DeployOutput> {
514 if command.action.print {
517 for (program_name, transaction) in transactions.iter() {
518 let transaction_json = serde_json::to_string_pretty(transaction)
520 .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
521 println!("🖨️ Printing deployment for {program_name}\n{transaction_json}")
522 }
523 }
524
525 if let Some(path) = &command.action.save {
529 std::fs::create_dir_all(path).map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
531 for (program_name, transaction) in transactions.iter() {
532 let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
534 println!("💾 Saving deployment for {program_name} at {}", file_path.display());
535 let transaction_json = serde_json::to_string_pretty(transaction)
536 .map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
537 std::fs::write(file_path, transaction_json)
538 .map_err(|e| crate::errors::custom(format!("Failed to write transaction to file: {e}")))?;
539 }
540 }
541
542 let mut all_broadcasts = Vec::new();
544 if command.action.broadcast {
545 for (i, (program_id, transaction)) in transactions.iter().enumerate() {
546 println!("\n📡 Broadcasting deployment for {}...", program_id.to_string().bold());
547 let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
549 if !confirm_fee(
550 &fee,
551 &setup.private_key,
552 &setup.address,
553 &setup.endpoint,
554 setup.network,
555 context,
556 command.extra.yes,
557 )? {
558 println!("⏩ Deployment skipped.");
559 continue;
560 }
561 let fee_id = fee.id().to_string();
562 let fee_transaction_id = Transaction::from_fee(fee.clone())?.id().to_string();
563 let id = transaction.id().to_string();
564 let height_before = check_transaction::current_height(
565 &setup.endpoint,
566 setup.network,
567 command.env_override.network_retries,
568 )?;
569 let (message, status) = handle_broadcast(
571 &format!("{}/{}/transaction/broadcast", setup.endpoint, setup.network),
572 transaction,
573 &program_id.to_string(),
574 )?;
575
576 let fail_and_prompt = |msg| {
577 println!("❌ Failed to deploy program {program_id}: {msg}.");
578 let count = transactions.len() - i - 1;
579 if count > 0 {
581 confirm("Do you want to continue with the next deployment?", command.extra.yes)
582 } else {
583 Ok(false)
584 }
585 };
586
587 match status {
588 200..=299 => {
589 let tx_status = check_transaction::check_transaction_with_message(
590 &id,
591 Some(&fee_id),
592 &setup.endpoint,
593 setup.network,
594 height_before + 1,
595 command.extra.max_wait,
596 command.extra.blocks_to_check,
597 command.env_override.network_retries,
598 )?;
599 let confirmed = tx_status == Some(TransactionStatus::Accepted);
600 if confirmed {
601 println!("✅ Deployment confirmed!");
602 } else if fail_and_prompt("could not find the transaction on the network")? {
603 continue;
604 } else {
605 return Ok(build_deploy_output(config.clone(), transactions, all_stats, &all_broadcasts));
606 }
607 all_broadcasts.push(BroadcastStats {
608 fee_id: fee_id.clone(),
609 fee_transaction_id: fee_transaction_id.clone(),
610 confirmed,
611 });
612 }
613 _ => {
614 if fail_and_prompt(&message)? {
615 continue;
616 } else {
617 return Ok(build_deploy_output(config.clone(), transactions, all_stats, &all_broadcasts));
618 }
619 }
620 }
621 }
622 }
623
624 Ok(build_deploy_output(config, transactions, all_stats, &all_broadcasts))
625}
626
627fn enforce_local_deploy_constructor_requirements<N: Network>(
628 tasks: &[Task<N>],
629 skipped: &HashSet<ProgramID<N>>,
630 consensus_version: ConsensusVersion,
631) -> Result<()> {
632 if consensus_version < ConsensusVersion::V9 {
633 return Ok(());
634 }
635
636 for Task { id, program, .. } in tasks {
637 if !skipped.contains(id) && !program.contains_constructor() {
638 return Err(crate::errors::missing_constructor(id).into());
639 }
640 }
641
642 Ok(())
643}
644
645fn handle_deploy<N: Network, A: Aleo<Network = N>>(
649 command: &LeoDeploy,
650 context: Context,
651 network: NetworkName,
652 package: Package,
653) -> Result<<LeoDeploy as Command>::Output> {
654 let setup = create_deploy_setup::<N>(command, &context, network)?;
655 let config = Some(Config {
656 address: setup.address.to_string(),
657 network: network.to_string(),
658 endpoint: Some(setup.endpoint.clone()),
659 consensus_version: Some(setup.consensus_version as u8),
660 });
661
662 let (local, remote, skipped) = prepare_package_tasks::<N>(command, &setup, &package)?;
663 enforce_local_deploy_constructor_requirements(&local, &skipped, setup.consensus_version)?;
664
665 print_deployment_plan(
667 &setup.private_key,
668 &setup.address,
669 &setup.endpoint,
670 &network,
671 &local,
672 &skipped,
673 &remote,
674 &check_tasks_for_warnings(&setup.endpoint, network, &local, setup.consensus_version, command),
675 setup.consensus_version,
676 command,
677 );
678
679 if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
681 println!("❌ Deployment aborted.");
682 return Ok(DeployOutput::default());
683 }
684
685 load_remote_deps(command, &setup, remote)?;
687
688 let mut already_deployed = HashSet::new();
690 match generate_deploy_transactions::<N, A>(command, &setup, local, &skipped, &mut already_deployed)? {
691 Some((transactions, stats)) => output_and_broadcast(command, &setup, &context, config, &transactions, &stats),
692 None => Ok(DeployOutput::default()),
693 }
694}
695
696fn handle_workspace_deploy(command: LeoDeploy, context: Context, targets: Vec<PathBuf>) -> Result<DeployOutput> {
700 if command.rename.is_some() {
702 return Err(crate::errors::custom(
703 "`--rename` is not supported when deploying multiple workspace members; deploy a single program instead.",
704 )
705 .into());
706 }
707 let mut packages = Vec::new();
709 for target in &targets {
710 let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
711 println!("\n--- building workspace member '{member_name}' ---");
712 let member_ctx = context.with_path(target.clone());
713 let package = LeoBuild {
714 env_override: command.env_override.clone(),
715 options: {
716 let mut opts = command.build_options.clone();
717 opts.no_cache = true;
718 opts
719 },
720 rename: None,
722 }
723 .execute(member_ctx)?;
724 if !package.compilation_units.last().is_some_and(|p| p.kind.is_library()) {
726 packages.push((member_name.to_string(), package));
727 }
728 }
729
730 if packages.is_empty() {
731 return Err(crate::errors::custom("No deployable workspace members found (all are libraries).").into());
732 }
733
734 let network = get_network(&command.env_override.network)?;
736 match network {
737 NetworkName::TestnetV0 => {
738 handle_workspace_deploy_inner::<TestnetV0, AleoTestnetV0>(&command, context, network, packages)
739 }
740 NetworkName::MainnetV0 => {
741 #[cfg(feature = "only_testnet")]
742 panic!("Mainnet chosen with only_testnet feature");
743 #[cfg(not(feature = "only_testnet"))]
744 handle_workspace_deploy_inner::<MainnetV0, snarkvm::circuit::AleoV0>(&command, context, network, packages)
745 }
746 NetworkName::CanaryV0 => {
747 #[cfg(feature = "only_testnet")]
748 panic!("Canary chosen with only_testnet feature");
749 #[cfg(not(feature = "only_testnet"))]
750 handle_workspace_deploy_inner::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(
751 &command, context, network, packages,
752 )
753 }
754 }
755}
756
757fn handle_workspace_deploy_inner<N: Network, A: Aleo<Network = N>>(
759 command: &LeoDeploy,
760 context: Context,
761 network: NetworkName,
762 packages: Vec<(String, Package)>,
763) -> Result<DeployOutput> {
764 let setup = create_deploy_setup::<N>(command, &context, network)?;
765 let config = Some(Config {
766 address: setup.address.to_string(),
767 network: network.to_string(),
768 endpoint: Some(setup.endpoint.clone()),
769 consensus_version: Some(setup.consensus_version as u8),
770 });
771
772 let mut member_tasks: Vec<MemberTasks<N>> = Vec::new();
774 let mut all_remote: Vec<Task<N>> = Vec::new();
775 let mut seen_remote: HashMap<ProgramID<N>, Option<u16>> = HashMap::new();
776
777 for (name, package) in &packages {
778 let (local, remote, skipped) = prepare_package_tasks::<N>(command, &setup, package)?;
779 for task in remote {
780 match seen_remote.entry(task.id) {
781 std::collections::hash_map::Entry::Vacant(e) => {
782 e.insert(task.edition);
783 all_remote.push(task);
784 }
785 std::collections::hash_map::Entry::Occupied(e) => {
786 if *e.get() != task.edition {
787 return Err(crate::errors::custom(format!(
788 "Workspace members disagree on edition for remote dependency '{}': \
789 {:?} vs {:?}. Ensure all members specify the same edition.",
790 task.id,
791 e.get(),
792 task.edition,
793 ))
794 .into());
795 }
796 }
797 }
798 }
799 member_tasks.push((name.clone(), local, skipped));
800 }
801
802 for (_, local, skipped) in &member_tasks {
803 enforce_local_deploy_constructor_requirements(local, skipped, setup.consensus_version)?;
804 }
805
806 let mut all_warnings = Vec::new();
808 for (_, local, _) in &member_tasks {
809 all_warnings.extend(check_tasks_for_warnings(
810 &setup.endpoint,
811 network,
812 local,
813 setup.consensus_version,
814 command,
815 ));
816 }
817
818 print_workspace_deployment_plan(&setup, &member_tasks, &all_remote, &all_warnings, command);
820
821 if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
823 println!("❌ Deployment aborted.");
824 return Ok(DeployOutput::default());
825 }
826
827 load_remote_deps(command, &setup, all_remote)?;
829
830 let mut already_deployed: HashSet<ProgramID<N>> = HashSet::new();
834 let mut all_transactions = Vec::new();
835 let mut all_stats = Vec::new();
836
837 for (name, local, skipped) in member_tasks {
838 println!("\n--- deploying workspace member '{name}' ---");
839 match generate_deploy_transactions::<N, A>(command, &setup, local, &skipped, &mut already_deployed)? {
840 Some((txns, stats)) => {
841 all_transactions.extend(txns);
842 all_stats.extend(stats);
843 }
844 None => return Ok(DeployOutput::default()),
845 }
846 }
847
848 output_and_broadcast(command, &setup, &context, config, &all_transactions, &all_stats)
849}
850
851fn print_workspace_deployment_plan<N: Network>(
853 setup: &DeploySetup<N>,
854 member_tasks: &[MemberTasks<N>],
855 all_remote: &[Task<N>],
856 warnings: &[String],
857 command: &LeoDeploy,
858) {
859 println!("\n{}", "🛠️ Workspace Deployment Plan".bold());
860 println!("{}", "──────────────────────────────────────────────".dimmed());
861
862 println!("{}", "🔧 Configuration:".bold());
864 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &setup.private_key.to_string()[..24]).yellow());
865 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &setup.address.to_string()[..24]).yellow());
866 println!(" {:20}{}", "Endpoint:".cyan(), setup.endpoint.yellow());
867 println!(" {:20}{}", "Network:".cyan(), setup.network.to_string().yellow());
868 println!(" {:20}{}", "Consensus Version:".cyan(), (setup.consensus_version as u8).to_string().yellow());
869
870 let mut already_seen: HashSet<ProgramID<N>> = HashSet::new();
872 println!("\n{}", "📦 Workspace Members:".bold());
873 for (name, local, skipped) in member_tasks {
874 println!(" {}:", name.bold());
875 if local.is_empty() {
876 println!(" (no programs)");
877 continue;
878 }
879 for task in local {
880 if skipped.contains(&task.id) {
881 println!(" • {} {}", task.id.to_string().dimmed(), "(skipped by --skip)".dimmed());
882 } else if already_seen.contains(&task.id) {
883 println!(" • {} {}", task.id.to_string().dimmed(), "(already covered by earlier member)".dimmed());
884 } else {
885 let priority_fee_str = task.priority_fee.map_or("0".into(), |v| v.to_string());
886 let record_str = if task.record.is_some() { "yes" } else { "no (public fee)" };
887 println!(
888 " • {} │ priority fee: {} │ fee record: {}",
889 task.id.to_string().cyan(),
890 priority_fee_str,
891 record_str
892 );
893 already_seen.insert(task.id);
894 }
895 }
896 }
897
898 if !all_remote.is_empty() {
900 println!("\n{}", "🌐 Remote Dependencies:".bold().red());
901 println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
902 for task in all_remote {
903 println!(" • {}", task.id.to_string().dimmed());
904 }
905 }
906
907 println!("\n{}", "⚙️ Actions:".bold());
909 if command.action.print {
910 println!(" • Transaction(s) will be printed to the console.");
911 } else {
912 println!(" • Transaction(s) will NOT be printed to the console.");
913 }
914 if let Some(path) = &command.action.save {
915 println!(" • Transaction(s) will be saved to {}", path.bold());
916 } else {
917 println!(" • Transaction(s) will NOT be saved to a file.");
918 }
919 if command.action.broadcast {
920 println!(" • Transaction(s) will be broadcast to {}", setup.endpoint.bold());
921 } else {
922 println!(" • Transaction(s) will NOT be broadcast to the network.");
923 }
924
925 if !warnings.is_empty() {
927 println!("\n{}", "⚠️ Warnings:".bold().red());
928 for warning in warnings {
929 println!(" • {}", warning.dimmed());
930 }
931 }
932
933 println!("{}", "──────────────────────────────────────────────\n".dimmed());
934}
935
936pub(crate) fn consensus_limit(
944 table: &[(ConsensusVersion, usize)],
945 consensus_version: ConsensusVersion,
946 latest: usize,
947) -> usize {
948 table
949 .iter()
950 .take_while(|(version, _)| *version <= consensus_version)
951 .last()
952 .map(|(_, size)| *size)
953 .unwrap_or(latest)
954}
955
956pub(crate) fn max_program_size_for_consensus_version<N: Network>(consensus_version: ConsensusVersion) -> usize {
957 consensus_limit(&N::MAX_PROGRAM_SIZE, consensus_version, N::LATEST_MAX_PROGRAM_SIZE())
958}
959
960pub(crate) fn max_transaction_size_for_consensus_version<N: Network>(consensus_version: ConsensusVersion) -> usize {
961 consensus_limit(&N::MAX_TRANSACTION_SIZE, consensus_version, N::LATEST_MAX_TRANSACTION_SIZE())
962}
963
964pub(crate) fn warn_if_transaction_oversized<N: Network>(
966 id: &ProgramID<N>,
967 transaction: &Transaction<N>,
968 consensus_version: ConsensusVersion,
969 action: &str,
970) {
971 let Ok(size) = transaction.to_bytes_le().map(|bytes| bytes.len()) else { return };
972 let max_size = max_transaction_size_for_consensus_version::<N>(consensus_version);
973 if size > max_size {
974 println!(
975 "⚠️ The '{id}' transaction is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The {action} will likely fail.",
976 size as f64 / 1024.0,
977 max_size as f64 / 1024.0,
978 consensus_version as u8,
979 );
980 }
981}
982
983fn check_tasks_for_warnings<N: Network>(
991 endpoint: &str,
992 network: NetworkName,
993 tasks: &[Task<N>],
994 consensus_version: ConsensusVersion,
995 command: &LeoDeploy,
996) -> Vec<String> {
997 let mut warnings = Vec::new();
998 for Task { id, is_local, program, bytecode_size, .. } in tasks {
999 if !is_local || !command.action.broadcast {
1000 continue;
1001 }
1002 if fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries).is_ok()
1004 {
1005 warnings
1006 .push(format!("The program '{id}' already exists on the network. Please use `leo upgrade` instead.",));
1007 }
1008 if consensus_version >= ConsensusVersion::V7
1010 && let Err(e) = program.check_program_naming_structure()
1011 {
1012 warnings.push(format!(
1013 "The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
1014 ));
1015 }
1016
1017 if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
1019 warnings.push(format!(
1020 "The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
1021 consensus_version as u8
1022 ));
1023 }
1024 if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
1026 warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The deployment will likely fail"));
1027 }
1028 if consensus_version < ConsensusVersion::V15 && program.contains_v15_syntax() {
1030 warnings.push(format!("The program '{id}' uses V15 features (e.g., `view fn`) but the consensus version is less than V15. The deployment will likely fail"));
1031 }
1032 if consensus_version < ConsensusVersion::V16 && program.contains_v16_syntax() {
1034 warnings.push(format!("The program '{id}' uses V16 features (e.g., `std::prog::function_checksum`) but the consensus version is less than V16. The deployment will likely fail"));
1035 }
1036 if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
1038 warnings
1039 .push(format!("The program '{id}' does not contain a constructor. The deployment will likely fail",));
1040 }
1041 let max_size = max_program_size_for_consensus_version::<N>(consensus_version);
1044 if *bytecode_size > max_size {
1045 warnings.push(format!(
1046 "The program '{id}' is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The deployment will likely fail.",
1047 *bytecode_size as f64 / 1024.0,
1048 max_size as f64 / 1024.0,
1049 consensus_version as u8,
1050 ));
1051 } else if let (_, _, Some(msg)) = format_program_size(*bytecode_size, max_size) {
1052 warnings.push(format!("The program '{id}' is {msg}."));
1053 }
1054 }
1055 if let Err(e) =
1057 check_consensus_version_mismatch(consensus_version, endpoint, network, command.env_override.network_retries)
1058 {
1059 warnings.push(format!("{e}. In some cases, the deployment may fail"));
1060 }
1061 warnings
1062}
1063
1064pub(crate) fn validate_deployment_limits<N: Network>(
1066 deployment: &Deployment<N>,
1067 program_id: &ProgramID<N>,
1068 network: &NetworkName,
1069 consensus_version: ConsensusVersion,
1070) -> Result<()> {
1071 let (max_variables, max_constraints) = if consensus_version >= ConsensusVersion::V19 {
1072 (Some(N::MAX_DEPLOYMENT_VARIABLES_V2), Some(N::MAX_DEPLOYMENT_CONSTRAINTS_V2))
1073 } else if consensus_version >= ConsensusVersion::V18 {
1074 (None, None)
1075 } else {
1076 (Some(N::MAX_DEPLOYMENT_VARIABLES), Some(N::MAX_DEPLOYMENT_CONSTRAINTS))
1077 };
1078
1079 let combined_variables = deployment.num_combined_variables()?;
1081 if let Some(max_variables) = max_variables
1082 && combined_variables > max_variables
1083 {
1084 return Err(
1085 crate::errors::variable_limit_exceeded(program_id, combined_variables, max_variables, network).into()
1086 );
1087 }
1088
1089 let constraints = deployment.num_combined_constraints()?;
1091 if let Some(max_constraints) = max_constraints
1092 && constraints > max_constraints
1093 {
1094 return Err(crate::errors::constraint_limit_exceeded(program_id, constraints, max_constraints, network).into());
1095 }
1096
1097 Ok(())
1098}
1099
1100#[allow(clippy::too_many_arguments)]
1102pub(crate) fn print_deployment_plan<N: Network>(
1103 private_key: &PrivateKey<N>,
1104 address: &Address<N>,
1105 endpoint: &str,
1106 network: &NetworkName,
1107 local: &[Task<N>],
1108 skipped: &HashSet<ProgramID<N>>,
1109 remote: &[Task<N>],
1110 warnings: &[String],
1111 consensus_version: ConsensusVersion,
1112 command: &LeoDeploy,
1113) {
1114 use colored::*;
1115
1116 println!("\n{}", "🛠️ Deployment Plan Summary".bold());
1117 println!("{}", "──────────────────────────────────────────────".dimmed());
1118
1119 println!("{}", "🔧 Configuration:".bold());
1121 println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
1122 println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
1123 println!(" {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
1124 println!(" {:20}{}", "Network:".cyan(), network.to_string().yellow());
1125 println!(" {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
1126
1127 println!("\n{}", "📦 Deployment Tasks:".bold());
1129 if local.is_empty() {
1130 println!(" (none)");
1131 } else {
1132 for Task { id, priority_fee, record, .. } in local.iter().filter(|task| !skipped.contains(&task.id)) {
1133 let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
1134 let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
1135 println!(
1136 " • {} │ priority fee: {} │ fee record: {}",
1137 id.to_string().cyan(),
1138 priority_fee_str,
1139 record_str
1140 );
1141 }
1142 }
1143
1144 if !skipped.is_empty() {
1146 println!("\n{}", "🚫 Skipped Programs:".bold().red());
1147 for symbol in skipped {
1148 println!(" • {}", symbol.to_string().dimmed());
1149 }
1150 }
1151
1152 if !remote.is_empty() {
1154 println!("\n{}", "🌐 Remote Dependencies:".bold().red());
1155 println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
1156 for Task { id, .. } in remote {
1157 println!(" • {}", id.to_string().dimmed());
1158 }
1159 }
1160
1161 println!("\n{}", "⚙️ Actions:".bold());
1163 if command.action.print {
1164 println!(" • Transaction(s) will be printed to the console.");
1165 } else {
1166 println!(" • Transaction(s) will NOT be printed to the console.");
1167 }
1168 if let Some(path) = &command.action.save {
1169 println!(" • Transaction(s) will be saved to {}", path.bold());
1170 } else {
1171 println!(" • Transaction(s) will NOT be saved to a file.");
1172 }
1173 if command.action.broadcast {
1174 println!(" • Transaction(s) will be broadcast to {}", endpoint.bold());
1175 } else {
1176 println!(" • Transaction(s) will NOT be broadcast to the network.");
1177 }
1178
1179 if !warnings.is_empty() {
1181 println!("\n{}", "⚠️ Warnings:".bold().red());
1182 for warning in warnings {
1183 println!(" • {}", warning.dimmed());
1184 }
1185 }
1186
1187 println!("{}", "──────────────────────────────────────────────\n".dimmed());
1188}
1189
1190pub(crate) fn compute_deployment_stats<N: Network, R: Rng + CryptoRng>(
1192 vm: &VM<N, ConsensusMemory<N>>,
1193 deployment: &Deployment<N>,
1194 priority_fee: Option<u64>,
1195 consensus_version: ConsensusVersion,
1196 bytecode_size: usize,
1197 rng: &mut R,
1198) -> Result<DeploymentStats> {
1199 let variables = deployment.num_combined_variables()?;
1200 let constraints = deployment.num_combined_constraints()?;
1201 let (_, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
1202 deployment_cost(vm.process(), deployment, consensus_version)?;
1203 let priority = priority_fee.unwrap_or(0);
1204
1205 let function_costs = calculate_function_costs(vm, deployment, consensus_version, rng)?;
1206
1207 let (max_variables, max_constraints) = if consensus_version >= ConsensusVersion::V19 {
1208 (Some(N::MAX_DEPLOYMENT_VARIABLES_V2), Some(N::MAX_DEPLOYMENT_CONSTRAINTS_V2))
1209 } else if consensus_version >= ConsensusVersion::V18 {
1210 (None, None)
1211 } else {
1212 (Some(N::MAX_DEPLOYMENT_VARIABLES), Some(N::MAX_DEPLOYMENT_CONSTRAINTS))
1213 };
1214
1215 Ok(DeploymentStats {
1216 program_size_bytes: bytecode_size,
1217 max_program_size_bytes: max_program_size_for_consensus_version::<N>(consensus_version),
1218 total_variables: Some(variables),
1219 total_constraints: Some(constraints),
1220 max_variables,
1221 max_constraints,
1222 storage_cost,
1223 synthesis_cost,
1224 namespace_cost,
1225 constructor_cost,
1226 priority_fee: priority,
1227 total_cost: storage_cost + synthesis_cost + namespace_cost + constructor_cost + priority,
1228 function_costs,
1229 })
1230}
1231
1232#[allow(clippy::too_many_arguments)]
1237pub(crate) fn deploy_with_placeholder_certificate<N: Network, A: Aleo<Network = N>, R: Rng + CryptoRng>(
1238 vm: &VM<N, ConsensusMemory<N>>,
1239 private_key: &PrivateKey<N>,
1240 program: &Program<N>,
1241 edition: u16,
1242 record: Option<Record<N, Plaintext<N>>>,
1243 priority_fee: Option<u64>,
1244 bytecode_size: usize,
1245 consensus_version: ConsensusVersion,
1246 query: &SnarkVMQuery<N, BlockMemory<N>>,
1247 network_retries: u32,
1248 rng: &mut R,
1249) -> Result<(Transaction<N>, DeploymentStats)> {
1250 assert!(!program.functions().is_empty(), "Program `{}` has no functions", program.id());
1251 let mut verifying_keys = Vec::with_capacity(program.functions().len() + program.records().len());
1253 for function_name in program.functions().keys() {
1254 let verifying_key = VerifyingKey::from_str(leo_compiler::run::PLACEHOLDER_VK)?;
1255 let certificate = Certificate::from_str(leo_compiler::run::PLACEHOLDER_CERT)?;
1256 verifying_keys.push((*function_name, (verifying_key, certificate)));
1257 }
1258 for record_name in program.records().keys() {
1259 let verifying_key = VerifyingKey::from_str(leo_compiler::run::PLACEHOLDER_VK)?;
1260 let certificate = Certificate::from_str(leo_compiler::run::PLACEHOLDER_CERT)?;
1261 verifying_keys.push((*record_name, (verifying_key, certificate)));
1262 }
1263 let mut deployment = Deployment::new(edition, program.clone(), verifying_keys, None, None).unwrap();
1265
1266 deployment.set_program_owner_raw(Some(Address::try_from(private_key)?));
1268
1269 deployment.set_program_checksum_raw(Some(deployment.program().to_checksum()));
1271
1272 let deployment_id = deployment.to_deployment_id()?;
1274
1275 let owner = ProgramOwner::new(private_key, deployment_id, rng)?;
1277
1278 let (minimum_deployment_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
1280 deployment_cost(vm.process(), &deployment, consensus_version)?;
1281 let fee_authorization = match record {
1283 Some(record) => vm
1284 .process()
1285 .authorize_fee_private::<A, _>(
1286 private_key,
1287 record,
1288 minimum_deployment_cost,
1289 priority_fee.unwrap_or(0),
1290 deployment_id,
1291 rng,
1292 )
1293 .map_err(|e| anyhow::anyhow!("{e}"))?,
1294 None => vm
1295 .process()
1296 .authorize_fee_public::<A, _>(
1297 private_key,
1298 minimum_deployment_cost,
1299 priority_fee.unwrap_or(0),
1300 deployment_id,
1301 rng,
1302 )
1303 .map_err(|e| anyhow::anyhow!("{e}"))?,
1304 };
1305
1306 let state_root = retry_network_call(network_retries, || query.current_state_root())?;
1308
1309 let fee = Fee::from(fee_authorization.transitions().into_iter().next().unwrap().1, state_root, None)?;
1311
1312 vm.process().lock().add_program(program)?;
1314 let priority = priority_fee.unwrap_or(0);
1316 let function_costs = calculate_function_costs(vm, &deployment, consensus_version, rng)?;
1317 let stats = DeploymentStats {
1318 program_size_bytes: bytecode_size,
1319 max_program_size_bytes: max_program_size_for_consensus_version::<N>(consensus_version),
1320 total_variables: None,
1321 total_constraints: None,
1322 max_variables: None,
1323 max_constraints: None,
1324 storage_cost,
1325 synthesis_cost,
1326 namespace_cost,
1327 constructor_cost,
1328 priority_fee: priority,
1329 total_cost: storage_cost + synthesis_cost + namespace_cost + constructor_cost + priority,
1330 function_costs,
1331 };
1332 let transaction = Transaction::from_deployment(owner, deployment, fee)?;
1334 Ok((transaction, stats))
1335}
1336
1337pub(crate) fn print_deployment_summary(program_id: &str, stats: &DeploymentStats) {
1339 use colored::*;
1340
1341 println!("\n{} {}", "📊 Deployment Summary for".bold(), program_id.bold());
1342 println!("{}", "──────────────────────────────────────────────".dimmed());
1343 print!("{stats}");
1344 println!("{}", "──────────────────────────────────────────────".dimmed());
1345}
1346
1347pub(crate) fn calculate_function_costs<N: Network, R: Rng + CryptoRng>(
1349 vm: &VM<N, ConsensusMemory<N>>,
1350 deployment: &Deployment<N>,
1351 consensus_version: ConsensusVersion,
1352 rng: &mut R,
1353) -> Result<Vec<FunctionCostStats>> {
1354 let stack = vm.process().get_stack(deployment.program().id())?;
1356
1357 let mut function_costs = Vec::new();
1358
1359 let sample_key = PrivateKey::new(rng)?;
1361 let sample_address = Address::try_from(&sample_key)?;
1362
1363 for (function_name, _) in deployment.function_verifying_keys() {
1364 let name = function_name.to_string();
1365
1366 let input_types = deployment.program().get_function(function_name)?.input_types();
1370 let inputs = input_types
1371 .into_iter()
1372 .map(|ty| {
1373 stack
1374 .sample_value(&sample_address, &ty.into(), rng)
1375 .map_err(|e| crate::errors::custom(format!("Failed to sample value: {e}")).into())
1376 })
1377 .collect::<Result<Vec<_>>>()?;
1378 let (finalize_cost, storage_cost, execution_cost) =
1379 match vm.authorize(&sample_key, deployment.program().id(), function_name, inputs.iter(), rng) {
1380 Err(e) => {
1381 tracing::debug!("Could not estimate execution cost for '{name}': {e}");
1382 let static_finalize_cost = if consensus_version >= ConsensusVersion::V10 {
1384 minimum_cost_in_microcredits_v3(&stack, function_name)?
1385 } else if consensus_version >= ConsensusVersion::V2 {
1386 minimum_cost_in_microcredits_v2(&stack, function_name)?
1387 } else {
1388 minimum_cost_in_microcredits_v1(&stack, function_name)?
1389 };
1390 (static_finalize_cost, None, None)
1391 }
1392 Ok(authorization) => {
1393 let (total, (storage, finalize)) =
1394 execution_cost_for_authorization(vm.process(), &authorization, consensus_version)?;
1395 (finalize, Some(storage), Some(total))
1396 }
1397 };
1398
1399 let has_dynamic_calls = stack.contains_dynamic_call(function_name).unwrap_or(false);
1402
1403 function_costs.push(FunctionCostStats { name, finalize_cost, storage_cost, execution_cost, has_dynamic_calls });
1404 }
1405
1406 Ok(function_costs)
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412 use std::str::FromStr;
1413
1414 fn task_from_source(source: &str) -> Task<TestnetV0> {
1415 let program = Program::<TestnetV0>::from_str(source).unwrap();
1416 Task {
1417 id: *program.id(),
1418 program,
1419 edition: None,
1420 is_local: true,
1421 priority_fee: None,
1422 record: None,
1423 bytecode_size: source.len(),
1424 }
1425 }
1426
1427 #[test]
1428 fn rejects_unskipped_local_deploy_without_constructor() {
1429 let task = task_from_source(concat!(
1430 "program missing_constructor.aleo;\n",
1431 "function main:\n",
1432 " input r0 as u32.public;\n",
1433 " output r0 as u32.public;\n",
1434 ));
1435
1436 let result = enforce_local_deploy_constructor_requirements(&[task], &HashSet::new(), ConsensusVersion::V9);
1437
1438 assert!(result.is_err());
1439 }
1440
1441 #[test]
1442 fn allows_skipped_local_deploy_without_constructor() {
1443 let task = task_from_source(concat!(
1444 "program skipped_constructor.aleo;\n",
1445 "function main:\n",
1446 " input r0 as u32.public;\n",
1447 " output r0 as u32.public;\n",
1448 ));
1449 let skipped = HashSet::from([task.id]);
1450
1451 enforce_local_deploy_constructor_requirements(&[task], &skipped, ConsensusVersion::V9).unwrap();
1452 }
1453
1454 #[test]
1455 fn allows_local_deploy_without_constructor_before_v9() {
1456 let task = task_from_source(concat!(
1457 "program pre_constructor_gate.aleo;\n",
1458 "function main:\n",
1459 " input r0 as u32.public;\n",
1460 " output r0 as u32.public;\n",
1461 ));
1462
1463 enforce_local_deploy_constructor_requirements(&[task], &HashSet::new(), ConsensusVersion::V8).unwrap();
1464 }
1465
1466 #[test]
1467 fn plaintext_size_limit_tracks_deployment_consensus_version() -> Result<()> {
1468 let command = LeoDeploy::try_parse_from(["deploy", "--yes", "--skip-deploy-certificate"])
1469 .expect("Valid deployment options");
1470 for (consensus_version, array_length, skip, rejected) in [
1471 (ConsensusVersion::V19, 512, false, false),
1472 (ConsensusVersion::V20, 512, false, true),
1473 (ConsensusVersion::V21, 512, false, true),
1474 (ConsensusVersion::V20, 511, false, false),
1475 (ConsensusVersion::V20, 512, true, false),
1476 ] {
1477 let task = task_from_source(&format!(
1479 "program plaintext_limit.aleo;\n\
1480 mapping values:\n\
1481 key as u8.public;\n\
1482 value as [[boolean; 2048u32]; {array_length}u32].public;\n\
1483 function main:\n\
1484 input r0 as u8.public;\n\
1485 output r0 as u8.public;\n\
1486 constructor:\n\
1487 assert.eq true true;\n",
1488 ));
1489 let skipped = if skip { HashSet::from([task.id]) } else { HashSet::new() };
1490 let private_key = PrivateKey::<TestnetV0>::new(&mut rand::rng())?;
1491 let vm = VM::from(ConsensusStore::<TestnetV0, ConsensusMemory<TestnetV0>>::open(StorageMode::Production)?)?;
1492 let setup = DeploySetup {
1493 address: Address::try_from(&private_key)?,
1494 private_key,
1495 endpoint: String::new(),
1496 consensus_version,
1497 network: NetworkName::TestnetV0,
1498 query: SnarkVMQuery::from(vm.block_store()),
1499 vm,
1500 };
1501 let result = generate_deploy_transactions::<TestnetV0, AleoTestnetV0>(
1502 &command,
1503 &setup,
1504 vec![task],
1505 &skipped,
1506 &mut HashSet::new(),
1507 );
1508
1509 if rejected {
1510 let error = result.err().expect("V20 and later must reject oversized plaintext declarations");
1511 assert!(error.to_string().contains("exceeds the maximum allowed size in bits"), "{error}");
1512 } else {
1513 let (transactions, _) = result?.expect("The deployment plan is confirmed by --yes");
1514 assert_eq!(transactions.len(), usize::from(!skip));
1515 }
1516 }
1517 Ok(())
1518 }
1519
1520 #[test]
1521 fn program_size_limit_tracks_consensus_version() {
1522 assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V1), 100_000);
1524 assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V13), 100_000);
1525 assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V14), 512_000);
1526 assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V15), 512_000);
1527 assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V16), 2_048_000);
1528 assert_eq!(
1530 max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::latest()),
1531 TestnetV0::LATEST_MAX_PROGRAM_SIZE()
1532 );
1533 }
1534}