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            print_program_source(&program.id().to_string(), edition);
215            let edition = edition.unwrap_or(LOCAL_PROGRAM_DEFAULT_EDITION);
216            (program, edition)
217        })
218        .collect::<Vec<_>>();
219    vm.process().write().add_programs_with_editions(&programs_and_editions)?;
220
221    // Get the edition and function IDs from the program.
222    let stack = vm.process().read().get_stack(program_id)?;
223    let edition = *stack.program_edition();
224    let function_ids = stack
225        .program()
226        .functions()
227        .keys()
228        .filter(|id| !command.skip.iter().any(|substring| id.to_string().contains(substring)))
229        .collect::<Vec<_>>();
230
231    // A helper function to hash the keys.
232    let hash = |bytes: &[u8]| -> anyhow::Result<String> {
233        let digest = sha256(bytes);
234        let mut hex = String::new();
235        for byte in digest {
236            write!(&mut hex, "{byte:02x}")?;
237        }
238        Ok(hex)
239    };
240
241    println!("\n🌱 Synthesizing the following keys in {program_id}:");
242    for id in &function_ids {
243        println!("    - {id}");
244    }
245
246    for function_id in function_ids {
247        stack.synthesize_key::<A, _>(function_id, rng)?;
248        let proving_key = stack.get_proving_key(function_id)?;
249        let verifying_key = stack.get_verifying_key(function_id)?;
250
251        println!("\n🔑 Synthesized keys for {program_id}/{function_id} (edition {edition})");
252        println!("ℹ️ Circuit Information:");
253        println!("    - Public Inputs: {}", verifying_key.circuit_info.num_public_inputs);
254        println!("    - Variables: {}", verifying_key.circuit_info.num_public_and_private_variables);
255        println!("    - Constraints: {}", verifying_key.circuit_info.num_constraints);
256        println!("    - Non-Zero Entries in A: {}", verifying_key.circuit_info.num_non_zero_a);
257        println!("    - Non-Zero Entries in B: {}", verifying_key.circuit_info.num_non_zero_b);
258        println!("    - Non-Zero Entries in C: {}", verifying_key.circuit_info.num_non_zero_c);
259        println!("    - Circuit ID: {}", verifying_key.id);
260
261        // Get the checksums of the keys.
262        let prover_bytes = proving_key.to_bytes_le()?;
263        let verifier_bytes = verifying_key.to_bytes_le()?;
264        let prover_checksum = hash(&prover_bytes)?;
265        let verifier_checksum = hash(&verifier_bytes)?;
266
267        // Construct the metadata.
268        let metadata = Metadata {
269            prover_checksum,
270            prover_size: prover_bytes.len(),
271            verifier_checksum,
272            verifier_size: verifier_bytes.len(),
273        };
274        let metadata_pretty = serde_json::to_string_pretty(&metadata)
275            .map_err(|e| CliError::custom(format!("Failed to serialize metadata: {e}")))?;
276
277        // A helper to write to a file.
278        let write_to_file = |path: PathBuf, data: &[u8]| -> Result<()> {
279            std::fs::write(path, data).map_err(|e| CliError::custom(format!("Failed to write to file: {e}")))?;
280            Ok(())
281        };
282
283        // If the `save` option is set, save the proving and verifying keys to a file in the specified directory.
284        // The file format is `program_id.function_id.edition_or_local.type.timestamp`.
285        // The directory is created if it doesn't exist.
286        if let Some(path) = &command.action.save {
287            // Create the directory if it doesn't exist.
288            std::fs::create_dir_all(path).map_err(|e| CliError::custom(format!("Failed to create directory: {e}")))?;
289            // Get the current timestamp.
290            let timestamp = chrono::Utc::now().timestamp();
291            // The edition.
292            let edition = if command.local { "local".to_string() } else { edition.to_string() };
293            // The prefix for the file names.
294            let prefix = format!("{network}.{program_id}.{function_id}.{edition}");
295            // Get the file paths.
296            let prover_file_path = PathBuf::from(path).join(format!("{prefix}.prover.{timestamp}"));
297            let verifier_file_path = PathBuf::from(path).join(format!("{prefix}.verifier.{timestamp}"));
298            let metadata_file_path = PathBuf::from(path)
299                .join(format!("{network}.{program_id}.{function_id}.{edition}.metadata.{timestamp}"));
300            // Print the save location.
301            println!(
302                "💾 Saving proving key, verifying key, and metadata to: {}/{network}.{program_id}.{function_id}.{edition}.prover|verifier|metadata.{timestamp}",
303                metadata_file_path.parent().unwrap().display()
304            );
305            // Save the keys.
306            write_to_file(prover_file_path, &prover_bytes)?;
307            write_to_file(verifier_file_path, &verifier_bytes)?;
308            write_to_file(metadata_file_path, metadata_pretty.as_bytes())?;
309        }
310    }
311
312    Ok(())
313}