aleo_rust/program/
resolver.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
17use super::*;
18
19impl<N: Network> ProgramManager<N> {
20    /// Find a program by first looking on disk, and if not found, on the aleo network
21    pub fn find_program(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
22        self.find_program_on_disk(program_id).or_else(|_| self.find_program_on_chain(program_id))
23    }
24
25    /// Load a program from a local program directory
26    pub fn find_program_on_disk(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
27        let local_program_directory =
28            self.local_program_directory.as_ref().ok_or_else(|| anyhow!("Local program directory not set"))?;
29        let imports_directory = local_program_directory.join("imports");
30        // Ensure the directory path exists.
31        ensure!(local_program_directory.exists(), "The program directory does not exist");
32
33        ensure!(!Program::is_reserved_keyword(program_id.name()), "Program name is invalid (reserved): {program_id}");
34
35        ensure!(
36            Manifest::<N>::exists_at(local_program_directory),
37            "Please ensure that the manifest file exists in the Aleo program directory (missing '{}' at '{}')",
38            Manifest::<N>::file_name(),
39            local_program_directory.display()
40        );
41
42        // Open the manifest file.
43        let manifest = Manifest::<N>::open(local_program_directory)?;
44
45        // Ensure the program ID matches the manifest program ID, or that the program is a local import
46        if manifest.program_id() == program_id {
47            // Load the package.
48            let package = Package::open(local_program_directory)?;
49            // Load the main program.
50            Ok(package.program().clone())
51        } else {
52            let import_file = imports_directory.join(program_id.to_string());
53            ensure!(
54                import_file.exists(),
55                "No program named {program_id:?} found at {:?}",
56                local_program_directory.display()
57            );
58            println!("Attempting to load program {program_id:?} at {:?}", import_file.display());
59            let mut program_file = File::open(import_file)?;
60            let mut program_string = String::new();
61            program_file.read_to_string(&mut program_string).map_err(|err| anyhow::anyhow!(err.to_string()))?;
62            let program = Program::from_str(&program_string)?;
63            println!("Loaded program {program_id:?} successfully!");
64            Ok(program)
65        }
66    }
67
68    /// Load a program from the network
69    pub fn find_program_on_chain(&self, program_id: &ProgramID<N>) -> Result<Program<N>> {
70        self.api_client()?.get_program(program_id)
71    }
72
73    /// Find a program's imports by first searching on disk, and if not found, on the aleo network
74    pub fn find_program_imports(&self, program: &Program<N>) -> Result<Vec<Program<N>>> {
75        let mut imports = vec![];
76        for program_id in program.imports().keys() {
77            if let Ok(program) = self.find_program(program_id) {
78                imports.push(program);
79            } else {
80                bail!("Could not find program import: {:?}", program_id);
81            }
82        }
83        Ok(imports)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::{
91        test_utils::{
92            random_program_id,
93            setup_directory,
94            teardown_directory,
95            HELLO_PROGRAM,
96            IMPORT_PROGRAM,
97            RECIPIENT_PRIVATE_KEY,
98        },
99        AleoAPIClient,
100    };
101    use snarkvm_console::{account::PrivateKey, network::Testnet3};
102
103    use std::{ops::Add, panic::catch_unwind, str::FromStr};
104
105    #[test]
106    fn test_file_loading_and_imports() {
107        let private_key = PrivateKey::<Testnet3>::new(&mut rand::thread_rng()).unwrap();
108        let credits = Program::<Testnet3>::credits().unwrap().to_string();
109        let imports = vec![("credits.aleo", credits.as_str()), ("hello.aleo", HELLO_PROGRAM)];
110        let test_path = setup_directory("aleo_test_file_resolution", IMPORT_PROGRAM, imports).unwrap();
111
112        let result = catch_unwind(|| {
113            // Create a program manager with file system access only
114            let program_manager =
115                ProgramManager::<Testnet3>::new(Some(private_key), None, None, Some(test_path.clone()), false).unwrap();
116
117            // TEST 1: Test that the program manager can load a program from a file.
118            let program_id = ProgramID::<Testnet3>::from_str("aleo_test.aleo").unwrap();
119            let expected_program = Program::<Testnet3>::from_str(IMPORT_PROGRAM).unwrap();
120            let found_program = program_manager.find_program_on_disk(&program_id).unwrap();
121            assert_eq!(expected_program, found_program);
122
123            // TEST 2: Test that the program manager can find local imports
124            let test_program = Program::<Testnet3>::from_str(IMPORT_PROGRAM).unwrap();
125            let credits_program = Program::<Testnet3>::credits().unwrap();
126            let imports = program_manager.find_program_imports(&test_program).unwrap();
127            assert_eq!(imports.len(), 1);
128
129            let local_credits_program = &imports[0];
130            assert_eq!(&credits_program, local_credits_program);
131
132            // TEST 3: Test that the program manager doesn't load a non-existent program.
133            let random_program = random_program_id(16);
134            let program_id = ProgramID::<Testnet3>::from_str(&random_program).unwrap();
135            assert!(program_manager.find_program_on_disk(&program_id).is_err());
136
137            // TEST 4: Test that the program_manager throws an error when a program has a bad import,
138            let bad_import_code = String::from("import ").add(&random_program_id(16)).add(";").add(IMPORT_PROGRAM);
139            let bad_import_program = Program::<Testnet3>::from_str(&bad_import_code).unwrap();
140            let imports = program_manager.find_program_imports(&bad_import_program);
141            assert!(imports.is_err());
142
143            // TEST 5: Ensure the program manager doesn't resolve imports for a program that doesn't have any.
144            let credits = Program::<Testnet3>::credits().unwrap();
145            let imports = program_manager.find_program_imports(&credits).unwrap();
146            assert_eq!(imports.len(), 0);
147        });
148        teardown_directory(&test_path);
149        // Ensure the test directory was deleted
150        assert!(!test_path.exists());
151        result.unwrap();
152    }
153
154    #[test]
155    fn test_hybrid_program_and_import_loading() {
156        let credits_program_string = Program::<Testnet3>::credits().unwrap().to_string();
157        let imports = vec![("credits.aleo", credits_program_string.as_str())];
158        let test_path = setup_directory("aleo_test_hybrid_resolution", IMPORT_PROGRAM, imports).unwrap();
159        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
160
161        let result = catch_unwind(|| {
162            // Create a program manager with file system and network access
163            let api_client = AleoAPIClient::<Testnet3>::testnet3();
164            let program_manager = ProgramManager::<Testnet3>::new(
165                Some(private_key),
166                None,
167                Some(api_client),
168                Some(test_path.clone()),
169                false,
170            )
171            .unwrap();
172
173            // TEST 1: Test that the program manager can load a program on disk that can't be found online
174            let program_id = ProgramID::<Testnet3>::from_str("aleo_test.aleo").unwrap();
175            let expected_program = Program::<Testnet3>::from_str(IMPORT_PROGRAM).unwrap();
176            let found_program = program_manager.find_program(&program_id).unwrap();
177            assert_eq!(expected_program, found_program);
178
179            // TEST 2: Test that the program manager can resolve imports when a program is missing from disk
180            let test_program = Program::<Testnet3>::from_str(IMPORT_PROGRAM).unwrap();
181            let credits_program = Program::<Testnet3>::credits().unwrap();
182            let credits_id = credits_program.id();
183            let imports = program_manager.find_program_imports(&test_program).unwrap();
184            assert_eq!(imports.len(), 1);
185
186            let local_credits_program = &imports[0];
187            assert_eq!(&credits_program, local_credits_program);
188
189            // TEST 3: Test that the program manager doesn't load a non-existent program.
190            let random_program = random_program_id(16);
191            let program_id = ProgramID::<Testnet3>::from_str(&random_program).unwrap();
192            assert!(program_manager.find_program(&program_id).is_err());
193
194            // TEST 4: Test that the program manager does load a program that can't be found locally, but can be found online
195            assert_eq!(program_manager.find_program(credits_id).unwrap(), credits_program);
196
197            // TEST 5: Test that the program manager throws an error when a program has an import
198            // that can't be found online or on disk
199            let bad_import_code = String::from("import ").add(&random_program_id(16)).add(";").add(IMPORT_PROGRAM);
200            let bad_import_program = Program::<Testnet3>::from_str(&bad_import_code).unwrap();
201            let imports = program_manager.find_program_imports(&bad_import_program);
202            assert!(imports.is_err());
203
204            // TEST 6: Ensure a network enabled program manager doesn't resolve imports for a
205            // program that doesn't have any
206            let credits = Program::<Testnet3>::credits().unwrap();
207            let imports = program_manager.find_program_imports(&credits).unwrap();
208            assert_eq!(imports.len(), 0);
209        });
210        teardown_directory(&test_path);
211        // Ensure the test directory was deleted
212        assert!(!test_path.exists());
213        result.unwrap();
214    }
215
216    #[test]
217    fn test_network_program_resolution() {
218        // Create a program manager with network access only
219        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
220        let api_client = AleoAPIClient::<Testnet3>::testnet3();
221        let program_manager =
222            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client), None, false).unwrap();
223        let program_id = ProgramID::<Testnet3>::from_str("credits.aleo").unwrap();
224        let credits_off_the_chain = Program::<Testnet3>::credits().unwrap();
225        let credits_on_the_chain = program_manager.find_program_on_chain(&program_id).unwrap();
226        assert_eq!(credits_off_the_chain, credits_on_the_chain);
227    }
228
229    #[test]
230    fn test_network_program_imports_are_resolved_correctly() {
231        let credits = Program::<Testnet3>::credits().unwrap();
232        // Create a program manager with network access only
233        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
234        let api_client = AleoAPIClient::<Testnet3>::testnet3();
235        let program_manager =
236            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client), None, false).unwrap();
237
238        // Ensure we can find program imports when the program is on chain
239        let test_program = Program::<Testnet3>::from_str(IMPORT_PROGRAM).unwrap();
240        // let credits_program = Program::<Testnet3>::credits().unwrap();
241        let imports = program_manager.find_program_imports(&test_program).unwrap();
242        assert_eq!(imports.len(), 1);
243
244        let credits_program_on_chain = &imports[0];
245        // let online_credits_on_chain = &imports[1];
246        assert_eq!(&credits, credits_program_on_chain);
247        // assert_eq!(&credits_program, online_credits_on_chain);
248    }
249
250    #[test]
251    fn test_network_resolution_doesnt_find_programs_not_on_chain() {
252        // Create a program with a random string as the program id
253        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
254        let random_program = random_program_id(16);
255        let api_client = AleoAPIClient::<Testnet3>::testnet3();
256
257        // Create a program manager with network access only
258        let program_manager =
259            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client), None, false).unwrap();
260
261        // Ensure the program is not on chain
262        let program_id = ProgramID::<Testnet3>::from_str(&random_program).unwrap();
263        assert!(program_manager.find_program_on_chain(&program_id).is_err())
264    }
265
266    #[test]
267    fn test_network_resolution_produces_resolution_errors_for_bad_imports() {
268        // Create program manager with only network access
269        let private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
270        let api_client = AleoAPIClient::<Testnet3>::testnet3();
271        let program_manager =
272            ProgramManager::<Testnet3>::new(Some(private_key), None, Some(api_client), None, false).unwrap();
273
274        // Create a bad program with a non-existent import
275        let bad_import_code = String::from("import ").add(&random_program_id(16)).add(";").add(IMPORT_PROGRAM);
276        let bad_import_program = Program::<Testnet3>::from_str(&bad_import_code).unwrap();
277
278        // Ensure that the imports failed
279        let imports = program_manager.find_program_imports(&bad_import_program);
280        assert!(imports.is_err());
281    }
282}