Skip to main content

leo_lang/cli/commands/
synthesize.rs

1// Copyright (C) 2019-2026 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 std::{fmt::Write, path::PathBuf};
40
41/// Synthesize proving and verifying keys for a given function.
42#[derive(Parser, Debug)]
43pub struct LeoSynthesize {
44    #[clap(name = "NAME", help = "The name of the program to synthesize, e.g `helloworld.aleo`")]
45    pub(crate) program_name: String,
46    #[arg(short, long, help = "Use the local Leo project.")]
47    pub(crate) local: bool,
48    #[arg(short, long, help = "Skip functions that contain any of the given substrings")]
49    pub(crate) skip: Vec<String>,
50    #[arg(long, help = "Save the synthesized keys to the provided directory.")]
51    pub(crate) save: Option<String>,
52    #[clap(flatten)]
53    pub(crate) env_override: EnvOptions,
54}
55
56impl Command for LeoSynthesize {
57    type Input = Option<Package>;
58    type Output = SynthesizeOutput;
59
60    fn log_span(&self) -> Span {
61        tracing::span!(tracing::Level::INFO, "Leo")
62    }
63
64    fn prelude(&self, context: Context) -> Result<Self::Input> {
65        // If the `--local` option is enabled, then build the project.
66        if self.local {
67            let package = LeoBuild {
68                env_override: self.env_override.clone(),
69                // Synthesis must reflect the current source, so always build without the cache.
70                options: BuildOptions { no_cache: true, ..Default::default() },
71                rename: None,
72            }
73            .execute(context)?;
74            // Return the package.
75            Ok(Some(package))
76        } else {
77            Ok(None)
78        }
79    }
80
81    fn apply(self, context: Context, input: Self::Input) -> Result<Self::Output> {
82        // Get the network, accounting for overrides.
83        let network = get_network(&self.env_override.network)?;
84        // Handle each network with the appropriate parameterization.
85        match network {
86            NetworkName::TestnetV0 => handle_synthesize::<AleoTestnetV0>(self, context, network, input),
87            NetworkName::MainnetV0 => {
88                #[cfg(feature = "only_testnet")]
89                panic!("Mainnet chosen with only_testnet feature");
90                #[cfg(not(feature = "only_testnet"))]
91                handle_synthesize::<AleoV0>(self, context, network, input)
92            }
93            NetworkName::CanaryV0 => {
94                #[cfg(feature = "only_testnet")]
95                panic!("Canary chosen with only_testnet feature");
96                #[cfg(not(feature = "only_testnet"))]
97                handle_synthesize::<AleoCanaryV0>(self, context, network, input)
98            }
99        }
100    }
101}
102
103// A helper function to handle the `synthesize` command.
104fn handle_synthesize<A: Aleo>(
105    command: LeoSynthesize,
106    context: Context,
107    network: NetworkName,
108    package: Option<Package>,
109) -> Result<<LeoSynthesize as Command>::Output> {
110    // Get the endpoint, accounting for overrides.
111    let endpoint = get_endpoint(&command.env_override.endpoint)?;
112
113    // Parse the program name as a `ProgramID`.
114    let program_id = ProgramID::<A::Network>::from_str(&command.program_name)
115        .map_err(|e| crate::errors::custom(format!("Failed to parse program name: {e}")))?;
116
117    // Get all the dependencies in the package if it exists.
118    // Get the programs and optional manifests for all programs.
119    let programs = if let Some(package) = &package {
120        // Get the program names and their bytecode.
121        package
122            .compilation_units
123            .iter()
124            .clone()
125            .filter(|unit| !unit.kind.is_library())
126            .map(|unit| {
127                let program_id = ProgramID::<A::Network>::from_str(&format!("{}", unit.name))
128                    .map_err(|e| crate::errors::custom(format!("Failed to parse program ID: {e}")))?;
129                match &unit.data {
130                    ProgramData::Bytecode(bytecode) => Ok((program_id, bytecode.to_string(), unit.edition)),
131                    ProgramData::SourcePath { .. } => {
132                        // Get the path to the built bytecode.
133                        let bytecode_path = package.unit_bytecode_path(&unit.name.to_string());
134                        // Fetch the bytecode.
135                        let bytecode = std::fs::read_to_string(&bytecode_path).map_err(|e| {
136                            crate::errors::custom(format!(
137                                "Failed to read bytecode at {}: {e}",
138                                bytecode_path.display()
139                            ))
140                        })?;
141                        // Return the bytecode and the manifest.
142                        Ok((program_id, bytecode, unit.edition))
143                    }
144                }
145            })
146            .collect::<Result<Vec<_>>>()?
147    } else {
148        Vec::new()
149    };
150
151    // Parse the program strings into AVM programs.
152    let mut programs = programs
153        .into_iter()
154        .map(|(_, bytecode, edition)| {
155            // Parse the program.
156            let program = snarkvm::prelude::Program::<A::Network>::from_str(&bytecode)
157                .map_err(|e| crate::errors::custom(format!("Failed to parse program: {e}")))?;
158            // Return the program and its name.
159            Ok((program, edition))
160        })
161        .collect::<Result<Vec<_>>>()?;
162
163    // Determine whether the program is local or remote.
164    let is_local = programs.iter().any(|(program, _)| program.id() == &program_id);
165
166    // Initialize an RNG.
167    let rng = &mut rand::rng();
168
169    // Initialize a new VM.
170    let vm = VM::from(ConsensusStore::<A::Network, ConsensusMemory<A::Network>>::open(StorageMode::Production)?)?;
171
172    // If the program is not local, then download it and its dependencies for the network.
173    // Note: The dependencies are downloaded in "post-order" (child before parent).
174    if !is_local {
175        println!("⬇️ Downloading {program_id} and its dependencies from {endpoint}...");
176        programs = load_latest_programs_from_network(
177            &context,
178            program_id,
179            network,
180            &endpoint,
181            command.env_override.network_retries,
182        )?;
183    };
184
185    // Add the programs to the VM.
186    println!("\n➕ Adding programs to the VM in the following order:");
187    let programs_and_editions = programs
188        .into_iter()
189        .map(|(program, edition)| {
190            print_program_source(&program.id().to_string(), edition);
191            let edition = edition.unwrap_or(LOCAL_PROGRAM_DEFAULT_EDITION);
192            (program, edition)
193        })
194        .collect::<Vec<_>>();
195    vm.process().lock().add_programs_with_editions(&programs_and_editions)?;
196
197    // Get the edition and function IDs from the program.
198    let stack = vm.process().get_stack(program_id)?;
199    let edition = *stack.program_edition();
200    let function_ids = stack
201        .program()
202        .functions()
203        .keys()
204        .filter(|id| !command.skip.iter().any(|substring| id.to_string().contains(substring)))
205        .collect::<Vec<_>>();
206
207    // A helper function to hash the keys.
208    let hash = |bytes: &[u8]| -> anyhow::Result<String> {
209        let digest = sha256(bytes);
210        let mut hex = String::new();
211        for byte in digest {
212            write!(&mut hex, "{byte:02x}")?;
213        }
214        Ok(hex)
215    };
216
217    // Collect record names for translation key synthesis.
218    let record_names: Vec<_> = stack.program().records().keys().cloned().collect();
219
220    println!("\n🌱 Synthesizing the following keys in {program_id}:");
221    for id in &function_ids {
222        println!("    - {id} (function)");
223    }
224    for name in &record_names {
225        println!("    - {name} (record translation)");
226    }
227
228    let mut synthesized_functions = Vec::new();
229
230    // Helper: process a synthesized key (function or translation), print circuit info,
231    // record metadata, and optionally save keys to disk.
232    let mut process_key = |name: &str, label: &str| -> Result<()> {
233        let name_id = snarkvm::prelude::Identifier::<A::Network>::from_str(name)?;
234        let proving_key = stack.get_proving_key(&name_id)?;
235        let verifying_key = stack.get_verifying_key(&name_id)?;
236
237        println!("\n🔑 Synthesized {label} for {program_id}/{name} (edition {edition})");
238        println!("ℹ️ Circuit Information:");
239        println!("    - Public Inputs: {}", verifying_key.circuit_info.num_public_inputs);
240        println!("    - Variables: {}", verifying_key.num_variables());
241        println!("    - Constraints: {}", verifying_key.circuit_info.num_constraints);
242        println!("    - Non-Zero Entries in A: {}", verifying_key.circuit_info.num_non_zero_a);
243        println!("    - Non-Zero Entries in B: {}", verifying_key.circuit_info.num_non_zero_b);
244        println!("    - Non-Zero Entries in C: {}", verifying_key.circuit_info.num_non_zero_c);
245        println!("    - Circuit ID: {}", verifying_key.id);
246
247        let prover_bytes = proving_key.to_bytes_le()?;
248        let verifier_bytes = verifying_key.to_bytes_le()?;
249        let prover_checksum = hash(&prover_bytes)?;
250        let verifier_checksum = hash(&verifier_bytes)?;
251
252        let metadata = Metadata {
253            prover_checksum,
254            prover_size: prover_bytes.len(),
255            verifier_checksum,
256            verifier_size: verifier_bytes.len(),
257        };
258        let metadata_pretty = serde_json::to_string_pretty(&metadata)
259            .map_err(|e| crate::errors::custom(format!("Failed to serialize metadata: {e}")))?;
260
261        let circuit_info = CircuitInfo {
262            num_public_inputs: verifying_key.circuit_info.num_public_inputs as u64,
263            num_variables: verifying_key.num_variables(),
264            num_constraints: verifying_key.circuit_info.num_constraints as u64,
265            num_non_zero_a: verifying_key.circuit_info.num_non_zero_a as u64,
266            num_non_zero_b: verifying_key.circuit_info.num_non_zero_b as u64,
267            num_non_zero_c: verifying_key.circuit_info.num_non_zero_c as u64,
268            circuit_id: verifying_key.id.to_string(),
269        };
270
271        synthesized_functions.push(SynthesizedFunction {
272            name: name.to_string(),
273            circuit_info,
274            metadata: metadata.clone(),
275        });
276
277        if let Some(path) = &command.save {
278            std::fs::create_dir_all(path)
279                .map_err(|e| crate::errors::custom(format!("Failed to create directory: {e}")))?;
280            let timestamp = chrono::Utc::now().timestamp();
281            let edition_str = if command.local { "local".to_string() } else { edition.to_string() };
282            let prefix = format!("{network}.{program_id}.{name}.{edition_str}");
283            let prover_file_path = PathBuf::from(path).join(format!("{prefix}.prover.{timestamp}"));
284            let verifier_file_path = PathBuf::from(path).join(format!("{prefix}.verifier.{timestamp}"));
285            let metadata_file_path = PathBuf::from(path).join(format!("{prefix}.metadata.{timestamp}"));
286            println!(
287                "💾 Saving {label} to: {}/{prefix}.prover|verifier|metadata.{timestamp}",
288                metadata_file_path.parent().unwrap().display()
289            );
290            std::fs::write(&prover_file_path, &prover_bytes)
291                .map_err(|e| crate::errors::custom(format!("Failed to write to file: {e}")))?;
292            std::fs::write(&verifier_file_path, &verifier_bytes)
293                .map_err(|e| crate::errors::custom(format!("Failed to write to file: {e}")))?;
294            std::fs::write(&metadata_file_path, metadata_pretty.as_bytes())
295                .map_err(|e| crate::errors::custom(format!("Failed to write to file: {e}")))?;
296        }
297
298        Ok(())
299    };
300
301    // Synthesize function keys.
302    for function_id in function_ids {
303        stack.synthesize_key::<A, _>(function_id, rng)?;
304        process_key(&function_id.to_string(), "keys")?;
305    }
306
307    // Synthesize record translation keys.
308    for record_name in &record_names {
309        stack.synthesize_translation_key::<A, _>(record_name, rng)?;
310        process_key(&record_name.to_string(), "translation key")?;
311    }
312
313    Ok(SynthesizeOutput { program: program_id.to_string(), functions: synthesized_functions })
314}