Skip to content

Commit 47e39ad

Browse files
authored
[Fix] Account for dynamic dispatch in CLI stats. (#29258)
1 parent 70ea0c4 commit 47e39ad

File tree

14 files changed

+300
-76
lines changed

14 files changed

+300
-76
lines changed

crates/leo/src/cli/commands/common/output.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,17 @@ pub(crate) fn microcredits_to_credits(microcredits: u64) -> f64 {
3030
pub struct FunctionCostStats {
3131
pub name: String,
3232
pub finalize_cost: u64,
33-
/// The cost of proving the execution transition (execution_cost - finalize_cost).
33+
/// The storage cost of the execution transition (execution_cost - finalize_cost).
3434
/// `None` when authorization sampling fails (e.g. functions requiring specific record types).
3535
#[serde(skip_serializing_if = "Option::is_none")]
36-
pub proof_cost: Option<u64>,
36+
pub storage_cost: Option<u64>,
3737
/// The total execution cost (finalize + proof).
3838
/// `None` when authorization sampling fails (e.g. functions requiring specific record types).
3939
#[serde(skip_serializing_if = "Option::is_none")]
4040
pub execution_cost: Option<u64>,
41+
/// Whether this function uses dynamic calls, making the finalize cost a lower bound.
42+
#[serde(skip_serializing_if = "std::ops::Not::not")]
43+
pub has_dynamic_calls: bool,
4144
}
4245

4346
/// Statistics for a deployed program.
@@ -123,15 +126,46 @@ impl fmt::Display for DeploymentStats {
123126

124127
for fc in &self.function_costs {
125128
writeln!(f, "{}", format!(" Function '{}'", fc.name).bold())?;
129+
let bound = if fc.has_dynamic_calls { " (lower bound)" } else { "" };
126130
if let Some(execution_cost) = fc.execution_cost {
127-
writeln!(f, " {:24}{:.6}", "Total Execution Cost:".cyan(), microcredits_to_credits(execution_cost))?;
128-
writeln!(f, " {:24}{:.6}", "|- Finalize Cost:".cyan(), microcredits_to_credits(fc.finalize_cost))?;
129-
if let Some(proof_cost) = fc.proof_cost {
130-
writeln!(f, " {:24}{:.6}", "|- Proof Cost:".cyan(), microcredits_to_credits(proof_cost))?;
131+
writeln!(
132+
f,
133+
" {:24}{:.6}{}",
134+
"Total Execution Cost:".cyan(),
135+
microcredits_to_credits(execution_cost),
136+
bound.dimmed()
137+
)?;
138+
if let Some(storage_cost) = fc.storage_cost {
139+
writeln!(
140+
f,
141+
" {:24}{:.6}{}",
142+
"|- Storage Cost:".cyan(),
143+
microcredits_to_credits(storage_cost),
144+
bound.dimmed()
145+
)?;
131146
}
147+
writeln!(
148+
f,
149+
" {:24}{:.6}{}",
150+
"|- Finalize Cost:".cyan(),
151+
microcredits_to_credits(fc.finalize_cost),
152+
bound.dimmed()
153+
)?;
132154
} else {
133-
writeln!(f, " {:24}{}", "Total Execution Cost:".cyan(), "Undetermined".dimmed())?;
134-
writeln!(f, " {:24}{:.6}", "|- Finalize Cost:".cyan(), microcredits_to_credits(fc.finalize_cost))?;
155+
writeln!(
156+
f,
157+
" {:24}{:.6} (lower bound)",
158+
"Total Execution Cost:".cyan(),
159+
microcredits_to_credits(fc.finalize_cost),
160+
)?;
161+
writeln!(f, " {:24}{}", "|- Storage Cost:".cyan(), "N/A (dynamic call)".dimmed())?;
162+
writeln!(
163+
f,
164+
" {:24}{:.6}{}",
165+
"|- Finalize Cost:".cyan(),
166+
microcredits_to_credits(fc.finalize_cost),
167+
bound.dimmed()
168+
)?;
135169
}
136170
}
137171
Ok(())

crates/leo/src/cli/commands/deploy.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -827,16 +827,9 @@ pub(crate) fn calculate_function_costs<N: Network, R: Rng + CryptoRng>(
827827
for (function_name, _) in deployment.function_verifying_keys() {
828828
let name = function_name.to_string();
829829

830-
// Compute the finalize cost based on the consensus version.
831-
let finalize_cost = if consensus_version >= ConsensusVersion::V10 {
832-
minimum_cost_in_microcredits_v3(&stack, function_name)?
833-
} else if consensus_version >= ConsensusVersion::V2 {
834-
minimum_cost_in_microcredits_v2(&stack, function_name)?
835-
} else {
836-
minimum_cost_in_microcredits_v1(&stack, function_name)?
837-
};
838-
839830
// Sample inputs and attempt authorization to estimate execution cost (best-effort).
831+
// When authorization succeeds, use the breakdown directly from snarkVM.
832+
// When it fails, fall back to the static finalize cost.
840833
let input_types = deployment.program().get_function(function_name)?.input_types();
841834
let inputs = input_types
842835
.into_iter()
@@ -846,20 +839,32 @@ pub(crate) fn calculate_function_costs<N: Network, R: Rng + CryptoRng>(
846839
.map_err(|e| CliError::custom(format!("Failed to sample value: {e}")).into())
847840
})
848841
.collect::<Result<Vec<_>>>()?;
849-
let execution_cost =
842+
let (finalize_cost, storage_cost, execution_cost) =
850843
match vm.authorize(&sample_key, deployment.program().id(), function_name, inputs.iter(), rng) {
851844
Err(e) => {
852845
tracing::debug!("Could not estimate execution cost for '{name}': {e}");
853-
None
846+
// Fall back to static finalize cost analysis.
847+
let static_finalize_cost = if consensus_version >= ConsensusVersion::V10 {
848+
minimum_cost_in_microcredits_v3(&stack, function_name)?
849+
} else if consensus_version >= ConsensusVersion::V2 {
850+
minimum_cost_in_microcredits_v2(&stack, function_name)?
851+
} else {
852+
minimum_cost_in_microcredits_v1(&stack, function_name)?
853+
};
854+
(static_finalize_cost, None, None)
854855
}
855856
Ok(authorization) => {
856-
Some(execution_cost_for_authorization(&vm.process().read(), &authorization, consensus_version)?.0)
857+
let (total, (storage, finalize)) =
858+
execution_cost_for_authorization(&vm.process().read(), &authorization, consensus_version)?;
859+
(finalize, Some(storage), Some(total))
857860
}
858861
};
859862

860-
let proof_cost = execution_cost.map(|ec| ec.saturating_sub(finalize_cost));
863+
// Check if this function (or any function it calls) uses dynamic dispatch.
864+
// Dynamic calls make costs a lower bound since the target is resolved at runtime.
865+
let has_dynamic_calls = stack.contains_dynamic_call(function_name).unwrap_or(false);
861866

862-
function_costs.push(FunctionCostStats { name, finalize_cost, proof_cost, execution_cost });
867+
function_costs.push(FunctionCostStats { name, finalize_cost, storage_cost, execution_cost, has_dynamic_calls });
863868
}
864869

865870
Ok(function_costs)

crates/leo/src/cli/commands/synthesize.rs

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -230,19 +230,27 @@ fn handle_synthesize<A: Aleo>(
230230
Ok(hex)
231231
};
232232

233+
// Collect record names for translation key synthesis.
234+
let record_names: Vec<_> = stack.program().records().keys().cloned().collect();
235+
233236
println!("\n🌱 Synthesizing the following keys in {program_id}:");
234237
for id in &function_ids {
235-
println!(" - {id}");
238+
println!(" - {id} (function)");
239+
}
240+
for name in &record_names {
241+
println!(" - {name} (record translation)");
236242
}
237243

238244
let mut synthesized_functions = Vec::new();
239245

240-
for function_id in function_ids {
241-
stack.synthesize_key::<A, _>(function_id, rng)?;
242-
let proving_key = stack.get_proving_key(function_id)?;
243-
let verifying_key = stack.get_verifying_key(function_id)?;
246+
// Helper: process a synthesized key (function or translation), print circuit info,
247+
// record metadata, and optionally save keys to disk.
248+
let mut process_key = |name: &str, label: &str| -> Result<()> {
249+
let name_id = snarkvm::prelude::Identifier::<A::Network>::from_str(name)?;
250+
let proving_key = stack.get_proving_key(&name_id)?;
251+
let verifying_key = stack.get_verifying_key(&name_id)?;
244252

245-
println!("\n🔑 Synthesized keys for {program_id}/{function_id} (edition {edition})");
253+
println!("\n🔑 Synthesized {label} for {program_id}/{name} (edition {edition})");
246254
println!("ℹ️ Circuit Information:");
247255
println!(" - Public Inputs: {}", verifying_key.circuit_info.num_public_inputs);
248256
println!(" - Variables: {}", verifying_key.circuit_info.num_public_and_private_variables);
@@ -252,13 +260,11 @@ fn handle_synthesize<A: Aleo>(
252260
println!(" - Non-Zero Entries in C: {}", verifying_key.circuit_info.num_non_zero_c);
253261
println!(" - Circuit ID: {}", verifying_key.id);
254262

255-
// Get the checksums of the keys.
256263
let prover_bytes = proving_key.to_bytes_le()?;
257264
let verifier_bytes = verifying_key.to_bytes_le()?;
258265
let prover_checksum = hash(&prover_bytes)?;
259266
let verifier_checksum = hash(&verifier_bytes)?;
260267

261-
// Construct the metadata.
262268
let metadata = Metadata {
263269
prover_checksum,
264270
prover_size: prover_bytes.len(),
@@ -279,44 +285,44 @@ fn handle_synthesize<A: Aleo>(
279285
};
280286

281287
synthesized_functions.push(SynthesizedFunction {
282-
name: function_id.to_string(),
288+
name: name.to_string(),
283289
circuit_info,
284290
metadata: metadata.clone(),
285291
});
286292

287-
// A helper to write to a file.
288-
let write_to_file = |path: PathBuf, data: &[u8]| -> Result<()> {
289-
std::fs::write(path, data).map_err(|e| CliError::custom(format!("Failed to write to file: {e}")))?;
290-
Ok(())
291-
};
292-
293-
// If the `save` option is set, save the proving and verifying keys to a file in the specified directory.
294-
// The file format is `program_id.function_id.edition_or_local.type.timestamp`.
295-
// The directory is created if it doesn't exist.
296293
if let Some(path) = &command.action.save {
297-
// Create the directory if it doesn't exist.
298294
std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
299-
// Get the current timestamp.
300295
let timestamp = chrono::Utc::now().timestamp();
301-
// The edition.
302-
let edition = if command.local { "local".to_string() } else { edition.to_string() };
303-
// The prefix for the file names.
304-
let prefix = format!("{network}.{program_id}.{function_id}.{edition}");
305-
// Get the file paths.
296+
let edition_str = if command.local { "local".to_string() } else { edition.to_string() };
297+
let prefix = format!("{network}.{program_id}.{name}.{edition_str}");
306298
let prover_file_path = PathBuf::from(path).join(format!("{prefix}.prover.{timestamp}"));
307299
let verifier_file_path = PathBuf::from(path).join(format!("{prefix}.verifier.{timestamp}"));
308-
let metadata_file_path = PathBuf::from(path)
309-
.join(format!("{network}.{program_id}.{function_id}.{edition}.metadata.{timestamp}"));
310-
// Print the save location.
300+
let metadata_file_path = PathBuf::from(path).join(format!("{prefix}.metadata.{timestamp}"));
311301
println!(
312-
"💾 Saving proving key, verifying key, and metadata to: {}/{network}.{program_id}.{function_id}.{edition}.prover|verifier|metadata.{timestamp}",
302+
"💾 Saving {label} to: {}/{prefix}.prover|verifier|metadata.{timestamp}",
313303
metadata_file_path.parent().unwrap().display()
314304
);
315-
// Save the keys.
316-
write_to_file(prover_file_path, &prover_bytes)?;
317-
write_to_file(verifier_file_path, &verifier_bytes)?;
318-
write_to_file(metadata_file_path, metadata_pretty.as_bytes())?;
305+
std::fs::write(&prover_file_path, &prover_bytes)
306+
.map_err(|e| CliError::custom(format!("Failed to write to file: {e}")))?;
307+
std::fs::write(&verifier_file_path, &verifier_bytes)
308+
.map_err(|e| CliError::custom(format!("Failed to write to file: {e}")))?;
309+
std::fs::write(&metadata_file_path, metadata_pretty.as_bytes())
310+
.map_err(|e| CliError::custom(format!("Failed to write to file: {e}")))?;
319311
}
312+
313+
Ok(())
314+
};
315+
316+
// Synthesize function keys.
317+
for function_id in function_ids {
318+
stack.synthesize_key::<A, _>(function_id, rng)?;
319+
process_key(&function_id.to_string(), "keys")?;
320+
}
321+
322+
// Synthesize record translation keys.
323+
for record_name in &record_names {
324+
stack.synthesize_translation_key::<A, _>(record_name, rng)?;
325+
process_key(&record_name.to_string(), "translation key")?;
320326
}
321327

322328
Ok(SynthesizeOutput { program: program_id.to_string(), functions: synthesized_functions })

tests/expectations/cli/broadcast_error/STDOUT

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ Once it is deployed, it CANNOT be changed.
5858
Total Fee: 1.946118
5959
Function 'main'
6060
Total Execution Cost: 0.001326
61+
|- Storage Cost: 0.001326
6162
|- Finalize Cost: 0.000000
62-
|- Proof Cost: 0.001326
6363
──────────────────────────────────────────────
6464

6565
📡 Broadcasting deployment for broadcast_error.aleo...
@@ -138,8 +138,8 @@ Once it is deployed, it CANNOT be changed.
138138
Total Fee: 1.946118
139139
Function 'main'
140140
Total Execution Cost: 0.001326
141+
|- Storage Cost: 0.001326
141142
|- Finalize Cost: 0.000000
142-
|- Proof Cost: 0.001326
143143
──────────────────────────────────────────────
144144

145145
📡 Broadcasting deployment for broadcast_error.aleo...

tests/expectations/cli/network_dependency/STDOUT

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ Once it is deployed, it CANNOT be changed.
5959
Total Fee: 1.897896
6060
Function 'main'
6161
Total Execution Cost: 0.001283
62+
|- Storage Cost: 0.001283
6263
|- Finalize Cost: 0.000000
63-
|- Proof Cost: 0.001283
6464
──────────────────────────────────────────────
6565

6666
📡 Broadcasting deployment for test_program1.aleo...
@@ -141,8 +141,8 @@ Once it is deployed, it CANNOT be changed.
141141
Total Fee: 1.954701
142142
Function 'main'
143143
Total Execution Cost: 0.001994
144+
|- Storage Cost: 0.001994
144145
|- Finalize Cost: 0.000000
145-
|- Proof Cost: 0.001994
146146
──────────────────────────────────────────────
147147

148148
📡 Broadcasting deployment for test_program2.aleo...

tests/expectations/cli/test_deploy/STDOUT

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

22
Leo 🔨 Compiling 'some_sample_leo_program.aleo'
3-
Leo 2 statements before dead code elimination.
4-
Leo 2 statements after dead code elimination.
5-
Leo The program checksum is: '[95u8, 99u8, 136u8, 243u8, 82u8, 169u8, 200u8, 72u8, 133u8, 227u8, 122u8, 161u8, 195u8, 178u8, 69u8, 37u8, 167u8, 114u8, 104u8, 192u8, 161u8, 175u8, 195u8, 180u8, 120u8, 4u8, 192u8, 16u8, 86u8, 199u8, 76u8, 235u8]'.
6-
Leo Program size: 0.20 KB / 500.00 KB
3+
Leo 8 statements before dead code elimination.
4+
Leo 8 statements after dead code elimination.
5+
Leo The program checksum is: '[109u8, 115u8, 37u8, 36u8, 104u8, 211u8, 71u8, 47u8, 167u8, 121u8, 76u8, 58u8, 12u8, 161u8, 114u8, 2u8, 167u8, 69u8, 23u8, 209u8, 243u8, 251u8, 90u8, 233u8, 85u8, 135u8, 145u8, 25u8, 18u8, 86u8, 72u8, 144u8]'.
6+
Leo Program size: 0.68 KB / 500.00 KB
77
Leo ✅ Compiled 'some_sample_leo_program.aleo' into Aleo instructions.
88
Leo ✅ Generated ABI at 'build/abi.json'.
99

@@ -43,23 +43,31 @@ Once it is deployed, it CANNOT be changed.
4343

4444
📊 Deployment Summary for some_sample_leo_program.aleo
4545
──────────────────────────────────────────────
46-
Program Size: 0.20 KB / 500.00 KB
47-
Total Variables: 17,516
48-
Total Constraints: 12,927
46+
Program Size: 0.68 KB / 500.00 KB
47+
Total Variables: 99,771
48+
Total Constraints: 76,101
4949
Max Variables: XXXXXX
5050
Max Constraints: XXXXXX
5151

5252
💰 Cost Breakdown (credits)
53-
Transaction Storage: 0.892000
54-
Program Synthesis: 0.030443
53+
Transaction Storage: 3.270000
54+
Program Synthesis: 0.175872
5555
Namespace: 1.000000
5656
Constructor: 0.002000
5757
Priority Fee: 0.000000
58-
Total Fee: 1.924443
58+
Total Fee: 4.447872
5959
Function 'main'
6060
Total Execution Cost: 0.001334
61+
|- Storage Cost: 0.001334
6162
|- Finalize Cost: 0.000000
62-
|- Proof Cost: 0.001334
63+
Function 'mint'
64+
Total Execution Cost: 0.001536
65+
|- Storage Cost: 0.001536
66+
|- Finalize Cost: 0.000000
67+
Function 'dynamic_transfer'
68+
Total Execution Cost: 0.000000 (lower bound)
69+
|- Storage Cost: N/A (dynamic call)
70+
|- Finalize Cost: 0.000000 (lower bound)
6371
──────────────────────────────────────────────
6472

6573
📡 Broadcasting deployment for some_sample_leo_program.aleo...

0 commit comments

Comments
 (0)