aztec_pxe/execution/
acvm_executor.rs

1//! ACVM integration for executing Noir bytecode.
2//!
3//! This module provides the bridge between compiled Aztec contract artifacts
4//! (ACIR bytecode) and the Noir ACVM (Abstract Circuit Virtual Machine).
5
6use acir::brillig::{ForeignCallParam, ForeignCallResult};
7use acir::circuit::Program;
8use acir::native_types::{Witness, WitnessMap};
9use acir::FieldElement;
10use acvm::pwg::{ACVMStatus, OpcodeResolutionError, ResolvedAssertionPayload, ACVM};
11use bn254_blackbox_solver::Bn254BlackBoxSolver;
12
13use aztec_core::abi::ContractArtifact;
14use aztec_core::error::Error;
15use aztec_core::types::Fr;
16
17use super::field_conversion::{fe_to_fr, fr_to_fe, witness_map_to_frs};
18
19/// Raw ACVM execution output before structuring into kernel types.
20///
21/// This contains the solved witness and return values from a single ACVM run.
22/// The oracle is responsible for collecting side effects (notes, nullifiers, logs)
23/// into the proper `PrivateCallExecutionResult` structure.
24#[derive(Debug, Clone)]
25pub struct AcvmExecutionOutput {
26    /// Return values from the function.
27    pub return_values: Vec<Fr>,
28    /// The full solved witness map (for kernel circuit input).
29    pub witness: WitnessMap<FieldElement>,
30    /// The ACIR bytecode used (for kernel proving).
31    pub acir_bytecode: Vec<u8>,
32    /// Return values from the first ACIR sub-circuit call (if any).
33    /// Used to extract the inner function's return value from an entrypoint wrapper.
34    pub first_acir_call_return_values: Vec<Fr>,
35}
36
37/// Result of executing a utility (unconstrained) function.
38#[derive(Debug, Clone)]
39pub struct UtilityResult {
40    /// Return values from the function.
41    pub return_values: Vec<Fr>,
42}
43
44/// Trait for oracle callback during ACVM execution.
45///
46/// Using a trait instead of a closure avoids the lifetime issues
47/// with async closures capturing mutable references.
48#[async_trait::async_trait]
49pub trait OracleCallback: Send {
50    async fn handle_foreign_call(
51        &mut self,
52        function: &str,
53        inputs: Vec<Vec<Fr>>,
54    ) -> Result<Vec<Vec<Fr>>, Error>;
55}
56
57/// Executor for Noir ACIR/Brillig bytecode via the ACVM.
58pub struct AcvmExecutor;
59
60impl AcvmExecutor {
61    fn error_types_contains_message(
62        error_types: Option<&serde_json::Value>,
63        expected: &str,
64    ) -> bool {
65        error_types
66            .and_then(|value| value.as_object())
67            .map(|entries| {
68                entries
69                    .values()
70                    .any(|entry| entry.get("string").and_then(|v| v.as_str()) == Some(expected))
71            })
72            .unwrap_or(false)
73    }
74
75    fn fallback_private_error_message(
76        last_oracle: &str,
77        resolved: &str,
78        error_types: Option<&serde_json::Value>,
79    ) -> Option<String> {
80        if resolved != "Cannot satisfy constraint" {
81            return None;
82        }
83
84        let invalid_nonce =
85            "Invalid authwit nonce. When 'from' and 'msg_sender' are the same, 'authwit_nonce' must be zero";
86        if last_oracle == "privateIsNullifierPending"
87            && Self::error_types_contains_message(error_types, invalid_nonce)
88        {
89            return Some(invalid_nonce.to_owned());
90        }
91
92        let balance_too_low = "Balance too low";
93        if (last_oracle == "privateNotifyNullifiedNote" || last_oracle == "utilityGetNotes")
94            && Self::error_types_contains_message(error_types, balance_too_low)
95        {
96            return Some(balance_too_low.to_owned());
97        }
98
99        None
100    }
101
102    /// Decode base64-encoded bytecode and deserialize into an ACIR Program.
103    ///
104    /// The ACIR library always expects gzip-compressed data. Some contract
105    /// artifacts (notably unconstrained/utility functions from nargo) ship
106    /// with uncompressed bytecode, so we detect the format and compress on
107    /// the fly when necessary.
108    fn decode_program(bytecode_b64: &str) -> Result<Program<FieldElement>, Error> {
109        let bytecode_bytes =
110            base64::Engine::decode(&base64::engine::general_purpose::STANDARD, bytecode_b64)
111                .map_err(|e| Error::InvalidData(format!("base64 decode error: {e}")))?;
112
113        // Check for gzip magic bytes (0x1f 0x8b).
114        let is_gzip =
115            bytecode_bytes.len() >= 2 && bytecode_bytes[0] == 0x1f && bytecode_bytes[1] == 0x8b;
116
117        if is_gzip {
118            Program::deserialize_program(&bytecode_bytes)
119                .map_err(|e| Error::InvalidData(format!("ACIR deserialize error: {e}")))
120        } else {
121            // Uncompressed bytecode — wrap in gzip so the ACIR deserializer
122            // (which always runs GzDecoder) can process it.
123            use std::io::Write;
124            let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
125            enc.write_all(&bytecode_bytes)
126                .map_err(|e| Error::InvalidData(format!("gzip compress error: {e}")))?;
127            let compressed = enc
128                .finish()
129                .map_err(|e| Error::InvalidData(format!("gzip finish error: {e}")))?;
130            Program::deserialize_program(&compressed)
131                .map_err(|e| Error::InvalidData(format!("ACIR deserialize error: {e}")))
132        }
133    }
134
135    /// Build the initial witness map from field elements.
136    fn build_initial_witness(args: &[Fr]) -> WitnessMap<FieldElement> {
137        let mut witness_map = WitnessMap::default();
138        for (i, arg) in args.iter().enumerate() {
139            // Match upstream `toACVMWitness(0, fields)`: witness indices start at 0.
140            witness_map.insert(Witness(i as u32), fr_to_fe(arg));
141        }
142        witness_map
143    }
144
145    /// Convert ACVM foreign call inputs to Vec<Vec<Fr>>.
146    fn convert_fc_inputs(inputs: &[ForeignCallParam<FieldElement>]) -> Vec<Vec<Fr>> {
147        inputs
148            .iter()
149            .map(|param| match param {
150                ForeignCallParam::Single(fe) => vec![fe_to_fr(fe)],
151                ForeignCallParam::Array(fes) => fes.iter().map(fe_to_fr).collect(),
152            })
153            .collect()
154    }
155
156    /// Convert Vec<Vec<Fr>> oracle result to ForeignCallResult.
157    ///
158    /// Some Noir oracle interfaces return fixed-size arrays of length 1. Those must
159    /// remain arrays, not be collapsed into scalars, or Brillig deserialization fails.
160    fn convert_fc_result(function: &str, result: Vec<Vec<Fr>>) -> ForeignCallResult<FieldElement> {
161        let force_array_indexes: &[usize] = match function {
162            "utilityLoadCapsule" | "loadCapsule" | "getCapsule" => &[1],
163            // BoundedVec return: storage array at [0] must stay as Array
164            "utilityAes128Decrypt" | "aes128Decrypt" => &[0],
165            // Option<[Field; N]> return: is_some at [0], array at [1]
166            "utilityTryGetPublicKeysAndPartialAddress" | "tryGetPublicKeysAndPartialAddress" => {
167                &[1]
168            }
169            // Execution cache returns an array of field elements.
170            "privateLoadFromExecutionCache" | "loadFromExecutionCache" => &[0],
171            _ => &[],
172        };
173        let values: Vec<ForeignCallParam<FieldElement>> = result
174            .into_iter()
175            .enumerate()
176            .map(|(index, frs)| {
177                if frs.len() == 1 && !force_array_indexes.contains(&index) {
178                    ForeignCallParam::Single(fr_to_fe(&frs[0]))
179                } else {
180                    ForeignCallParam::Array(frs.iter().map(fr_to_fe).collect())
181                }
182            })
183            .collect();
184        ForeignCallResult { values }
185    }
186
187    /// Resolve an ACVM error into a human-readable message.
188    ///
189    /// Extracts the assertion payload from `OpcodeResolutionError` variants
190    /// and maps `Raw` payloads back to the string from the function's
191    /// `error_types` mapping when the error kind is `"string"`.
192    fn resolve_error(
193        err: &OpcodeResolutionError<FieldElement>,
194        error_types: Option<&serde_json::Value>,
195    ) -> String {
196        let payload = match err {
197            OpcodeResolutionError::BrilligFunctionFailed { payload, .. } => payload.as_ref(),
198            OpcodeResolutionError::UnsatisfiedConstrain { payload, .. } => payload.as_ref(),
199            _ => None,
200        };
201
202        if let Some(ResolvedAssertionPayload::String(msg)) = payload {
203            return msg.clone();
204        }
205
206        if let Some(ResolvedAssertionPayload::Raw(raw)) = payload {
207            let selector_key = raw.selector.as_u64().to_string();
208            if let Some(et) = error_types {
209                if let Some(entry) = et.get(&selector_key) {
210                    if entry.get("error_kind").and_then(|v| v.as_str()) == Some("string") {
211                        if let Some(msg) = entry.get("string").and_then(|v| v.as_str()) {
212                            return msg.to_owned();
213                        }
214                    }
215                    if entry.get("error_kind").and_then(|v| v.as_str()) == Some("fmtstring") {
216                        if let Some(tmpl) = entry.get("string").and_then(|v| v.as_str()) {
217                            return tmpl.to_owned();
218                        }
219                    }
220                }
221            }
222        }
223
224        err.to_string()
225    }
226
227    /// Execute a constrained (private) function from a contract artifact.
228    ///
229    /// Returns the raw ACVM output. The caller (oracle) is responsible for
230    /// assembling side effects into a `PrivateCallExecutionResult`.
231    pub async fn execute_private(
232        artifact: &ContractArtifact,
233        function_name: &str,
234        initial_witness_fields: &[Fr],
235        oracle: &mut dyn OracleCallback,
236    ) -> Result<AcvmExecutionOutput, Error> {
237        let function = artifact.find_function(function_name)?;
238
239        let bytecode_b64 = function.bytecode.as_ref().ok_or_else(|| {
240            Error::InvalidData(format!(
241                "function '{}' in '{}' has no bytecode",
242                function_name, artifact.name
243            ))
244        })?;
245
246        let program = Self::decode_program(bytecode_b64)?;
247        let initial_witness = Self::build_initial_witness(initial_witness_fields);
248
249        let main_circuit = program
250            .functions
251            .first()
252            .ok_or_else(|| Error::InvalidData("program has no circuits".to_string()))?;
253
254        let backend = Bn254BlackBoxSolver;
255        let empty_assertions = [];
256        let mut acvm = ACVM::new(
257            &backend,
258            &main_circuit.opcodes,
259            initial_witness,
260            &program.unconstrained_functions,
261            &empty_assertions,
262        );
263
264        // Solve loop with oracle dispatch
265        let mut last_private_fc = String::new();
266        let mut first_acir_call_return_values: Vec<Fr> = Vec::new();
267        loop {
268            let status = acvm.solve();
269            match status {
270                ACVMStatus::Solved => break,
271                ACVMStatus::InProgress => continue,
272                ACVMStatus::RequiresForeignCall(foreign_call) => {
273                    last_private_fc = foreign_call.function.clone();
274                    let inputs = Self::convert_fc_inputs(&foreign_call.inputs);
275                    tracing::trace!(
276                        function = function_name,
277                        oracle = foreign_call.function.as_str(),
278                        "private oracle call"
279                    );
280                    let result = oracle
281                        .handle_foreign_call(&foreign_call.function, inputs)
282                        .await?;
283                    acvm.resolve_pending_foreign_call(Self::convert_fc_result(
284                        &foreign_call.function,
285                        result,
286                    ));
287                }
288                ACVMStatus::RequiresAcirCall(acir_call) => {
289                    let called_circuit_idx = acir_call.id.0 as usize;
290                    if called_circuit_idx >= program.functions.len() {
291                        return Err(Error::InvalidData(format!(
292                            "ACIR call references circuit {} but program only has {}",
293                            called_circuit_idx,
294                            program.functions.len()
295                        )));
296                    }
297                    let called_circuit = &program.functions[called_circuit_idx];
298                    let sub_witness = acir_call.initial_witness;
299                    let mut sub_acvm = ACVM::new(
300                        &backend,
301                        &called_circuit.opcodes,
302                        sub_witness,
303                        &program.unconstrained_functions,
304                        &empty_assertions,
305                    );
306                    loop {
307                        let sub_status = sub_acvm.solve();
308                        match sub_status {
309                            ACVMStatus::Solved => break,
310                            ACVMStatus::InProgress => continue,
311                            ACVMStatus::RequiresForeignCall(fc) => {
312                                let inputs = Self::convert_fc_inputs(&fc.inputs);
313                                let result =
314                                    oracle.handle_foreign_call(&fc.function, inputs).await?;
315                                sub_acvm.resolve_pending_foreign_call(Self::convert_fc_result(
316                                    &fc.function,
317                                    result,
318                                ));
319                            }
320                            ACVMStatus::RequiresAcirCall(_) => {
321                                return Err(Error::InvalidData(
322                                    "nested ACIR calls deeper than 2 levels not supported".into(),
323                                ));
324                            }
325                            ACVMStatus::Failure(ref err) => {
326                                let resolved =
327                                    Self::resolve_error(err, function.error_types.as_ref());
328                                return Err(Error::InvalidData(format!(
329                                    "sub-circuit execution failed: {resolved}"
330                                )));
331                            }
332                        }
333                    }
334                    let sub_witness_map = sub_acvm.finalize();
335                    let return_values: Vec<FieldElement> = called_circuit
336                        .return_values
337                        .0
338                        .iter()
339                        .filter_map(|w| sub_witness_map.get(w).copied())
340                        .collect();
341                    // Capture the first ACIR sub-call return values for
342                    // inner function return value extraction.
343                    if first_acir_call_return_values.is_empty() {
344                        first_acir_call_return_values =
345                            return_values.iter().map(fe_to_fr).collect();
346                    }
347                    acvm.resolve_pending_acir_call(return_values);
348                }
349                ACVMStatus::Failure(ref err) => {
350                    let resolved = Self::resolve_error(err, function.error_types.as_ref());
351                    let resolved = Self::fallback_private_error_message(
352                        &last_private_fc,
353                        &resolved,
354                        function.error_types.as_ref(),
355                    )
356                    .unwrap_or(resolved);
357                    return Err(Error::InvalidData(format!(
358                        "private function '{}' execution failed (last oracle: {last_private_fc}): {resolved}",
359                        function_name
360                    )));
361                }
362            }
363        }
364
365        let witness = acvm.finalize();
366        let return_values = witness_map_to_frs(&witness, &main_circuit.return_values.0);
367
368        // Capture the bytecode for kernel proving
369        let acir_bytecode = base64::Engine::decode(
370            &base64::engine::general_purpose::STANDARD,
371            function.bytecode.as_deref().unwrap_or(""),
372        )
373        .unwrap_or_default();
374
375        Ok(AcvmExecutionOutput {
376            return_values,
377            witness,
378            acir_bytecode,
379            first_acir_call_return_values,
380        })
381    }
382
383    /// Execute a utility (unconstrained/Brillig) function.
384    pub async fn execute_utility(
385        artifact: &ContractArtifact,
386        function_name: &str,
387        args: &[Fr],
388        oracle: &mut dyn OracleCallback,
389    ) -> Result<UtilityResult, Error> {
390        let function = artifact.find_function(function_name)?;
391
392        let bytecode_b64 = function.bytecode.as_ref().ok_or_else(|| {
393            Error::InvalidData(format!(
394                "function '{}' in '{}' has no bytecode",
395                function_name, artifact.name
396            ))
397        })?;
398
399        let program = Self::decode_program(bytecode_b64)?;
400        let initial_witness = Self::build_initial_witness(args);
401
402        let main_circuit = program
403            .functions
404            .first()
405            .ok_or_else(|| Error::InvalidData("program has no circuits".to_string()))?;
406
407        let backend = Bn254BlackBoxSolver;
408        let empty_assertions = [];
409        let mut acvm = ACVM::new(
410            &backend,
411            &main_circuit.opcodes,
412            initial_witness,
413            &program.unconstrained_functions,
414            &empty_assertions,
415        );
416
417        let mut last_fc = String::new();
418        loop {
419            let status = acvm.solve();
420            match status {
421                ACVMStatus::Solved => break,
422                ACVMStatus::InProgress => continue,
423                ACVMStatus::RequiresForeignCall(foreign_call) => {
424                    last_fc = foreign_call.function.clone();
425                    tracing::trace!(
426                        function = function_name,
427                        oracle = foreign_call.function.as_str(),
428                        "utility oracle call"
429                    );
430                    let inputs = Self::convert_fc_inputs(&foreign_call.inputs);
431                    let result = oracle
432                        .handle_foreign_call(&foreign_call.function, inputs)
433                        .await?;
434                    acvm.resolve_pending_foreign_call(Self::convert_fc_result(
435                        &foreign_call.function,
436                        result,
437                    ));
438                }
439                ACVMStatus::RequiresAcirCall(_) => {
440                    return Err(Error::InvalidData(
441                        "utility functions should not make ACIR calls".into(),
442                    ));
443                }
444                ACVMStatus::Failure(ref err) => {
445                    let resolved = Self::resolve_error(err, function.error_types.as_ref());
446                    return Err(Error::InvalidData(format!(
447                        "utility function '{}' execution failed (last oracle: {last_fc}): {resolved}",
448                        function_name
449                    )));
450                }
451            }
452        }
453
454        let witness = acvm.finalize();
455        let return_values = witness_map_to_frs(&witness, &main_circuit.return_values.0);
456
457        Ok(UtilityResult { return_values })
458    }
459}