aleo_rust/program/
mod.rs

1// Copyright (C) 2019-2023 Aleo Systems Inc.
2// This file is part of the Aleo SDK library.
3
4// The Aleo SDK 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 Aleo SDK 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 Aleo SDK library. If not, see <https://www.gnu.org/licenses/>.
16
17//! Tools for deploying, executing, and managing programs on the Aleo network
18
19use super::*;
20
21pub mod deploy;
22pub use deploy::*;
23
24pub mod execute;
25pub use execute::*;
26
27pub mod helpers;
28pub use helpers::*;
29
30pub mod network;
31pub use network::*;
32
33pub mod resolver;
34pub use resolver::*;
35
36pub mod transfer;
37pub use transfer::*;
38
39/// Program management object for loading programs for building, execution, and deployment
40///
41/// This object is meant to be a software abstraction that can be consumed by software like
42/// CLI tools, IDE plugins, Server-side stack components and other software that needs to
43/// interact with the Aleo network.
44#[derive(Clone)]
45pub struct ProgramManager<N: Network> {
46    pub(crate) programs: IndexMap<ProgramID<N>, Program<N>>,
47    pub(crate) private_key: Option<PrivateKey<N>>,
48    pub(crate) private_key_ciphertext: Option<Ciphertext<N>>,
49    pub(crate) local_program_directory: Option<PathBuf>,
50    pub(crate) api_client: Option<AleoAPIClient<N>>,
51    pub(crate) vm: Option<VM<N, ConsensusMemory<N>>>,
52}
53
54impl<N: Network> ProgramManager<N> {
55    /// Create a new program manager by specifying custom options for the private key (or private
56    /// key ciphertext) and resolver. Use this method if you want to create a custom resolver
57    /// (i.e. one that searches a local or remote database) for program and record resolution.
58    pub fn new(
59        private_key: Option<PrivateKey<N>>,
60        private_key_ciphertext: Option<Ciphertext<N>>,
61        api_client: Option<AleoAPIClient<N>>,
62        local_program_directory: Option<PathBuf>,
63        use_cache: bool,
64    ) -> Result<Self> {
65        if private_key.is_some() && private_key_ciphertext.is_some() {
66            bail!("Cannot have both private key and private key ciphertext");
67        } else if private_key.is_none() && private_key_ciphertext.is_none() {
68            bail!("Must have either private key or private key ciphertext");
69        }
70        let programs = IndexMap::new();
71        let vm = if use_cache {
72            let store = ConsensusStore::<N, ConsensusMemory<N>>::open(None)?;
73            Some(VM::<N, ConsensusMemory<N>>::from(store)?)
74        } else {
75            None
76        };
77        Ok(Self { programs, private_key, private_key_ciphertext, local_program_directory, api_client, vm })
78    }
79
80    /// Manually add a program to the program manager from memory if it does not already exist
81    pub fn add_program(&mut self, program: &Program<N>) -> Result<()> {
82        if self.contains_program(program.id())? {
83            bail!("program already exists")
84        };
85        self.programs.entry(*program.id()).or_insert(program.clone());
86        Ok(())
87    }
88
89    /// Initialize a SnarkVM instance with a program and its imports
90    pub fn initialize_vm(
91        api_client: &AleoAPIClient<N>,
92        program: &Program<N>,
93        initialize_execution: bool,
94    ) -> Result<VM<N, ConsensusMemory<N>>> {
95        // Create an ephemeral SnarkVM to store the programs
96        // Initialize an RNG and query object for the transaction
97        let store = ConsensusStore::<N, ConsensusMemory<N>>::open(None)?;
98        let vm = VM::<N, ConsensusMemory<N>>::from(store)?;
99
100        // Resolve imports
101        let credits_id = ProgramID::<N>::from_str("credits.aleo")?;
102        api_client.get_program_imports_from_source(program)?.iter().try_for_each(|(_, import)| {
103            if import.id() != &credits_id {
104                vm.process().write().add_program(import)?
105            }
106            Ok::<_, Error>(())
107        })?;
108
109        // If the initialization is for an execution, add the program. Otherwise, don't add it as
110        // it will be added during the deployment process
111        if initialize_execution {
112            vm.process().write().add_program(program)?;
113        }
114        Ok(vm)
115    }
116
117    /// Manually add a program to the program manager if it does not already exist or update
118    /// it if it does
119    pub fn update_program(&mut self, program: &Program<N>) -> Option<Program<N>> {
120        self.programs.insert(*program.id(), program.clone())
121    }
122
123    /// Retrieve a program from the program manager if it exists
124    pub fn get_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<Program<N>> {
125        let program_id = program_id.try_into().map_err(|_| anyhow!("invalid program id"))?;
126        self.programs.get(&program_id).map_or(Err(anyhow!("program not found")), |program| Ok(program.clone()))
127    }
128
129    /// Determine if a program exists in the program manager
130    pub fn contains_program(&self, program_id: impl TryInto<ProgramID<N>>) -> Result<bool> {
131        let program_id = program_id.try_into().map_err(|_| anyhow!("invalid program id"))?;
132        Ok(self.programs.contains_key(&program_id))
133    }
134
135    /// Get the private key from the program manager. If the key is stored as ciphertext, a
136    /// password must be provided to decrypt it
137    pub(super) fn get_private_key(&self, password: Option<&str>) -> Result<PrivateKey<N>> {
138        if self.private_key.is_none() && self.private_key_ciphertext.is_none() {
139            bail!("Private key is not configured");
140        };
141        if let Some(private_key) = &self.private_key {
142            if self.private_key_ciphertext.is_some() {
143                bail!(
144                    "Private key ciphertext is also configured, cannot have both private key and private key ciphertext"
145                );
146            }
147            return Ok(*private_key);
148        };
149        if let Some(ciphertext) = &self.private_key_ciphertext {
150            if self.private_key.is_some() {
151                bail!("Private key is already configured, cannot have both private key and private key ciphertext");
152            }
153
154            let password = password.ok_or_else(|| anyhow!("Private key is encrypted, password is required"))?;
155            return Encryptor::<N>::decrypt_private_key_with_secret(ciphertext, password);
156        };
157        bail!("Private key configuration error")
158    }
159    
160    pub fn vm(&self) -> &Option<VM<N, ConsensusMemory<N>>> {
161        &self.vm
162    }
163}
164
165#[cfg(test)]
166#[cfg(not(feature = "wasm"))]
167mod tests {
168
169    use super::*;
170    use crate::{
171        test_utils::{HELLO_PROGRAM, HELLO_PROGRAM_2},
172        RECIPIENT_PRIVATE_KEY,
173    };
174
175    #[test]
176    fn test_constructors_fail_with_multiple_keys_or_no_keys() {
177        let api_client = AleoAPIClient::<Testnet3>::testnet3();
178        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
179        let private_key_ciphertext =
180            Encryptor::<Testnet3>::encrypt_private_key_with_secret(&private_key, "password").unwrap();
181        // Create a temp dir without proper programs to test that the hybrid client works even if the local resource directory doesn't exist
182        let temp_dir = std::env::temp_dir();
183
184        // Ensure that program manager creation fails if no key is provided
185        let program_manager =
186            ProgramManager::<Testnet3>::new(None, None, Some(api_client.clone()), Some(temp_dir.clone()), false);
187
188        assert!(program_manager.is_err());
189
190        // Ensure that program manager creation fails if both key and key ciphertext are provided
191        let program_manager = ProgramManager::<Testnet3>::new(
192            Some(private_key),
193            Some(private_key_ciphertext.clone()),
194            Some(api_client.clone()),
195            Some(temp_dir.clone()),
196            false,
197        );
198
199        assert!(program_manager.is_err());
200
201        // Ensure program manager is created successfully if only a private key is provided
202        let program_manager = ProgramManager::<Testnet3>::new(
203            Some(private_key),
204            None,
205            Some(api_client.clone()),
206            Some(temp_dir.clone()),
207            false,
208        );
209
210        assert!(program_manager.is_ok());
211
212        // Ensure program manager is created successfully if only a private key ciphertext is provided
213        let program_manager = ProgramManager::<Testnet3>::new(
214            None,
215            Some(private_key_ciphertext),
216            Some(api_client),
217            Some(temp_dir),
218            false,
219        );
220
221        assert!(program_manager.is_ok());
222    }
223
224    #[test]
225    fn test_program_management_methods() {
226        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
227        let mut program_manager = ProgramManager::<Testnet3>::new(Some(private_key), None, None, None, false).unwrap();
228
229        // Test program addition
230        let program = Program::<Testnet3>::from_str(HELLO_PROGRAM).unwrap();
231        assert!(!program_manager.contains_program(program.id()).unwrap());
232        program_manager.add_program(&program).unwrap();
233        assert!(program_manager.contains_program(program.id()).unwrap());
234        assert_eq!(program_manager.get_program(program.id()).unwrap(), program);
235        assert_eq!(program_manager.get_program("hello.aleo").unwrap(), program);
236        assert!(program_manager.contains_program("hello.aleo").unwrap());
237        assert!(program_manager.add_program(&program).is_err());
238
239        // Test program update methods
240        let program_2 = Program::<Testnet3>::from_str(HELLO_PROGRAM_2).unwrap();
241        let replaced_program = program_manager.update_program(&program_2).unwrap();
242        let retrieved_program = program_manager.get_program(program.id()).unwrap();
243        assert_eq!(replaced_program, program);
244        assert_eq!(retrieved_program, program_2);
245    }
246
247    #[test]
248    fn test_private_key_retrieval_from_ciphertext() {
249        let private_key = PrivateKey::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
250        let private_key_ciphertext =
251            Encryptor::<Testnet3>::encrypt_private_key_with_secret(&private_key, "password").unwrap();
252        let temp_dir = std::env::temp_dir();
253        let api_client = AleoAPIClient::<Testnet3>::testnet3();
254
255        let program_manager = ProgramManager::<Testnet3>::new(
256            None,
257            Some(private_key_ciphertext),
258            Some(api_client),
259            Some(temp_dir),
260            false,
261        )
262        .unwrap();
263
264        // Assert private key recovers correctly
265        let recovered_private_key = program_manager.get_private_key(Some("password")).unwrap();
266        assert_eq!(recovered_private_key, private_key);
267
268        // Assert error is thrown if password is incorrect
269        let recovered_private_key = program_manager.get_private_key(Some("wrong_password"));
270        assert!(recovered_private_key.is_err());
271
272        // Assert error is thrown if password is not provided
273        let recoverd_private_key = program_manager.get_private_key(None);
274        assert!(recoverd_private_key.is_err());
275    }
276
277    #[test]
278    fn test_private_key_retrieval_from_plaintext() {
279        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
280        let temp_dir = std::env::temp_dir();
281        let api_client = AleoAPIClient::<Testnet3>::testnet3();
282
283        let program_manager =
284            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client), Some(temp_dir), false).unwrap();
285
286        // Assert private key recovers correctly regardless of password
287        let recovered_private_key = program_manager.get_private_key(None).unwrap();
288        assert_eq!(recovered_private_key, private_key);
289
290        let recovered_private_key = program_manager.get_private_key(Some("password")).unwrap();
291        assert_eq!(recovered_private_key, private_key);
292    }
293
294    #[test]
295    fn test_import_resolution() {
296        let api_client = AleoAPIClient::<Testnet3>::testnet3();
297        let top_level_program = api_client.get_program("imported_add_mul.aleo").unwrap();
298        let add_program = api_client.get_program("addition_test.aleo").unwrap();
299        let multiply_program = api_client.get_program("multiply_test.aleo").unwrap();
300        let double_program = api_client.get_program("double_test.aleo").unwrap();
301        let vm_execute = ProgramManager::<Testnet3>::initialize_vm(&api_client, &top_level_program, true).unwrap();
302        let vm_deploy = ProgramManager::<Testnet3>::initialize_vm(&api_client, &top_level_program, false).unwrap();
303
304        // Ensure the initialization contained all imported programs
305        let top_program = vm_execute.process().read().get_program("imported_add_mul.aleo").unwrap().clone();
306        assert_eq!(top_level_program, top_program);
307        let add_import = vm_execute.process().read().get_program("addition_test.aleo").unwrap().clone();
308        assert_eq!(add_program, add_import);
309        let multiply_import = vm_execute.process().read().get_program("multiply_test.aleo").unwrap().clone();
310        assert_eq!(multiply_program, multiply_import);
311        let double_import = vm_execute.process().read().get_program("double_test.aleo").unwrap().clone();
312        assert_eq!(double_program, double_import);
313
314        // Ensure the initialization contained all imported programs except the top level program
315        let top_program = vm_deploy.process().read().get_program("imported_add_mul.aleo").cloned();
316        assert!(top_program.is_err());
317        let add_import = vm_deploy.process().read().get_program("addition_test.aleo").unwrap().clone();
318        assert_eq!(add_program, add_import);
319        let multiply_import = vm_deploy.process().read().get_program("multiply_test.aleo").unwrap().clone();
320        assert_eq!(multiply_program, multiply_import);
321        let double_import = vm_deploy.process().read().get_program("double_test.aleo").unwrap().clone();
322        assert_eq!(double_program, double_import);
323    }
324}