leo_lang/cli/commands/
synthesize.rs

1// Copyright (C) 2019-2025 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use super::*;
18
19use leo_ast::NetworkName;
20use leo_package::{Package, ProgramData};
21
22use aleo_std::StorageMode;
23
24#[cfg(not(feature = "only_testnet"))]
25use snarkvm::circuit::{AleoCanaryV0, AleoV0};
26use snarkvm::{
27    algorithms::crypto_hash::sha256,
28    circuit::{Aleo, AleoTestnetV0},
29    prelude::{
30        ProgramID,
31        ToBytes,
32        VM,
33        store::{ConsensusStore, helpers::memory::ConsensusMemory},
34    },
35    synthesizer::program::StackTrait,
36};
37
38use clap::Parser;
39use serde::Serialize;
40use std::{fmt::Write, path::PathBuf};
41
42#[derive(Serialize)]
43pub struct Metadata {
44    pub prover_checksum: String,
45    pub prover_size: usize,
46    pub verifier_checksum: String,
47    pub verifier_size: usize,
48}
49
50/// Synthesize proving and verifying keys for a given function.
51#[derive(Parser, Debug)]
52pub struct LeoSynthesize {
53    #[clap(name = "NAME", help = "The name of the program to synthesize, e.g `helloworld.aleo`")]
54    pub(crate) program_name: String,
55    #[arg(short, long, help = "Use the local Leo project.")]
56    pub(crate) local: bool,
57    #[arg(short, long, help = "Skip functions that contain any of the given substrings")]
58    pub(crate) skip: Vec<String>,
59    #[clap(flatten)]
60    pub(crate) action: TransactionAction,
61    #[clap(flatten)]
62    pub(crate) env_override: EnvOptions,
63    #[clap(flatten)]
64    build_options: BuildOptions,
65}
66
67impl Command for LeoSynthesize {
68    type Input = Option<Package>;
69    type Output = ();
70
71    fn log_span(&self) -> Span {
72        tracing::span!(tracing::Level::INFO, "Leo")
73    }
74
75    fn prelude(&self, context: Context) -> Result<Self::Input> {
76        // If the `--local` option is enabled, then build the project.
77        if self.local {
78            let package = LeoBuild {
79                env_override: self.env_override.clone(),
80                options: {
81                    let mut options = self.build_options.clone();
82                    options.no_cache = true;
83                    options
84                },
85            }
86            .execute(context)?;
87            // Return the package.
88            Ok(Some(package))
89        } else {
90            Ok(None)
91        }
92    }
93
94    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
95        // Verify that the transaction action is not "broadcast" or "print"
96        if self.action.broadcast {
97            println!(
98                "❌ `--broadcast` is not a valid option for `leo synthesize`. Please use `--save` and specify a valid directory."
99            );
100            return Ok(());
101        } else if self.action.print {
102            println!(
103                "❌ `--print` is not a valid option for `leo synthesize`. Please use `--save` and specify a valid directory."
104            );
105            return Ok(());
106        }
107
108        // Get the network, accounting for overrides.
109        let network = get_network(&self.env_override.network)?;
110        // Handle each network with the appropriate parameterization.
111        match network {
112            NetworkName::TestnetV0 => handle_synthesize::<AleoTestnetV0>(self, context, network, input),
113            NetworkName::MainnetV0 => {
114                #[cfg(feature = "only_testnet")]
115                panic!("Mainnet chosen with only_testnet feature");
116                #[cfg(not(feature = "only_testnet"))]
117                handle_synthesize::<AleoV0>(self, context, network, input)
118            }
119            NetworkName::CanaryV0 => {
120                #[cfg(feature = "only_testnet")]
121                panic!("Canary chosen with only_testnet feature");
122                #[cfg(not(feature = "only_testnet"))]
123                handle_synthesize::<AleoCanaryV0>(self, context, network, input)
124            }
125        }
126    }
127}
128
129// A helper function to handle the `synthesize` command.
130fn handle_synthesize<A: Aleo>(
131    command: LeoSynthesize,
132    context: Context,
133    network: NetworkName,
134    package: Option<Package>,
135) -> Result<<LeoSynthesize as Command>::Output> {
136    // Get the endpoint, accounting for overrides.
137    let endpoint = get_endpoint(&command.env_override.endpoint)?;
138
139    // Parse the program name as a `ProgramID`.
140    let program_id = ProgramID::<A::Network>::from_str(&command.program_name)
141        .map_err(|e| CliError::custom(format!("Failed to parse program name: {e}")))?;
142
143    // Get all the dependencies in the package if it exists.
144    // Get the programs and optional manifests for all programs.
145    let programs = if let Some(package) = &package {
146        // Get the package directories.
147        let build_directory = package.build_directory();
148        let imports_directory = package.imports_directory();
149        let source_directory = package.source_directory();
150        // Get the program names and their bytecode.
151        package
152            .programs
153            .iter()
154            .clone()
155            .map(|program| {
156                let program_id = ProgramID::<A::Network>::from_str(&format!("{}.aleo", program.name))
157                    .map_err(|e| CliError::custom(format!("Failed to parse program ID: {e}")))?;
158                match &program.data {
159                    ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), program.edition)),
160                    ProgramData::SourcePath { source, .. } => {
161                        // Get the path to the built bytecode.
162                        let bytecode_path = if source.as_path() == source_directory.join("main.leo") {
163                            build_directory.join("main.aleo")
164                        } else {
165                            imports_directory.join(format!("{}.aleo", program.name))
166                        };
167                        // Fetch the bytecode.
168                        let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
169                            CliError::custom(format!("Failed to read bytecode at {}: {e}", bytecode_path.display()))
170                        })?;
171                        // Return the bytecode and the manifest.
172                        Ok((program_id, bytecode, program.edition))
173                    }
174                }
175            })
176            .collect::<Result<Vec<_>>>()?
177    } else {
178        Vec::new()
179    };
180
181    // Parse the program strings into AVM programs.
182    let mut programs = programs
183        .into_iter()
184        .map(|(_, bytecode, edition)| {
185            // Parse the program.
186            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
187                .map_err(|e| CliError::custom(format!("Failed to parse program: {e}")))?;
188            // Return the program and its name.
189            Ok((program, edition))
190        })
191        .collect::<Result<Vec<_>>>()?;
192
193    // Determine whether the program is local or remote.
194    let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
195
196    // Initialize an RNG.
197    let rng = &mut rand::thread_rng();
198
199    // Initialize a new VM.
200    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
201
202    // If the program is not local, then download it and its dependencies for the network.
203    // Note: The dependencies are downloaded in "post-order" (child before parent).
204    if !is_local {
205        println!("⬇️ Downloading {program_id} and its dependencies from {endpoint}...");
206        programs = load_latest_programs_from_network(&context, program_id, network, &endpoint)?;
207    };
208
209    // Add the programs to the VM.
210    println!("\n➕ Adding programs to the VM in the following order:");
211    let programs_and_editions = programs
212        .into_iter()
213        .map(|(program, edition)| {
214            // Note: We default to edition 1 since snarkVM execute may produce spurious errors if the program does not have a constructor but uses edition 0.
215            let is_default = edition.is_none();
216            let edition = edition.unwrap_or(1);
217            // Get the program ID.
218            let id = program.id().to_string();
219            // Print the program ID and edition.
220            match id == "credits.aleo" {
221                true => println!("  - {id} (already included)"),
222                false => match is_default {
223                    true => println!(" - {id} (defaulting to edition {edition})"),
224                    false => println!("  - {id} (edition: {edition})"),
225                },
226            }
227            (program, edition)
228        })
229        .collect::<Vec<_>>();
230    vm.process().write().add_programs_with_editions(&programs_and_editions)?;
231
232    // Get the edition and function IDs from the program.
233    let stack = vm.process().read().get_stack(program_id)?;
234    let edition = *stack.program_edition();
235    let function_ids = stack
236        .program()
237        .functions()
238        .keys()
239        .filter(|id| !command.skip.iter().any(|substring| id.to_string().contains(substring)))
240        .collect::<Vec<_>>();
241
242    // A helper function to hash the keys.
243    let hash = |bytes: &[u8]| -> anyhow::Result<String> {
244        let digest = sha256(bytes);
245        let mut hex = String::new();
246        for byte in digest {
247            write!(&mut hex, "{byte:02x}")?;
248        }
249        Ok(hex)
250    };
251
252    println!("\n🌱 Synthesizing the following keys in {program_id}:");
253    for id in &function_ids {
254        println!("    - {id}");
255    }
256
257    for function_id in function_ids {
258        stack.synthesize_key::<A, _>(function_id, rng)?;
259        let proving_key = stack.get_proving_key(function_id)?;
260        let verifying_key = stack.get_verifying_key(function_id)?;
261
262        println!("\n🔑 Synthesized keys for {program_id}/{function_id} (edition {edition})");
263        println!("ℹ️ Circuit Information:");
264        println!("    - Public Inputs: {}", verifying_key.circuit_info.num_public_inputs);
265        println!("    - Variables: {}", verifying_key.circuit_info.num_public_and_private_variables);
266        println!("    - Constraints: {}", verifying_key.circuit_info.num_constraints);
267        println!("    - Non-Zero Entries in A: {}", verifying_key.circuit_info.num_non_zero_a);
268        println!("    - Non-Zero Entries in B: {}", verifying_key.circuit_info.num_non_zero_b);
269        println!("    - Non-Zero Entries in C: {}", verifying_key.circuit_info.num_non_zero_c);
270        println!("    - Circuit ID: {}", verifying_key.id);
271
272        // Get the checksums of the keys.
273        let prover_bytes = proving_key.to_bytes_le()?;
274        let verifier_bytes = verifying_key.to_bytes_le()?;
275        let prover_checksum = hash(&prover_bytes)?;
276        let verifier_checksum = hash(&verifier_bytes)?;
277
278        // Construct the metadata.
279        let metadata = Metadata {
280            prover_checksum,
281            prover_size: prover_bytes.len(),
282            verifier_checksum,
283            verifier_size: verifier_bytes.len(),
284        };
285        let metadata_pretty = serde_json::to_string_pretty(&metadata)
286            .map_err(|e| CliError::custom(format!("Failed to serialize metadata: {e}")))?;
287
288        // A helper to write to a file.
289        let write_to_file = |path: PathBuf, data: &[u8]| -> Result<()> {
290            std::fs::write(path, data).map_err(|e| CliError::custom(format!("Failed to write to file: {e}")))?;
291            Ok(())
292        };
293
294        // If the `save` option is set, save the proving and verifying keys to a file in the specified directory.
295        // The file format is `program_id.function_id.edition_or_local.type.timestamp`.
296        // The directory is created if it doesn't exist.
297        if let Some(path) = &command.action.save {
298            // Create the directory if it doesn't exist.
299            std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
300            // Get the current timestamp.
301            let timestamp = chrono::Utc::now().timestamp();
302            // The edition.
303            let edition = if command.local { "local".to_string() } else { edition.to_string() };
304            // The prefix for the file names.
305            let prefix = format!("{network}.{program_id}.{function_id}.{edition}");
306            // Get the file paths.
307            let prover_file_path = PathBuf::from(path).join(format!("{prefix}.prover.{timestamp}"));
308            let verifier_file_path = PathBuf::from(path).join(format!("{prefix}.verifier.{timestamp}"));
309            let metadata_file_path = PathBuf::from(path)
310                .join(format!("{network}.{program_id}.{function_id}.{edition}.metadata.{timestamp}"));
311            // Print the save location.
312            println!(
313                "💾 Saving proving key, verifying key, and metadata to: {}/{network}.{program_id}.{function_id}.{edition}.prover|verifier|metadata.{timestamp}",
314                metadata_file_path.parent().unwrap().display()
315            );
316            // Save the keys.
317            write_to_file(prover_file_path, &prover_bytes)?;
318            write_to_file(verifier_file_path, &verifier_bytes)?;
319            write_to_file(metadata_file_path, metadata_pretty.as_bytes())?;
320        }
321    }
322
323    Ok(())
324}