Skip to main content

aleo_rust_sdk/
execution.rs

1//! # Aleo Execution — authorization, proving, and transaction packaging pipeline.
2//!
3//! The execution pipeline has three phases:
4//!
5//! 1. **Authorize** — build authorization for a function call
6//! 2. **Execute** — run locally, getting response + trace
7//! 3. **Prove + package** — prepare trace, prove execution + fee, verify, package into `Transaction`
8//!
9//! ## Usage
10//!
11//! ```no_run
12//! use aleo_rust_sdk::ExecutionEngine;
13//!
14//! let engine = ExecutionEngine::new_with_v0_fee_keys().unwrap();
15//! // see AleoClient::execute_and_broadcast() for a full pipeline example
16//! ```
17
18use anyhow::{Context, Result};
19use indexmap::IndexMap;
20use snarkvm::algorithms::snark::varuna::VarunaVersion;
21use snarkvm::circuit::AleoTestnetV0;
22use snarkvm::console::program::{Identifier, ProgramID, ProgramOwner, Value};
23use snarkvm::ledger::block::{Deployment, Transaction};
24use snarkvm::prelude::{
25    Address, ConsensusVersion, CryptoRng, InclusionVersion, PrivateKey, Process, Program, Response,
26    Rng, TestRng, TestnetV0,
27};
28use snarkvm::synthesizer::process::{Stack, Trace, deployment_cost};
29use std::str::FromStr;
30use std::sync::Arc;
31
32/// Custom query trait for state root fetching.
33pub use snarkvm::ledger::query::QueryTrait;
34
35/// Fee key loader — injects V0 proving/verifying keys for testnet fee_public.
36mod fee_keys {
37    use snarkvm::parameters::testnet::{FeePublicV0Prover, FeePublicV0Verifier};
38    use snarkvm::prelude::FromBytes as _;
39    use snarkvm::synthesizer::snark::{ProvingKey, VerifyingKey};
40
41    pub fn load_pk() -> Result<ProvingKey<snarkvm::prelude::TestnetV0>, anyhow::Error> {
42        ProvingKey::from_bytes_le(&FeePublicV0Prover::load_bytes()?)
43    }
44
45    pub fn load_vk() -> Result<VerifyingKey<snarkvm::prelude::TestnetV0>, anyhow::Error> {
46        VerifyingKey::from_bytes_le(&FeePublicV0Verifier::load_bytes()?)
47    }
48}
49
50/// Execution engine wrapping a snarkVM `Process`.
51pub struct ExecutionEngine {
52    pub process: Process<TestnetV0>,
53}
54
55impl ExecutionEngine {
56    /// Initialize a new process using `Process::load()` (credits loaded by default).
57    pub fn new() -> Result<Self> {
58        let process = Process::<TestnetV0>::load()?;
59        Ok(Self { process })
60    }
61
62    /// Initialize with explicit V0 fee keys injected.
63    pub fn new_with_v0_fee_keys() -> Result<Self> {
64        let mut engine = Self::new()?;
65        engine.inject_v0_fee_keys()?;
66        Ok(engine)
67    }
68
69    /// Inject V0 fee proving/verifying keys for testnet fee_public support.
70    pub fn inject_v0_fee_keys(&mut self) -> Result<()> {
71        tracing::info!("Loading V0 fee keys from testnet parameters...");
72        let fee_pk = fee_keys::load_pk().context("Failed to deserialize V0 fee proving key")?;
73        let fee_vk = fee_keys::load_vk().context("Failed to deserialize V0 fee verifying key")?;
74
75        let credits_id = ProgramID::<TestnetV0>::from_str("credits.aleo")?;
76        let fee_fn = Identifier::<TestnetV0>::from_str("fee_public")?;
77        let guard = self.process.lock();
78        guard.insert_proving_key(&credits_id, &fee_fn, fee_pk)?;
79        guard.insert_verifying_key(&credits_id, &fee_fn, fee_vk)?;
80        drop(guard);
81        tracing::info!("V0 fee keys injected");
82        Ok(())
83    }
84
85    /// Add a program to the engine.
86    pub fn add_program(&self, program: &Program<TestnetV0>) -> Result<()> {
87        // `add_program` is on ProcessExclusiveGuard, accessed via lock()
88        self.process.lock().add_program(program)?;
89        Ok(())
90    }
91
92    /// Authorize and execute locally using pre-parsed snarkVM `Value` inputs.
93    /// Use this variant when inputs include record ciphertexts that must be decrypted first.
94    pub fn authorize_and_execute_with_values(
95        &self,
96        private_key: &PrivateKey<TestnetV0>,
97        program_id: &ProgramID<TestnetV0>,
98        function_name: &str,
99        values: Vec<Value<TestnetV0>>,
100        rng: &mut TestRng,
101    ) -> Result<(Response<TestnetV0>, Trace<TestnetV0>)> {
102        tracing::info!("Authorizing with pre-parsed values...");
103        let fn_id = Identifier::<TestnetV0>::from_str(function_name)?;
104        let authorization = self
105            .process
106            .authorize::<AleoTestnetV0, _>(private_key, *program_id, fn_id, values.into_iter(), rng)
107            .context("Authorization failed")?;
108
109        tracing::info!("Executing locally...");
110        let (response, trace) = self
111            .process
112            .execute::<AleoTestnetV0, _>(authorization, rng)
113            .context("Local execution failed")?;
114
115        tracing::info!("Execution succeeded");
116        Ok((response, trace))
117    }
118
119    /// Authorize and execute locally. Returns response + trace.
120    ///
121    /// Inputs are parsed as snarkVM `Value`s — record ciphertexts (e.g. `record1...`)
122    /// are handled correctly, unlike raw string inputs.
123    pub fn authorize_and_execute(
124        &self,
125        private_key: &PrivateKey<TestnetV0>,
126        program_id: &ProgramID<TestnetV0>,
127        function_name: &str,
128        inputs: Vec<&str>,
129        rng: &mut TestRng,
130    ) -> Result<(Response<TestnetV0>, Trace<TestnetV0>)> {
131        tracing::info!("Authorizing...");
132        let fn_id = Identifier::<TestnetV0>::from_str(function_name)?;
133        let authorization = self
134            .process
135            .authorize::<AleoTestnetV0, _>(private_key, *program_id, fn_id, inputs.into_iter(), rng)
136            .context("Authorization failed")?;
137
138        tracing::info!("Executing locally...");
139        let (response, trace) = self
140            .process
141            .execute::<AleoTestnetV0, _>(authorization, rng)
142            .context("Local execution failed")?;
143
144        Ok((response, trace))
145    }
146
147    /// Prove execution + fee, verify, package into `Transaction`.
148    #[allow(clippy::too_many_arguments)]
149    pub fn prove_and_package(
150        &self,
151        trace: Trace<TestnetV0>,
152        private_key: &PrivateKey<TestnetV0>,
153        program_id: &ProgramID<TestnetV0>,
154        function_name: &str,
155        base_fee: u64,
156        priority_fee: u64,
157        query: &impl QueryTrait<TestnetV0>,
158        rng: &mut TestRng,
159    ) -> Result<Transaction<TestnetV0>> {
160        let locator = format!("{program_id}/{function_name}");
161
162        // Prove execution
163        let mut exec_trace = trace;
164        exec_trace.prepare(query).context("Failed to prepare execution trace")?;
165        let execution = exec_trace
166            .prove_execution::<AleoTestnetV0, _>(&locator, VarunaVersion::V2, rng)
167            .context("Failed to generate execution proof")?;
168
169        // Fee authorization + proving
170        let execution_id = execution.to_execution_id()?;
171        let fee_authorization = self
172            .process
173            .authorize_fee_public::<AleoTestnetV0, _>(
174                private_key,
175                base_fee,
176                priority_fee,
177                execution_id,
178                rng,
179            )
180            .context("Failed to authorize fee")?;
181
182        let (_fee_response, mut fee_trace) = self
183            .process
184            .execute::<AleoTestnetV0, _>(fee_authorization, rng)
185            .context("Failed to execute fee")?;
186
187        fee_trace.prepare(query).context("Failed to prepare fee trace")?;
188        let fee = fee_trace
189            .prove_fee::<AleoTestnetV0, _>(VarunaVersion::V2, rng)
190            .context("Failed to generate fee proof")?;
191
192        // Build execution_stacks for verification (needed by 4.10 API)
193        let mut execution_stacks: IndexMap<ProgramID<TestnetV0>, Arc<Stack<TestnetV0>>> =
194            IndexMap::new();
195        let guard = self.process.lock();
196        for transition in execution.transitions() {
197            let pid = *transition.program_id();
198            if !execution_stacks.contains_key(&pid) {
199                let stack = guard.get_stack(pid).context("Missing stack for verification")?;
200                execution_stacks.insert(pid, stack.clone());
201            }
202        }
203        drop(guard);
204
205        // Local verification (associated function in 4.10)
206        tracing::info!("Verifying proofs locally...");
207        Process::<TestnetV0>::verify_execution(
208            ConsensusVersion::V14,
209            VarunaVersion::V2,
210            InclusionVersion::V1,
211            &execution,
212            &execution_stacks,
213        )
214        .context("Local execution verification FAILED")?;
215
216        self.process
217            .verify_fee(
218                ConsensusVersion::V14,
219                VarunaVersion::V2,
220                InclusionVersion::V1,
221                &fee,
222                execution_id,
223            )
224            .context("Local fee verification FAILED")?;
225
226        Transaction::<TestnetV0>::from_execution(execution, Some(fee))
227            .context("Failed to package transaction")
228    }
229
230    /// Reference to the inner `Process`.
231    pub fn inner(&self) -> &Process<TestnetV0> {
232        &self.process
233    }
234
235    // ── Deployment (program publishing) ─────────────────────────────────
236
237    /// Pure program deployment proof — generates a `Deployment` from source.
238    ///
239    /// This only proves the program circuit. Fee and transaction packaging
240    /// must be done separately via [`build_deployment_transaction`](Self::build_deployment_transaction).
241    pub fn deploy_program<R: Rng + CryptoRng>(
242        &self,
243        program: &Program<TestnetV0>,
244        rng: &mut R,
245    ) -> Result<Deployment<TestnetV0>> {
246        self.process
247            .deploy::<AleoTestnetV0, R>(program, rng)
248            .context("Failed to generate program deployment")
249    }
250
251    /// Full deployment pipeline: prove program → authorize fee → prove fee →
252    /// verify → package into [`Transaction`].
253    ///
254    /// This is the equivalent of JS SDK's `buildDeploymentTransaction`.
255    ///
256    /// `base_fee` is the minimum deployment cost (use [`ExecutionEngine::deployment_cost_minimum`] to compute it).
257    /// `priority_fee_in_microcredits` is an additional fee on top.
258    /// `consensus_version` determines which cost formula applies (TestnetV0 uses V14).
259    /// `query` supplies the current state root (from [`AleoHttpClient::fetch_state_root`](crate::network::AleoHttpClient::fetch_state_root)).
260    #[allow(clippy::too_many_arguments)]
261    pub fn build_deployment_transaction<R: Rng + CryptoRng>(
262        &self,
263        private_key: &PrivateKey<TestnetV0>,
264        _program: &Program<TestnetV0>,
265        deployment: &Deployment<TestnetV0>,
266        base_fee: u64,
267        priority_fee_in_microcredits: u64,
268        _consensus_version: ConsensusVersion,
269        query: &impl QueryTrait<TestnetV0>,
270        rng: &mut R,
271    ) -> Result<Transaction<TestnetV0>> {
272        // Ensure the program has functions.
273        if deployment.program().functions().is_empty() {
274            anyhow::bail!("Attempted to create an empty deployment");
275        }
276
277        // Compute deployment ID and owner.
278        // NOTE: Process::deploy returns a deployment with edition set by Stack::new(process, program),
279        // which gives a non-zero edition. The on-chain validator checks `deployment.edition() == 0` for
280        // new deployments (non-zero edition → amendment path). Reset it to 0 for a fresh deploy.
281        let mut deployment = deployment.clone();
282        deployment.set_edition_raw(0);
283        let addr = Address::try_from(private_key)?;
284        deployment.set_program_owner_raw(Some(addr));
285        deployment.set_program_checksum_raw(Some(deployment.program().to_checksum()));
286
287        let deployment_id = deployment.to_deployment_id()?;
288        let owner = ProgramOwner::new(private_key, deployment_id, rng)?;
289
290        // Authorize the fee.
291        let fee_authorization = self
292            .process
293            .authorize_fee_public::<AleoTestnetV0, R>(
294                private_key,
295                base_fee,
296                priority_fee_in_microcredits,
297                deployment_id,
298                rng,
299            )
300            .context("Failed to authorize deployment fee")?;
301
302        // Execute + prove the fee.
303        let (_, mut fee_trace) = self
304            .process
305            .execute::<AleoTestnetV0, R>(fee_authorization, rng)
306            .context("Failed to execute fee")?;
307
308        fee_trace.prepare(query).context("Failed to prepare fee trace")?;
309
310        let fee = fee_trace
311            .prove_fee::<AleoTestnetV0, R>(VarunaVersion::V2, rng)
312            .context("Failed to prove fee")?;
313
314        // Package into transaction.
315        Transaction::<TestnetV0>::from_deployment(owner, deployment.clone(), fee)
316            .context("Failed to package deployment transaction")
317    }
318
319    /// Compute the minimum deployment cost for a deployment at the given consensus version.
320    pub fn deployment_cost_minimum(
321        &self,
322        deployment: &Deployment<TestnetV0>,
323        _consensus_version: ConsensusVersion,
324    ) -> Result<u64> {
325        let (min_cost, _details) = deployment_cost(&self.process, deployment, _consensus_version)?;
326        Ok(min_cost)
327    }
328
329    // ── Verification ─────────────────────────────────────────────────────
330
331    /// Verify an **execute** transaction's proof.
332    ///
333    /// The program must already be registered in this engine via `add_program()`.
334    pub fn verify_execution_transaction(
335        &self,
336        execution: &snarkvm::ledger::block::Execution<TestnetV0>,
337    ) -> Result<()> {
338        // Build execution stacks from the process
339        let mut execution_stacks: IndexMap<ProgramID<TestnetV0>, Arc<Stack<TestnetV0>>> =
340            IndexMap::new();
341        let guard = self.process.lock();
342        for t in execution.transitions() {
343            let pid = *t.program_id();
344            if !execution_stacks.contains_key(&pid) {
345                let stack = guard.get_stack(pid).context("Missing stack for verification")?;
346                execution_stacks.insert(pid, stack.clone());
347            }
348        }
349        drop(guard);
350
351        Process::<TestnetV0>::verify_execution(
352            ConsensusVersion::V14,
353            VarunaVersion::V2,
354            InclusionVersion::V1,
355            execution,
356            &execution_stacks,
357        )
358        .context("Execution proof verification FAILED")
359    }
360
361    /// Verify a **deploy** transaction's proof.
362    /// Uses an empty engine (no user programs loaded) to avoid edition-zero collision.
363    pub fn verify_deployment_transaction(
364        &self,
365        deployment: &Deployment<TestnetV0>,
366        rng: &mut TestRng,
367    ) -> Result<()> {
368        self.process
369            .verify_deployment::<AleoTestnetV0, _>(ConsensusVersion::V14, deployment, rng)
370            .context("Deployment proof verification FAILED")
371    }
372}