aleo_rust/program/
deploy.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    /// Deploy a program to the network
21    pub fn deploy_program(
22        &mut self,
23        program_id: impl TryInto<ProgramID<N>>,
24        priority_fee: u64,
25        fee_record: Option<Record<N, Plaintext<N>>>,
26        password: Option<&str>,
27    ) -> Result<String> {
28        // Ensure a network client is configured, otherwise deployment is not possible
29        ensure!(
30            self.api_client.is_some(),
31            "❌ Network client not set, network config must be set before deployment in order to send transactions to the Aleo network"
32        );
33
34        // Check program has a valid name
35        let program_id = program_id.try_into().map_err(|_| anyhow!("Invalid program ID"))?;
36
37        // Check if program is already deployed on chain, cancel deployment if so
38        ensure!(
39            self.api_client()?.get_program(program_id).is_err(),
40            "❌ Program {:?} already deployed on chain, cancelling deployment",
41            program_id
42        );
43
44        // Get the program if it already exists, otherwise find it
45        println!("Loading program {program_id:?}..");
46        let program = if let Ok(program) = self.get_program(program_id) {
47            println!("Program {:?} already exists in program manager, using existing program", program_id);
48            program
49        } else if let Some(dir) = self.local_program_directory.as_ref() {
50            let program = self.find_program_on_disk(&program_id);
51            if program.is_err() {
52                bail!(
53                    "❌ Program {program_id:?} could not be found at {dir:?} or in the program manager, please ensure the program is in the correct directory before continuing with deployment"
54                );
55            }
56            program?
57        } else {
58            bail!(
59                "❌ Program {:?} not found in program manager and no local program directory was configured",
60                program_id
61            );
62        };
63
64        // If the program has imports, check if they are deployed on chain. If they are not or if
65        // the imports on disk or in-memory do not match the programs deployed on chain, cancel deployment
66        program.imports().keys().try_for_each(|program_id| {
67            let imported_program = if self.contains_program(program_id)? {
68                // If the import is in memory, use it
69                self.get_program(program_id)
70            } else {
71                // Else look on disk or on the network for the import
72                self.find_program(program_id)
73            }.map_err(|_| anyhow!("❌ Imported program {program_id:?} could not be found locally or on the Aleo Network"))?;
74
75            // Check that the program import matches a deployed program on chain
76            let imported_program_id = imported_program.id();
77            match self.on_chain_program_state(&imported_program)? {
78                OnChainProgramState::NotDeployed => {
79                    // For now enforce that users deploy imports individually. In the future, create a more detailed program resolution method for local imports
80                    bail!("❌ Imported program {imported_program_id:?} could not be found on the Aleo Network, please deploy this imported program first before continuing with deployment of {program_id:?}");
81                }
82                OnChainProgramState::Different => {
83                    // If the on-chain program is different, cancel deployment
84                    bail!("❌ Imported program {imported_program_id:?} is already deployed on chain and did not match local import");
85                }
86                OnChainProgramState::Same => (),
87            };
88
89            Ok::<_, Error>(())
90        })?;
91
92        // Try to get the private key
93        let private_key = self.get_private_key(password)?;
94
95        // Attempt to construct the transaction
96        println!("Building transaction..");
97        let query = self.api_client.as_ref().unwrap().base_url();
98        let transaction = Self::create_deploy_transaction(
99            &program,
100            &private_key,
101            priority_fee,
102            fee_record,
103            query.to_string(),
104            self.api_client()?,
105            &self.vm,
106        )?;
107
108        println!(
109            "Attempting to broadcast a deploy transaction for program {:?} to node {:?}",
110            program_id,
111            self.api_client().unwrap().base_url()
112        );
113
114        let result = self.broadcast_transaction(transaction);
115
116        // Notify the developer of the result
117        if result.is_ok() {
118            println!("✅ Deployment transaction for {program_id:?} broadcast successfully");
119        } else {
120            println!("❌ Deployment transaction for {program_id:?} failed to broadcast");
121        };
122
123        result
124    }
125
126    /// Create a deploy transaction for a program without instantiating the program manager
127    pub fn create_deploy_transaction(
128        program: &Program<N>,
129        private_key: &PrivateKey<N>,
130        priority_fee: u64,
131        fee_record: Option<Record<N, Plaintext<N>>>,
132        node_url: String,
133        api_client: &AleoAPIClient<N>,
134        vm: &Option<VM<N, ConsensusMemory<N>>>,
135    ) -> Result<Transaction<N>> {
136        // Initialize an RNG.
137        let rng = &mut rand::thread_rng();
138        let query = Query::from(node_url);
139
140        if let Some(vm) = vm {
141            // Create the deployment transaction
142            vm.deploy(private_key, program, fee_record, priority_fee, Some(query), rng)
143        } else {
144            // Initialize the VM
145            let vm = Self::initialize_vm(api_client, program, false)?;
146
147            // Create the deployment transaction
148            vm.deploy(private_key, program, fee_record, priority_fee, Some(query), rng)
149        }
150    }
151
152    /// Estimate deployment fee for a program in microcredits. The result will be in the form
153    /// (total_cost, (storage_cost, namespace_cost))
154    ///
155    /// Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network
156    pub fn estimate_deployment_fee<A: Aleo<Network = N>>(&self, program: &Program<N>) -> Result<(u64, (u64, u64))> {
157        let vm = Self::initialize_vm(self.api_client()?, program, false)?;
158        let rng = &mut rand::thread_rng();
159        let private_key = PrivateKey::<N>::new(rng)?;
160        let deployment = vm.deploy(&private_key, program, None, 0u64, None, rng)?;
161        let (minimum_deployment_cost, (storage_cost, namespace_cost)) =
162            deployment_cost::<N>(deployment.deployment().ok_or(anyhow!("Deployment failed"))?)?;
163        Ok((minimum_deployment_cost, (storage_cost, namespace_cost)))
164    }
165
166    /// Estimate the component of the deployment cost derived from the program name. Note that this
167    /// cost does not represent the entire cost of deployment. It is additional to the cost of the
168    /// size (in bytes) of the deployment.
169    ///
170    /// Disclaimer: Fee estimation is experimental and may not represent a correct estimate on any current or future network
171    pub fn estimate_namespace_fee(program_id: impl TryInto<ProgramID<N>>) -> Result<u64> {
172        let program_id = program_id.try_into().map_err(|_| anyhow!("❌ Invalid program ID"))?;
173        let num_characters = program_id.to_string().chars().count() as u32;
174        let namespace_cost = 10u64
175            .checked_pow(10u32.saturating_sub(num_characters))
176            .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))?
177            .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits.
178        Ok(namespace_cost)
179    }
180}
181
182#[cfg(test)]
183#[cfg(not(feature = "wasm"))]
184mod tests {
185    use super::*;
186    use crate::{
187        test_utils::{
188            random_program,
189            random_program_id,
190            setup_directory,
191            transfer_to_test_account,
192            CREDITS_IMPORT_TEST_PROGRAM,
193            HELLO_PROGRAM,
194            MULTIPLY_IMPORT_PROGRAM,
195            MULTIPLY_PROGRAM,
196            RECORD_2000000001_MICROCREDITS,
197            RECORD_5_MICROCREDITS,
198        },
199        AleoAPIClient,
200        RecordFinder,
201    };
202    use snarkvm_console::network::Testnet3;
203
204    use std::{ops::Add, str::FromStr, thread};
205
206    #[test]
207    #[ignore]
208    fn test_deploy() {
209        let recipient_private_key = PrivateKey::<Testnet3>::from_str(RECIPIENT_PRIVATE_KEY).unwrap();
210        let finalize_program = Program::<Testnet3>::from_str(FINALIZE_TEST_PROGRAM).unwrap();
211        let multiply_program = Program::<Testnet3>::from_str(MULTIPLY_PROGRAM).unwrap();
212        let multiply_import_program = Program::<Testnet3>::from_str(MULTIPLY_IMPORT_PROGRAM).unwrap();
213
214        // Wait for the node to bootup
215        thread::sleep(std::time::Duration::from_secs(5));
216        transfer_to_test_account(2000000001, 14, recipient_private_key, "3033").unwrap();
217        let api_client = AleoAPIClient::<Testnet3>::local_testnet3("3033");
218        let record_finder = RecordFinder::<Testnet3>::new(api_client.clone());
219        let temp_dir = setup_directory("aleo_test_deploy", CREDITS_IMPORT_TEST_PROGRAM, vec![]).unwrap();
220
221        // Ensure that program manager creation fails if no key is provided
222        let mut program_manager =
223            ProgramManager::<Testnet3>::new(Some(recipient_private_key), None, Some(api_client), Some(temp_dir), false)
224                .unwrap();
225
226        // Wait for the transactions to show up on chain
227        thread::sleep(std::time::Duration::from_secs(30));
228        let deployment_fee = 200_000_001;
229        let fee_record = record_finder.find_one_record(&recipient_private_key, deployment_fee, None).unwrap();
230        program_manager.deploy_program("credits_import_test.aleo", deployment_fee, Some(fee_record), None).unwrap();
231
232        // Wait for the program to show up on chain
233        thread::sleep(std::time::Duration::from_secs(45));
234        for _ in 0..4 {
235            let deployed_program = program_manager.api_client().unwrap().get_program("credits_import_test.aleo");
236
237            if deployed_program.is_ok() {
238                assert_eq!(deployed_program.unwrap(), Program::from_str(CREDITS_IMPORT_TEST_PROGRAM).unwrap());
239                break;
240            }
241            println!("Program has not yet appeared on chain, waiting another 15 seconds");
242            thread::sleep(std::time::Duration::from_secs(15));
243        }
244
245        // Deploy a program with a finalize scope
246        program_manager.add_program(&finalize_program).unwrap();
247
248        let fee_record = record_finder.find_one_record(&recipient_private_key, deployment_fee, None).unwrap();
249        program_manager.deploy_program("finalize_test.aleo", deployment_fee, Some(fee_record), None).unwrap();
250
251        // Wait for the program to show up on chain
252        thread::sleep(std::time::Duration::from_secs(45));
253        for _ in 0..4 {
254            let deployed_program = program_manager.api_client().unwrap().get_program("finalize_test.aleo");
255
256            if deployed_program.is_ok() {
257                assert_eq!(deployed_program.unwrap(), Program::from_str(FINALIZE_TEST_PROGRAM).unwrap());
258                break;
259            }
260            println!("Program has not yet appeared on chain, waiting another 15 seconds");
261            thread::sleep(std::time::Duration::from_secs(15));
262        }
263
264        // Deploy a program other than credits.aleo to be imported
265        program_manager.add_program(&multiply_program).unwrap();
266
267        let fee_record = record_finder.find_one_record(&recipient_private_key, deployment_fee, None).unwrap();
268        program_manager.deploy_program("multiply_test.aleo", deployment_fee, Some(fee_record), None).unwrap();
269
270        // Wait for the program to show up on chain
271        thread::sleep(std::time::Duration::from_secs(45));
272        for _ in 0..4 {
273            let deployed_program = program_manager.api_client().unwrap().get_program("multiply_test.aleo");
274
275            if deployed_program.is_ok() {
276                assert_eq!(deployed_program.unwrap(), Program::from_str(MULTIPLY_PROGRAM).unwrap());
277                break;
278            }
279            println!("Program has not yet appeared on chain, waiting another 15 seconds");
280            thread::sleep(std::time::Duration::from_secs(15));
281        }
282
283        // Deploy a program with imports other than credits.aleo
284        program_manager.add_program(&multiply_import_program).unwrap();
285
286        let fee_record = record_finder.find_one_record(&recipient_private_key, deployment_fee, None).unwrap();
287        program_manager.deploy_program("double_test.aleo", deployment_fee, Some(fee_record), None).unwrap();
288
289        // Wait for the program to show up on chain
290        thread::sleep(std::time::Duration::from_secs(45));
291        for _ in 0..4 {
292            let deployed_program = program_manager.api_client().unwrap().get_program("double_test.aleo");
293
294            if deployed_program.is_ok() {
295                assert_eq!(deployed_program.unwrap(), Program::from_str(MULTIPLY_IMPORT_PROGRAM).unwrap());
296                break;
297            }
298            println!("Program has not yet appeared on chain, waiting another 15 seconds");
299            thread::sleep(std::time::Duration::from_secs(15));
300        }
301    }
302
303    #[test]
304    fn test_deploy_failure_conditions() {
305        let rng = &mut rand::thread_rng();
306        let recipient_private_key = PrivateKey::<Testnet3>::new(rng).unwrap();
307        let record_5_microcredits = Record::<Testnet3, Plaintext<Testnet3>>::from_str(RECORD_5_MICROCREDITS).unwrap();
308        let record_2000000001_microcredits =
309            Record::<Testnet3, Plaintext<Testnet3>>::from_str(RECORD_2000000001_MICROCREDITS).unwrap();
310        let api_client = AleoAPIClient::<Testnet3>::local_testnet3("3033");
311        let randomized_program = random_program();
312        let randomized_program_id = randomized_program.id().to_string();
313        let randomized_program_string = randomized_program.to_string();
314        let temp_dir = setup_directory("aleo_unit_test_fees", &randomized_program.to_string(), vec![]).unwrap();
315
316        // Ensure that program manager creation fails if no key is provided
317        let mut program_manager = ProgramManager::<Testnet3>::new(
318            Some(recipient_private_key),
319            None,
320            Some(api_client.clone()),
321            Some(temp_dir),
322            false,
323        )
324        .unwrap();
325
326        let deployment_fee = 200000001;
327        // Ensure that deployment fails if the fee is zero
328        let deployment =
329            program_manager.deploy_program(&randomized_program_id, 0, Some(record_5_microcredits.clone()), None);
330        assert!(deployment.is_err());
331
332        // Ensure that deployment fails if the fee is insufficient
333        let deployment =
334            program_manager.deploy_program(&randomized_program_id, 2, Some(record_5_microcredits.clone()), None);
335        assert!(deployment.is_err());
336
337        // Ensure that deployment fails if the record used to pay the fee is insufficient
338        let deployment =
339            program_manager.deploy_program(&randomized_program_id, deployment_fee, Some(record_5_microcredits), None);
340        assert!(deployment.is_err());
341
342        // Ensure that deployment fails if the program is already on chain
343        let deployment = program_manager.deploy_program(
344            "hello.aleo",
345            deployment_fee,
346            Some(record_2000000001_microcredits.clone()),
347            None,
348        );
349        assert!(deployment.is_err());
350
351        // Ensure that deployment fails if import cannot be found on chain
352        let missing_import_program_string =
353            format!("import {};\n", random_program_id(16)).add(&randomized_program_string);
354        let temp_dir_2 = setup_directory("aleo_unit_test_imports", &missing_import_program_string, vec![
355            ("hello.aleo", HELLO_PROGRAM),
356            (&randomized_program_id, &missing_import_program_string),
357        ])
358        .unwrap();
359        let mut program_manager = ProgramManager::<Testnet3>::new(
360            Some(recipient_private_key),
361            None,
362            Some(api_client),
363            Some(temp_dir_2),
364            false,
365        )
366        .unwrap();
367
368        let deployment = program_manager.deploy_program(
369            &randomized_program_id,
370            deployment_fee,
371            Some(record_2000000001_microcredits),
372            None,
373        );
374        assert!(deployment.is_err());
375    }
376}