use super::*;
use check_transaction::TransactionStatus;
use leo_ast::NetworkName;
use leo_package::{Package, ProgramData, fetch_program_from_network, retry_network_call};
use aleo_std::StorageMode;
use rand::CryptoRng;
#[cfg(not(feature = "only_testnet"))]
use snarkvm::prelude::{CanaryV0, MainnetV0};
use snarkvm::{
circuit::{Aleo, AleoTestnetV0},
ledger::{
query::{Query as SnarkVMQuery, QueryTrait},
store::helpers::memory::BlockMemory,
},
prelude::{
Certificate,
ConsensusVersion,
Deployment,
Fee,
Program,
ProgramID,
ProgramOwner,
Rng,
TestnetV0,
ToBytes,
Transaction,
VM,
VerifyingKey,
deployment_cost,
execution_cost_for_authorization,
minimum_cost_in_microcredits_v1,
minimum_cost_in_microcredits_v2,
minimum_cost_in_microcredits_v3,
store::{ConsensusStore, helpers::memory::ConsensusMemory},
},
synthesizer::program::StackTrait,
};
use colored::*;
use itertools::Itertools;
use std::{
collections::{HashMap, HashSet},
fs,
path::PathBuf,
};
#[derive(Parser, Debug)]
pub struct LeoDeploy {
#[clap(flatten)]
pub(crate) fee_options: FeeOptions,
#[clap(flatten)]
pub(crate) action: TransactionAction,
#[clap(flatten)]
pub(crate) env_override: EnvOptions,
#[clap(flatten)]
pub(crate) key_override: PrivateKeyOptions,
#[clap(flatten)]
pub(crate) consensus_override: ConsensusOptions,
#[clap(flatten)]
pub(crate) extra: ExtraOptions,
#[clap(long, help = "Skips deployment of any program that contains one of the given substrings.", value_delimiter = ',', num_args = 1..)]
pub(crate) skip: Vec<String>,
#[clap(
long,
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."
)]
pub(crate) rename: Option<String>,
#[clap(flatten)]
pub(crate) build_options: BuildOptions,
#[clap(long, help = "Use placeholder certificate and verifying keys during deployment.", default_value = "false")]
pub(crate) skip_deploy_certificate: bool,
}
pub struct Task<N: Network> {
pub id: ProgramID<N>,
pub program: Program<N>,
pub edition: Option<u16>,
pub is_local: bool,
pub priority_fee: Option<u64>,
pub record: Option<Record<N, Plaintext<N>>>,
pub bytecode_size: usize,
}
type PreparedTasks<N> = (Vec<Task<N>>, Vec<Task<N>>, HashSet<ProgramID<N>>);
type DeployTransactions<N> = (Vec<(ProgramID<N>, Transaction<N>)>, Vec<DeploymentStats>);
type MemberTasks<N> = (String, Vec<Task<N>>, HashSet<ProgramID<N>>);
struct DeploySetup<N: Network> {
private_key: PrivateKey<N>,
address: Address<N>,
endpoint: String,
consensus_version: ConsensusVersion,
network: NetworkName,
vm: VM<N, ConsensusMemory<N>>,
query: SnarkVMQuery<N, BlockMemory<N>>,
}
impl Command for LeoDeploy {
type Input = Package;
type Output = DeployOutput;
fn log_span(&self) -> Span {
tracing::span!(tracing::Level::INFO, "Leo")
}
fn prelude(&self, context: Context) -> Result<Self::Input> {
LeoBuild {
env_override: self.env_override.clone(),
options: {
let mut options = self.build_options.clone();
options.no_cache = true;
options
},
rename: self.rename.clone(),
}
.execute(context)
}
fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
if input.compilation_units.last().is_some_and(|p| p.kind.is_library()) {
return Err(crate::errors::custom("Cannot deploy a library package. Only programs can be deployed.").into());
}
let network = get_network(&self.env_override.network)?;
match network {
NetworkName::TestnetV0 => handle_deploy::<TestnetV0, AleoTestnetV0>(&self, context, network, input),
NetworkName::MainnetV0 => {
#[cfg(feature = "only_testnet")]
panic!("Mainnet chosen with only_testnet feature");
#[cfg(not(feature = "only_testnet"))]
handle_deploy::<MainnetV0, snarkvm::circuit::AleoV0>(&self, context, network, input)
}
NetworkName::CanaryV0 => {
#[cfg(feature = "only_testnet")]
panic!("Canary chosen with only_testnet feature");
#[cfg(not(feature = "only_testnet"))]
handle_deploy::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(&self, context, network, input)
}
}
}
fn execute(self, context: Context) -> Result<Self::Output> {
match context.resolve_targets()? {
Some((_, targets)) if targets.len() > 1 => handle_workspace_deploy(self, context, targets),
_ => {
let input = self.prelude(context.clone())?;
let span = self.log_span();
let span = span.enter();
let out = self.apply(context, input);
drop(span);
out
}
}
}
}
fn create_deploy_setup<N: Network>(
command: &LeoDeploy,
context: &Context,
network: NetworkName,
) -> Result<DeploySetup<N>> {
let private_key = get_private_key(&command.key_override.private_key)?;
let address =
Address::try_from(&private_key).map_err(|e| crate::errors::custom(format!("Failed to parse address: {e}")))?;
let endpoint = get_endpoint(&command.env_override.endpoint)?;
let is_devnet = get_is_devnet(command.consensus_override.devnet);
let consensus_heights = command
.consensus_override
.consensus_heights
.clone()
.unwrap_or_else(|| get_consensus_heights(network, is_devnet));
validate_consensus_heights(&consensus_heights)
.map_err(|e| crate::errors::custom(format!("⚠️ Invalid consensus heights: {e}")))?;
let consensus_heights_string = consensus_heights.iter().format(",").to_string();
println!(
"\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"
);
#[allow(unsafe_code)]
unsafe {
std::env::set_var("CONSENSUS_VERSION_HEIGHTS", consensus_heights_string);
}
let consensus_version = get_consensus_version(
&command.extra.consensus_version,
&endpoint,
network,
&consensus_heights,
context,
command.env_override.network_retries,
)?;
let vm = VM::from(ConsensusStore::<N, ConsensusMemory<N>>::open(StorageMode::Production)?)?;
let query = SnarkVMQuery::<N, BlockMemory<N>>::from(
endpoint
.parse::<Uri>()
.map_err(|e| crate::errors::custom(format!("Failed to parse endpoint URI '{endpoint}': {e}")))?,
);
Ok(DeploySetup { private_key, address, endpoint, consensus_version, network, vm, query })
}
fn prepare_package_tasks<N: Network>(
command: &LeoDeploy,
setup: &DeploySetup<N>,
package: &Package,
) -> Result<PreparedTasks<N>> {
let programs = package.compilation_units.iter().filter(|unit| unit.kind.is_program()).cloned();
let programs_and_bytecode: Vec<(leo_package::CompilationUnit, String)> = programs
.into_iter()
.map(|program| {
let bytecode = match &program.data {
ProgramData::Bytecode(s) => s.clone(),
ProgramData::SourcePath { .. } => {
let aleo_path = package.unit_bytecode_path(&program.name.to_string());
fs::read_to_string(aleo_path.clone()).map_err(|e| {
crate::errors::custom(format!("Failed to read file {}: {e}", aleo_path.display()))
})?
}
};
Ok((program, bytecode))
})
.collect::<Result<_>>()?;
let fee_options = parse_fee_options(&setup.private_key, &command.fee_options, programs_and_bytecode.len())?;
let tasks: Vec<Task<N>> = programs_and_bytecode
.into_iter()
.zip(fee_options)
.map(|((program, bytecode), (priority_fee, record))| {
let id_str = format!("{}", program.name);
let id = id_str
.parse()
.map_err(|e| crate::errors::custom(format!("Failed to parse program ID {id_str}: {e}")))?;
let bytecode_size = bytecode.len();
let parsed_program =
bytecode.parse().map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
Ok(Task {
id,
program: parsed_program,
edition: program.edition,
is_local: program.is_local,
priority_fee,
record,
bytecode_size,
})
})
.collect::<Result<_>>()?;
let (local, remote) = tasks.into_iter().partition::<Vec<_>, _>(|task| task.is_local);
let skipped: HashSet<ProgramID<N>> = local
.iter()
.filter_map(|task| {
let id_string = task.id.to_string();
command.skip.iter().any(|skip| id_string.contains(skip)).then_some(task.id)
})
.collect();
Ok((local, remote, skipped))
}
fn load_remote_deps<N: Network>(command: &LeoDeploy, setup: &DeploySetup<N>, remote: Vec<Task<N>>) -> Result<()> {
if remote.is_empty() {
return Ok(());
}
let programs_and_editions = remote
.into_iter()
.map(|task| {
let edition = match task.edition {
Some(e) => e,
None => leo_package::fetch_latest_edition(
&task.id.to_string(),
&setup.endpoint,
setup.network,
command.env_override.network_retries,
)?,
};
Ok((task.program, edition))
})
.collect::<Result<Vec<_>>>()?;
check_edition_constructor_requirements(&programs_and_editions, setup.consensus_version, "deploy")?;
setup.vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
Ok(())
}
fn generate_deploy_transactions<N: Network, A: Aleo<Network = N>>(
command: &LeoDeploy,
setup: &DeploySetup<N>,
local: Vec<Task<N>>,
skipped: &HashSet<ProgramID<N>>,
already_deployed: &mut HashSet<ProgramID<N>>,
) -> Result<Option<DeployTransactions<N>>> {
let rng = &mut rand::rng();
let mut transactions = Vec::new();
let mut all_stats = Vec::new();
for Task { id, program, edition, priority_fee, record, bytecode_size, .. } in local {
if !skipped.contains(&id) && !already_deployed.contains(&id) {
if let Some(constructor) = program.constructor() {
println!(
r"
🔧 Your program '{}' has the following constructor.
──────────────────────────────────────────────
{constructor}
──────────────────────────────────────────────
Once it is deployed, it CANNOT be changed.
",
id.to_string().bold()
);
if !confirm("Would you like to proceed?", command.extra.yes)? {
println!("❌ Deployment aborted.");
return Ok(None);
}
}
println!("📦 Creating deployment transaction for '{}'...\n", id.to_string().bold());
let (transaction, stats) = if command.skip_deploy_certificate {
println!("⚠️ Skipping deployment certificate and verifier key generation as per user request.\n");
deploy_with_placeholder_certificate::<N, A, _>(
&setup.vm,
&setup.private_key,
&program,
edition.unwrap_or(0),
record,
priority_fee,
bytecode_size,
setup.consensus_version,
&setup.query,
command.env_override.network_retries,
rng,
)?
} else {
let transaction = setup
.vm
.deploy(&setup.private_key, &program, record, priority_fee.unwrap_or(0), Some(&setup.query), rng)
.map_err(|e| crate::errors::custom(format!("Failed to generate deployment transaction: {e}")))?;
let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
setup.vm.process().lock().add_program(&program)?;
let stats = compute_deployment_stats(
&setup.vm,
deployment,
priority_fee,
setup.consensus_version,
bytecode_size,
rng,
)?;
(transaction, stats)
};
print_deployment_summary(&id.to_string(), &stats);
if command.action.broadcast {
warn_if_transaction_oversized(&id, &transaction, setup.consensus_version, "deployment");
}
transactions.push((id, transaction));
all_stats.push(stats);
already_deployed.insert(id);
if !command.skip_deploy_certificate {
let (program_id, transaction) = transactions.last().expect("Transaction was just pushed");
let deployment = transaction.deployment().expect("Expected a deployment in the transaction");
validate_deployment_limits(deployment, program_id, &setup.network)?;
}
}
if let Err(e) = setup.vm.process().lock().add_program(&program) {
tracing::debug!("Program {id} already in VM: {e}");
}
}
Ok(Some((transactions, all_stats)))
}
fn output_and_broadcast<N: Network>(
command: &LeoDeploy,
setup: &DeploySetup<N>,
context: &Context,
config: Option<Config>,
transactions: &[(ProgramID<N>, Transaction<N>)],
all_stats: &[DeploymentStats],
) -> Result<DeployOutput> {
if command.action.print {
for (program_name, transaction) in transactions.iter() {
let transaction_json = serde_json::to_string_pretty(transaction)
.map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
println!("🖨️ Printing deployment for {program_name}\n{transaction_json}")
}
}
if let Some(path) = &command.action.save {
std::fs::create_dir_all(path).map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
for (program_name, transaction) in transactions.iter() {
let file_path = PathBuf::from(path).join(format!("{program_name}.deployment.json"));
println!("💾 Saving deployment for {program_name} at {}", file_path.display());
let transaction_json = serde_json::to_string_pretty(transaction)
.map_err(|e| crate::errors::custom(format!("Failed to serialize transaction: {e}")))?;
std::fs::write(file_path, transaction_json)
.map_err(|e| crate::errors::custom(format!("Failed to write transaction to file: {e}")))?;
}
}
let mut all_broadcasts = Vec::new();
if command.action.broadcast {
for (i, (program_id, transaction)) in transactions.iter().enumerate() {
println!("\n📡 Broadcasting deployment for {}...", program_id.to_string().bold());
let fee = transaction.fee_transition().expect("Expected a fee in the transaction");
if !confirm_fee(
&fee,
&setup.private_key,
&setup.address,
&setup.endpoint,
setup.network,
context,
command.extra.yes,
)? {
println!("⏩ Deployment skipped.");
continue;
}
let fee_id = fee.id().to_string();
let fee_transaction_id = Transaction::from_fee(fee.clone())?.id().to_string();
let id = transaction.id().to_string();
let height_before = check_transaction::current_height(
&setup.endpoint,
setup.network,
command.env_override.network_retries,
)?;
let (message, status) = handle_broadcast(
&format!("{}/{}/transaction/broadcast", setup.endpoint, setup.network),
transaction,
&program_id.to_string(),
)?;
let fail_and_prompt = |msg| {
println!("❌ Failed to deploy program {program_id}: {msg}.");
let count = transactions.len() - i - 1;
if count > 0 {
confirm("Do you want to continue with the next deployment?", command.extra.yes)
} else {
Ok(false)
}
};
match status {
200..=299 => {
let tx_status = check_transaction::check_transaction_with_message(
&id,
Some(&fee_id),
&setup.endpoint,
setup.network,
height_before + 1,
command.extra.max_wait,
command.extra.blocks_to_check,
command.env_override.network_retries,
)?;
let confirmed = tx_status == Some(TransactionStatus::Accepted);
if confirmed {
println!("✅ Deployment confirmed!");
} else if fail_and_prompt("could not find the transaction on the network")? {
continue;
} else {
return Ok(build_deploy_output(config.clone(), transactions, all_stats, &all_broadcasts));
}
all_broadcasts.push(BroadcastStats {
fee_id: fee_id.clone(),
fee_transaction_id: fee_transaction_id.clone(),
confirmed,
});
}
_ => {
if fail_and_prompt(&message)? {
continue;
} else {
return Ok(build_deploy_output(config.clone(), transactions, all_stats, &all_broadcasts));
}
}
}
}
}
Ok(build_deploy_output(config, transactions, all_stats, &all_broadcasts))
}
fn enforce_local_deploy_constructor_requirements<N: Network>(
tasks: &[Task<N>],
skipped: &HashSet<ProgramID<N>>,
consensus_version: ConsensusVersion,
) -> Result<()> {
if consensus_version < ConsensusVersion::V9 {
return Ok(());
}
for Task { id, program, .. } in tasks {
if !skipped.contains(id) && !program.contains_constructor() {
return Err(crate::errors::missing_constructor(id).into());
}
}
Ok(())
}
fn handle_deploy<N: Network, A: Aleo<Network = N>>(
command: &LeoDeploy,
context: Context,
network: NetworkName,
package: Package,
) -> Result<<LeoDeploy as Command>::Output> {
let setup = create_deploy_setup::<N>(command, &context, network)?;
let config = Some(Config {
address: setup.address.to_string(),
network: network.to_string(),
endpoint: Some(setup.endpoint.clone()),
consensus_version: Some(setup.consensus_version as u8),
});
let (local, remote, skipped) = prepare_package_tasks::<N>(command, &setup, &package)?;
enforce_local_deploy_constructor_requirements(&local, &skipped, setup.consensus_version)?;
print_deployment_plan(
&setup.private_key,
&setup.address,
&setup.endpoint,
&network,
&local,
&skipped,
&remote,
&check_tasks_for_warnings(&setup.endpoint, network, &local, setup.consensus_version, command),
setup.consensus_version,
command,
);
if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
println!("❌ Deployment aborted.");
return Ok(DeployOutput::default());
}
load_remote_deps(command, &setup, remote)?;
let mut already_deployed = HashSet::new();
match generate_deploy_transactions::<N, A>(command, &setup, local, &skipped, &mut already_deployed)? {
Some((transactions, stats)) => output_and_broadcast(command, &setup, &context, config, &transactions, &stats),
None => Ok(DeployOutput::default()),
}
}
fn handle_workspace_deploy(command: LeoDeploy, context: Context, targets: Vec<PathBuf>) -> Result<DeployOutput> {
if command.rename.is_some() {
return Err(crate::errors::custom(
"`--rename` is not supported when deploying multiple workspace members; deploy a single program instead.",
)
.into());
}
let mut packages = Vec::new();
for target in &targets {
let member_name = target.file_name().and_then(|n| n.to_str()).unwrap_or("?");
println!("\n--- building workspace member '{member_name}' ---");
let member_ctx = context.with_path(target.clone());
let package = LeoBuild {
env_override: command.env_override.clone(),
options: {
let mut opts = command.build_options.clone();
opts.no_cache = true;
opts
},
rename: None,
}
.execute(member_ctx)?;
if !package.compilation_units.last().is_some_and(|p| p.kind.is_library()) {
packages.push((member_name.to_string(), package));
}
}
if packages.is_empty() {
return Err(crate::errors::custom("No deployable workspace members found (all are libraries).").into());
}
let network = get_network(&command.env_override.network)?;
match network {
NetworkName::TestnetV0 => {
handle_workspace_deploy_inner::<TestnetV0, AleoTestnetV0>(&command, context, network, packages)
}
NetworkName::MainnetV0 => {
#[cfg(feature = "only_testnet")]
panic!("Mainnet chosen with only_testnet feature");
#[cfg(not(feature = "only_testnet"))]
handle_workspace_deploy_inner::<MainnetV0, snarkvm::circuit::AleoV0>(&command, context, network, packages)
}
NetworkName::CanaryV0 => {
#[cfg(feature = "only_testnet")]
panic!("Canary chosen with only_testnet feature");
#[cfg(not(feature = "only_testnet"))]
handle_workspace_deploy_inner::<CanaryV0, snarkvm::circuit::AleoCanaryV0>(
&command, context, network, packages,
)
}
}
}
fn handle_workspace_deploy_inner<N: Network, A: Aleo<Network = N>>(
command: &LeoDeploy,
context: Context,
network: NetworkName,
packages: Vec<(String, Package)>,
) -> Result<DeployOutput> {
let setup = create_deploy_setup::<N>(command, &context, network)?;
let config = Some(Config {
address: setup.address.to_string(),
network: network.to_string(),
endpoint: Some(setup.endpoint.clone()),
consensus_version: Some(setup.consensus_version as u8),
});
let mut member_tasks: Vec<MemberTasks<N>> = Vec::new();
let mut all_remote: Vec<Task<N>> = Vec::new();
let mut seen_remote: HashMap<ProgramID<N>, Option<u16>> = HashMap::new();
for (name, package) in &packages {
let (local, remote, skipped) = prepare_package_tasks::<N>(command, &setup, package)?;
for task in remote {
match seen_remote.entry(task.id) {
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(task.edition);
all_remote.push(task);
}
std::collections::hash_map::Entry::Occupied(e) => {
if *e.get() != task.edition {
return Err(crate::errors::custom(format!(
"Workspace members disagree on edition for remote dependency '{}': \
{:?} vs {:?}. Ensure all members specify the same edition.",
task.id,
e.get(),
task.edition,
))
.into());
}
}
}
}
member_tasks.push((name.clone(), local, skipped));
}
for (_, local, skipped) in &member_tasks {
enforce_local_deploy_constructor_requirements(local, skipped, setup.consensus_version)?;
}
let mut all_warnings = Vec::new();
for (_, local, _) in &member_tasks {
all_warnings.extend(check_tasks_for_warnings(
&setup.endpoint,
network,
local,
setup.consensus_version,
command,
));
}
print_workspace_deployment_plan(&setup, &member_tasks, &all_remote, &all_warnings, command);
if !confirm("Do you want to proceed with deployment?", command.extra.yes)? {
println!("❌ Deployment aborted.");
return Ok(DeployOutput::default());
}
load_remote_deps(command, &setup, all_remote)?;
let mut already_deployed: HashSet<ProgramID<N>> = HashSet::new();
let mut all_transactions = Vec::new();
let mut all_stats = Vec::new();
for (name, local, skipped) in member_tasks {
println!("\n--- deploying workspace member '{name}' ---");
match generate_deploy_transactions::<N, A>(command, &setup, local, &skipped, &mut already_deployed)? {
Some((txns, stats)) => {
all_transactions.extend(txns);
all_stats.extend(stats);
}
None => return Ok(DeployOutput::default()),
}
}
output_and_broadcast(command, &setup, &context, config, &all_transactions, &all_stats)
}
fn print_workspace_deployment_plan<N: Network>(
setup: &DeploySetup<N>,
member_tasks: &[MemberTasks<N>],
all_remote: &[Task<N>],
warnings: &[String],
command: &LeoDeploy,
) {
println!("\n{}", "🛠️ Workspace Deployment Plan".bold());
println!("{}", "──────────────────────────────────────────────".dimmed());
println!("{}", "🔧 Configuration:".bold());
println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &setup.private_key.to_string()[..24]).yellow());
println!(" {:20}{}", "Address:".cyan(), format!("{}...", &setup.address.to_string()[..24]).yellow());
println!(" {:20}{}", "Endpoint:".cyan(), setup.endpoint.yellow());
println!(" {:20}{}", "Network:".cyan(), setup.network.to_string().yellow());
println!(" {:20}{}", "Consensus Version:".cyan(), (setup.consensus_version as u8).to_string().yellow());
let mut already_seen: HashSet<ProgramID<N>> = HashSet::new();
println!("\n{}", "📦 Workspace Members:".bold());
for (name, local, skipped) in member_tasks {
println!(" {}:", name.bold());
if local.is_empty() {
println!(" (no programs)");
continue;
}
for task in local {
if skipped.contains(&task.id) {
println!(" • {} {}", task.id.to_string().dimmed(), "(skipped by --skip)".dimmed());
} else if already_seen.contains(&task.id) {
println!(" • {} {}", task.id.to_string().dimmed(), "(already covered by earlier member)".dimmed());
} else {
let priority_fee_str = task.priority_fee.map_or("0".into(), |v| v.to_string());
let record_str = if task.record.is_some() { "yes" } else { "no (public fee)" };
println!(
" • {} │ priority fee: {} │ fee record: {}",
task.id.to_string().cyan(),
priority_fee_str,
record_str
);
already_seen.insert(task.id);
}
}
}
if !all_remote.is_empty() {
println!("\n{}", "🌐 Remote Dependencies:".bold().red());
println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
for task in all_remote {
println!(" • {}", task.id.to_string().dimmed());
}
}
println!("\n{}", "⚙️ Actions:".bold());
if command.action.print {
println!(" • Transaction(s) will be printed to the console.");
} else {
println!(" • Transaction(s) will NOT be printed to the console.");
}
if let Some(path) = &command.action.save {
println!(" • Transaction(s) will be saved to {}", path.bold());
} else {
println!(" • Transaction(s) will NOT be saved to a file.");
}
if command.action.broadcast {
println!(" • Transaction(s) will be broadcast to {}", setup.endpoint.bold());
} else {
println!(" • Transaction(s) will NOT be broadcast to the network.");
}
if !warnings.is_empty() {
println!("\n{}", "⚠️ Warnings:".bold().red());
for warning in warnings {
println!(" • {}", warning.dimmed());
}
}
println!("{}", "──────────────────────────────────────────────\n".dimmed());
}
fn consensus_limit(table: &[(ConsensusVersion, usize)], consensus_version: ConsensusVersion, latest: usize) -> usize {
table
.iter()
.take_while(|(version, _)| *version <= consensus_version)
.last()
.map(|(_, size)| *size)
.unwrap_or(latest)
}
pub(crate) fn max_program_size_for_consensus_version<N: Network>(consensus_version: ConsensusVersion) -> usize {
consensus_limit(&N::MAX_PROGRAM_SIZE, consensus_version, N::LATEST_MAX_PROGRAM_SIZE())
}
pub(crate) fn max_transaction_size_for_consensus_version<N: Network>(consensus_version: ConsensusVersion) -> usize {
consensus_limit(&N::MAX_TRANSACTION_SIZE, consensus_version, N::LATEST_MAX_TRANSACTION_SIZE())
}
pub(crate) fn warn_if_transaction_oversized<N: Network>(
id: &ProgramID<N>,
transaction: &Transaction<N>,
consensus_version: ConsensusVersion,
action: &str,
) {
let Ok(size) = transaction.to_bytes_le().map(|bytes| bytes.len()) else { return };
let max_size = max_transaction_size_for_consensus_version::<N>(consensus_version);
if size > max_size {
println!(
"⚠️ The '{id}' transaction is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The {action} will likely fail.",
size as f64 / 1024.0,
max_size as f64 / 1024.0,
consensus_version as u8,
);
}
}
fn check_tasks_for_warnings<N: Network>(
endpoint: &str,
network: NetworkName,
tasks: &[Task<N>],
consensus_version: ConsensusVersion,
command: &LeoDeploy,
) -> Vec<String> {
let mut warnings = Vec::new();
for Task { id, is_local, program, bytecode_size, .. } in tasks {
if !is_local || !command.action.broadcast {
continue;
}
if fetch_program_from_network(&id.to_string(), endpoint, network, command.env_override.network_retries).is_ok()
{
warnings
.push(format!("The program '{id}' already exists on the network. Please use `leo upgrade` instead.",));
}
if consensus_version >= ConsensusVersion::V7
&& let Err(e) = program.check_program_naming_structure()
{
warnings.push(format!(
"The program '{id}' has an invalid naming scheme: {e}. The deployment will likely fail."
));
}
if let Err(e) = program.check_restricted_keywords_for_consensus_version(consensus_version) {
warnings.push(format!(
"The program '{id}' contains restricted keywords for consensus version {}: {e}. The deployment will likely fail.",
consensus_version as u8
));
}
if consensus_version < ConsensusVersion::V9 && program.contains_v9_syntax() {
warnings.push(format!("The program '{id}' uses V9 features but the consensus version is less than V9. The deployment will likely fail"));
}
if consensus_version < ConsensusVersion::V15 && program.contains_v15_syntax() {
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"));
}
if consensus_version < ConsensusVersion::V16 && program.contains_v16_syntax() {
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"));
}
if consensus_version >= ConsensusVersion::V9 && !program.contains_constructor() {
warnings
.push(format!("The program '{id}' does not contain a constructor. The deployment will likely fail",));
}
let max_size = max_program_size_for_consensus_version::<N>(consensus_version);
if *bytecode_size > max_size {
warnings.push(format!(
"The program '{id}' is {:.2} KB, exceeding the {:.2} KB limit for consensus version {}. The deployment will likely fail.",
*bytecode_size as f64 / 1024.0,
max_size as f64 / 1024.0,
consensus_version as u8,
));
} else if let (_, _, Some(msg)) = format_program_size(*bytecode_size, max_size) {
warnings.push(format!("The program '{id}' is {msg}."));
}
}
if let Err(e) =
check_consensus_version_mismatch(consensus_version, endpoint, network, command.env_override.network_retries)
{
warnings.push(format!("{e}. In some cases, the deployment may fail"));
}
warnings
}
pub(crate) fn validate_deployment_limits<N: Network>(
deployment: &Deployment<N>,
program_id: &ProgramID<N>,
network: &NetworkName,
) -> Result<()> {
let combined_variables = deployment.num_combined_variables()?;
if combined_variables > N::MAX_DEPLOYMENT_VARIABLES {
return Err(crate::errors::variable_limit_exceeded(
program_id,
combined_variables,
N::MAX_DEPLOYMENT_VARIABLES,
network,
)
.into());
}
let constraints = deployment.num_combined_constraints()?;
if constraints > N::MAX_DEPLOYMENT_CONSTRAINTS {
return Err(crate::errors::constraint_limit_exceeded(
program_id,
constraints,
N::MAX_DEPLOYMENT_CONSTRAINTS,
network,
)
.into());
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn print_deployment_plan<N: Network>(
private_key: &PrivateKey<N>,
address: &Address<N>,
endpoint: &str,
network: &NetworkName,
local: &[Task<N>],
skipped: &HashSet<ProgramID<N>>,
remote: &[Task<N>],
warnings: &[String],
consensus_version: ConsensusVersion,
command: &LeoDeploy,
) {
use colored::*;
println!("\n{}", "🛠️ Deployment Plan Summary".bold());
println!("{}", "──────────────────────────────────────────────".dimmed());
println!("{}", "🔧 Configuration:".bold());
println!(" {:20}{}", "Private Key:".cyan(), format!("{}...", &private_key.to_string()[..24]).yellow());
println!(" {:20}{}", "Address:".cyan(), format!("{}...", &address.to_string()[..24]).yellow());
println!(" {:20}{}", "Endpoint:".cyan(), endpoint.yellow());
println!(" {:20}{}", "Network:".cyan(), network.to_string().yellow());
println!(" {:20}{}", "Consensus Version:".cyan(), (consensus_version as u8).to_string().yellow());
println!("\n{}", "📦 Deployment Tasks:".bold());
if local.is_empty() {
println!(" (none)");
} else {
for Task { id, priority_fee, record, .. } in local.iter().filter(|task| !skipped.contains(&task.id)) {
let priority_fee_str = priority_fee.map_or("0".into(), |v| v.to_string());
let record_str = if record.is_some() { "yes" } else { "no (public fee)" };
println!(
" • {} │ priority fee: {} │ fee record: {}",
id.to_string().cyan(),
priority_fee_str,
record_str
);
}
}
if !skipped.is_empty() {
println!("\n{}", "🚫 Skipped Programs:".bold().red());
for symbol in skipped {
println!(" • {}", symbol.to_string().dimmed());
}
}
if !remote.is_empty() {
println!("\n{}", "🌐 Remote Dependencies:".bold().red());
println!("{}", "(Leo will not generate transactions for these programs)".bold().red());
for Task { id, .. } in remote {
println!(" • {}", id.to_string().dimmed());
}
}
println!("\n{}", "⚙️ Actions:".bold());
if command.action.print {
println!(" • Transaction(s) will be printed to the console.");
} else {
println!(" • Transaction(s) will NOT be printed to the console.");
}
if let Some(path) = &command.action.save {
println!(" • Transaction(s) will be saved to {}", path.bold());
} else {
println!(" • Transaction(s) will NOT be saved to a file.");
}
if command.action.broadcast {
println!(" • Transaction(s) will be broadcast to {}", endpoint.bold());
} else {
println!(" • Transaction(s) will NOT be broadcast to the network.");
}
if !warnings.is_empty() {
println!("\n{}", "⚠️ Warnings:".bold().red());
for warning in warnings {
println!(" • {}", warning.dimmed());
}
}
println!("{}", "──────────────────────────────────────────────\n".dimmed());
}
pub(crate) fn compute_deployment_stats<N: Network, R: Rng + CryptoRng>(
vm: &VM<N, ConsensusMemory<N>>,
deployment: &Deployment<N>,
priority_fee: Option<u64>,
consensus_version: ConsensusVersion,
bytecode_size: usize,
rng: &mut R,
) -> Result<DeploymentStats> {
let variables = deployment.num_combined_variables()?;
let constraints = deployment.num_combined_constraints()?;
let (_, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
deployment_cost(vm.process(), deployment, consensus_version)?;
let priority = priority_fee.unwrap_or(0);
let function_costs = calculate_function_costs(vm, deployment, consensus_version, rng)?;
Ok(DeploymentStats {
program_size_bytes: bytecode_size,
max_program_size_bytes: max_program_size_for_consensus_version::<N>(consensus_version),
total_variables: Some(variables),
total_constraints: Some(constraints),
max_variables: Some(N::MAX_DEPLOYMENT_VARIABLES),
max_constraints: Some(N::MAX_DEPLOYMENT_CONSTRAINTS),
storage_cost,
synthesis_cost,
namespace_cost,
constructor_cost,
priority_fee: priority,
total_cost: storage_cost + synthesis_cost + namespace_cost + constructor_cost + priority,
function_costs,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn deploy_with_placeholder_certificate<N: Network, A: Aleo<Network = N>, R: Rng + CryptoRng>(
vm: &VM<N, ConsensusMemory<N>>,
private_key: &PrivateKey<N>,
program: &Program<N>,
edition: u16,
record: Option<Record<N, Plaintext<N>>>,
priority_fee: Option<u64>,
bytecode_size: usize,
consensus_version: ConsensusVersion,
query: &SnarkVMQuery<N, BlockMemory<N>>,
network_retries: u32,
rng: &mut R,
) -> Result<(Transaction<N>, DeploymentStats)> {
assert!(!program.functions().is_empty(), "Program `{}` has no functions", program.id());
let mut verifying_keys = Vec::with_capacity(program.functions().len() + program.records().len());
for function_name in program.functions().keys() {
let verifying_key = VerifyingKey::from_str(leo_compiler::run::PLACEHOLDER_VK)?;
let certificate = Certificate::from_str(leo_compiler::run::PLACEHOLDER_CERT)?;
verifying_keys.push((*function_name, (verifying_key, certificate)));
}
for record_name in program.records().keys() {
let verifying_key = VerifyingKey::from_str(leo_compiler::run::PLACEHOLDER_VK)?;
let certificate = Certificate::from_str(leo_compiler::run::PLACEHOLDER_CERT)?;
verifying_keys.push((*record_name, (verifying_key, certificate)));
}
let mut deployment = Deployment::new(edition, program.clone(), verifying_keys, None, None).unwrap();
deployment.set_program_owner_raw(Some(Address::try_from(private_key)?));
deployment.set_program_checksum_raw(Some(deployment.program().to_checksum()));
let deployment_id = deployment.to_deployment_id()?;
let owner = ProgramOwner::new(private_key, deployment_id, rng)?;
let (minimum_deployment_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)) =
deployment_cost(vm.process(), &deployment, consensus_version)?;
let fee_authorization = match record {
Some(record) => vm
.process()
.authorize_fee_private::<A, _>(
private_key,
record,
minimum_deployment_cost,
priority_fee.unwrap_or(0),
deployment_id,
rng,
)
.map_err(|e| anyhow::anyhow!("{e}"))?,
None => vm
.process()
.authorize_fee_public::<A, _>(
private_key,
minimum_deployment_cost,
priority_fee.unwrap_or(0),
deployment_id,
rng,
)
.map_err(|e| anyhow::anyhow!("{e}"))?,
};
let state_root = retry_network_call(network_retries, || query.current_state_root())?;
let fee = Fee::from(fee_authorization.transitions().into_iter().next().unwrap().1, state_root, None)?;
vm.process().lock().add_program(program)?;
let priority = priority_fee.unwrap_or(0);
let function_costs = calculate_function_costs(vm, &deployment, consensus_version, rng)?;
let stats = DeploymentStats {
program_size_bytes: bytecode_size,
max_program_size_bytes: max_program_size_for_consensus_version::<N>(consensus_version),
total_variables: None,
total_constraints: None,
max_variables: None,
max_constraints: None,
storage_cost,
synthesis_cost,
namespace_cost,
constructor_cost,
priority_fee: priority,
total_cost: storage_cost + synthesis_cost + namespace_cost + constructor_cost + priority,
function_costs,
};
let transaction = Transaction::from_deployment(owner, deployment, fee)?;
Ok((transaction, stats))
}
pub(crate) fn print_deployment_summary(program_id: &str, stats: &DeploymentStats) {
use colored::*;
println!("\n{} {}", "📊 Deployment Summary for".bold(), program_id.bold());
println!("{}", "──────────────────────────────────────────────".dimmed());
print!("{stats}");
println!("{}", "──────────────────────────────────────────────".dimmed());
}
pub(crate) fn calculate_function_costs<N: Network, R: Rng + CryptoRng>(
vm: &VM<N, ConsensusMemory<N>>,
deployment: &Deployment<N>,
consensus_version: ConsensusVersion,
rng: &mut R,
) -> Result<Vec<FunctionCostStats>> {
let stack = vm.process().get_stack(deployment.program().id())?;
let mut function_costs = Vec::new();
let sample_key = PrivateKey::new(rng)?;
let sample_address = Address::try_from(&sample_key)?;
for (function_name, _) in deployment.function_verifying_keys() {
let name = function_name.to_string();
let input_types = deployment.program().get_function(function_name)?.input_types();
let inputs = input_types
.into_iter()
.map(|ty| {
stack
.sample_value(&sample_address, &ty.into(), rng)
.map_err(|e| crate::errors::custom(format!("Failed to sample value: {e}")).into())
})
.collect::<Result<Vec<_>>>()?;
let (finalize_cost, storage_cost, execution_cost) =
match vm.authorize(&sample_key, deployment.program().id(), function_name, inputs.iter(), rng) {
Err(e) => {
tracing::debug!("Could not estimate execution cost for '{name}': {e}");
let static_finalize_cost = if consensus_version >= ConsensusVersion::V10 {
minimum_cost_in_microcredits_v3(&stack, function_name)?
} else if consensus_version >= ConsensusVersion::V2 {
minimum_cost_in_microcredits_v2(&stack, function_name)?
} else {
minimum_cost_in_microcredits_v1(&stack, function_name)?
};
(static_finalize_cost, None, None)
}
Ok(authorization) => {
let (total, (storage, finalize)) =
execution_cost_for_authorization(vm.process(), &authorization, consensus_version)?;
(finalize, Some(storage), Some(total))
}
};
let has_dynamic_calls = stack.contains_dynamic_call(function_name).unwrap_or(false);
function_costs.push(FunctionCostStats { name, finalize_cost, storage_cost, execution_cost, has_dynamic_calls });
}
Ok(function_costs)
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn task_from_source(source: &str) -> Task<TestnetV0> {
let program = Program::<TestnetV0>::from_str(source).unwrap();
Task {
id: *program.id(),
program,
edition: None,
is_local: true,
priority_fee: None,
record: None,
bytecode_size: source.len(),
}
}
#[test]
fn rejects_unskipped_local_deploy_without_constructor() {
let task = task_from_source(concat!(
"program missing_constructor.aleo;\n",
"function main:\n",
" input r0 as u32.public;\n",
" output r0 as u32.public;\n",
));
let result = enforce_local_deploy_constructor_requirements(&[task], &HashSet::new(), ConsensusVersion::V9);
assert!(result.is_err());
}
#[test]
fn allows_skipped_local_deploy_without_constructor() {
let task = task_from_source(concat!(
"program skipped_constructor.aleo;\n",
"function main:\n",
" input r0 as u32.public;\n",
" output r0 as u32.public;\n",
));
let skipped = HashSet::from([task.id]);
enforce_local_deploy_constructor_requirements(&[task], &skipped, ConsensusVersion::V9).unwrap();
}
#[test]
fn allows_local_deploy_without_constructor_before_v9() {
let task = task_from_source(concat!(
"program pre_constructor_gate.aleo;\n",
"function main:\n",
" input r0 as u32.public;\n",
" output r0 as u32.public;\n",
));
enforce_local_deploy_constructor_requirements(&[task], &HashSet::new(), ConsensusVersion::V8).unwrap();
}
#[test]
fn program_size_limit_tracks_consensus_version() {
assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V1), 100_000);
assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V13), 100_000);
assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V14), 512_000);
assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V15), 512_000);
assert_eq!(max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::V16), 2_048_000);
assert_eq!(
max_program_size_for_consensus_version::<TestnetV0>(ConsensusVersion::latest()),
TestnetV0::LATEST_MAX_PROGRAM_SIZE()
);
}
}