aztec_pxe/kernel/
execution_prover.rs

1//! Private kernel execution prover — orchestrates the kernel circuit sequence.
2//!
3//! Ports the TS `PrivateKernelExecutionProver` which processes transaction
4//! requests through the kernel circuit sequence:
5//! init → inner (looped) → reset (dynamic) → tail → hiding → ChonkProof
6
7use aztec_core::error::Error;
8use aztec_core::types::{AztecAddress, Fr};
9use aztec_node_client::AztecNode;
10
11use crate::stores::{ContractStore, KeyStore};
12
13use super::oracle::PrivateKernelOracle;
14use super::prover::{
15    ChonkProofWithPublicInputs, PrivateExecutionStep, PrivateKernelProver,
16    PrivateKernelSimulateOutput, ProvingTimings,
17};
18
19/// Configuration for the kernel execution prover.
20#[derive(Debug, Clone)]
21pub struct KernelExecutionConfig {
22    /// Whether to simulate (skip witness generation and proof creation).
23    pub simulate: bool,
24    /// Whether to skip fee enforcement validation.
25    pub skip_fee_enforcement: bool,
26    /// Profile mode: "none", "gates", "execution_steps", "full".
27    pub profile_mode: String,
28}
29
30impl Default for KernelExecutionConfig {
31    fn default() -> Self {
32        Self {
33            simulate: false,
34            skip_fee_enforcement: false,
35            profile_mode: "none".to_owned(),
36        }
37    }
38}
39
40/// Result from the kernel proving sequence.
41#[derive(Debug)]
42pub struct KernelProvingResult {
43    /// The tail circuit public inputs.
44    pub public_inputs: serde_json::Value,
45    /// The aggregated ChonkProof (None if simulating).
46    pub chonk_proof: Option<ChonkProofWithPublicInputs>,
47    /// Execution steps used for the proof.
48    pub execution_steps: Vec<PrivateExecutionStep>,
49    /// Timings for each circuit.
50    pub timings: ProvingTimings,
51}
52
53/// Orchestrates private kernel proof generation.
54///
55/// Processes a private execution result through the full kernel circuit sequence:
56/// 1. **Init** — first private call produces kernel public inputs
57/// 2. **Inner** — each subsequent nested call chains with previous output
58/// 3. **Reset** — between calls: squash transient side effects, verify reads
59/// 4. **Tail** — finalize private kernel public inputs
60/// 5. **Hiding** — wrap into privacy-preserving proof (to-rollup or to-public)
61/// 6. **ChonkProof** — aggregate all execution steps into one proof
62pub struct PrivateKernelExecutionProver<'a, N: AztecNode> {
63    oracle: PrivateKernelOracle<'a, N>,
64    prover: &'a dyn PrivateKernelProver,
65    config: KernelExecutionConfig,
66}
67
68impl<'a, N: AztecNode> PrivateKernelExecutionProver<'a, N> {
69    pub fn new(
70        oracle: PrivateKernelOracle<'a, N>,
71        prover: &'a dyn PrivateKernelProver,
72        config: KernelExecutionConfig,
73    ) -> Self {
74        Self {
75            oracle,
76            prover,
77            config,
78        }
79    }
80
81    /// Create from component stores and node.
82    pub fn from_stores(
83        node: &'a N,
84        contract_store: &'a ContractStore,
85        key_store: &'a KeyStore,
86        prover: &'a dyn PrivateKernelProver,
87        block_hash: Fr,
88        config: KernelExecutionConfig,
89    ) -> Self {
90        let oracle = PrivateKernelOracle::new(node, contract_store, key_store, block_hash);
91        Self::new(oracle, prover, config)
92    }
93
94    /// Execute the full kernel proving sequence for a transaction.
95    ///
96    /// Takes the private execution results (from ACVM execution) and processes
97    /// them through the kernel circuit sequence.
98    pub async fn prove_with_kernels(
99        &self,
100        execution_results: &[PrivateCallExecution],
101    ) -> Result<KernelProvingResult, Error> {
102        let start = std::time::Instant::now();
103        let mut timings = ProvingTimings::default();
104        let mut execution_steps = Vec::new();
105
106        if execution_results.is_empty() {
107            return Err(Error::InvalidData("no execution results to prove".into()));
108        }
109
110        // Step 1: Init circuit — process the first private call
111        let first = &execution_results[0];
112        let init_inputs = self.build_init_inputs(first).await?;
113
114        let init_output = if self.config.simulate {
115            self.prover.simulate_init(&init_inputs).await?
116        } else {
117            let output = self.prover.generate_init_output(&init_inputs).await?;
118            execution_steps.push(self.output_to_step(&output, "private_kernel_init"));
119            output
120        };
121
122        timings
123            .circuits
124            .push(("init".to_owned(), start.elapsed().as_millis() as u64));
125        let mut current_output = init_output;
126
127        // Step 2: Inner circuits — process each subsequent nested call
128        for (i, call) in execution_results.iter().enumerate().skip(1) {
129            let inner_start = std::time::Instant::now();
130            let inner_inputs = self.build_inner_inputs(call, &current_output).await?;
131
132            current_output = if self.config.simulate {
133                self.prover.simulate_inner(&inner_inputs).await?
134            } else {
135                let output = self.prover.generate_inner_output(&inner_inputs).await?;
136                execution_steps
137                    .push(self.output_to_step(&output, &format!("private_kernel_inner_{i}")));
138                output
139            };
140
141            timings.circuits.push((
142                format!("inner_{i}"),
143                inner_start.elapsed().as_millis() as u64,
144            ));
145
146            // Check if reset is needed between iterations
147            if self.needs_reset(&current_output) {
148                let reset_start = std::time::Instant::now();
149                let reset_inputs = self.build_reset_inputs(&current_output).await?;
150
151                current_output = if self.config.simulate {
152                    self.prover.simulate_reset(&reset_inputs).await?
153                } else {
154                    let output = self.prover.generate_reset_output(&reset_inputs).await?;
155                    execution_steps
156                        .push(self.output_to_step(&output, &format!("private_kernel_reset_{i}")));
157                    output
158                };
159
160                timings.circuits.push((
161                    format!("reset_{i}"),
162                    reset_start.elapsed().as_millis() as u64,
163                ));
164            }
165        }
166
167        // Step 3: Final reset — with siloing enabled
168        let final_reset_start = std::time::Instant::now();
169        let final_reset_inputs = self.build_final_reset_inputs(&current_output).await?;
170
171        current_output = if self.config.simulate {
172            self.prover.simulate_reset(&final_reset_inputs).await?
173        } else {
174            let output = self
175                .prover
176                .generate_reset_output(&final_reset_inputs)
177                .await?;
178            execution_steps.push(self.output_to_step(&output, "private_kernel_reset_final"));
179            output
180        };
181
182        timings.circuits.push((
183            "reset_final".to_owned(),
184            final_reset_start.elapsed().as_millis() as u64,
185        ));
186
187        // Step 4: Tail circuit — finalize
188        let tail_start = std::time::Instant::now();
189        let tail_inputs = self.build_tail_inputs(&current_output).await?;
190        let is_for_public = self.is_for_public(&current_output);
191
192        let tail_output = if self.config.simulate {
193            self.prover.simulate_tail(&tail_inputs).await?
194        } else {
195            let output = self.prover.generate_tail_output(&tail_inputs).await?;
196            let name = if is_for_public {
197                "private_kernel_tail_to_public"
198            } else {
199                "private_kernel_tail"
200            };
201            execution_steps.push(self.output_to_step(&output, name));
202            output
203        };
204
205        timings
206            .circuits
207            .push(("tail".to_owned(), tail_start.elapsed().as_millis() as u64));
208
209        // Step 5: Hiding kernel — only for non-simulation
210        if !self.config.simulate {
211            let hiding_start = std::time::Instant::now();
212            let hiding_inputs = self
213                .build_hiding_inputs(&tail_output, is_for_public)
214                .await?;
215
216            let hiding_output = if is_for_public {
217                let output = self
218                    .prover
219                    .generate_hiding_to_public_output(&hiding_inputs)
220                    .await?;
221                execution_steps.push(self.output_to_step(&output, "hiding_kernel_to_public"));
222                output
223            } else {
224                let output = self
225                    .prover
226                    .generate_hiding_to_rollup_output(&hiding_inputs)
227                    .await?;
228                execution_steps.push(self.output_to_step(&output, "hiding_kernel_to_rollup"));
229                output
230            };
231
232            timings.circuits.push((
233                "hiding".to_owned(),
234                hiding_start.elapsed().as_millis() as u64,
235            ));
236
237            // Use hiding output as the final public inputs
238            let _ = hiding_output;
239        }
240
241        // Step 6: ChonkProof — aggregate all execution steps
242        let chonk_proof = if !self.config.simulate && !execution_steps.is_empty() {
243            let chonk_start = std::time::Instant::now();
244            let proof = self.prover.create_chonk_proof(&execution_steps).await?;
245            timings
246                .circuits
247                .push(("chonk".to_owned(), chonk_start.elapsed().as_millis() as u64));
248            Some(proof)
249        } else {
250            None
251        };
252
253        timings.total_ms = start.elapsed().as_millis() as u64;
254
255        Ok(KernelProvingResult {
256            public_inputs: tail_output.public_inputs,
257            chonk_proof,
258            execution_steps,
259            timings,
260        })
261    }
262
263    // --- Input builders ---
264
265    /// Build inputs for the init kernel circuit.
266    async fn build_init_inputs(
267        &self,
268        call: &PrivateCallExecution,
269    ) -> Result<serde_json::Value, Error> {
270        let contract_preimage = self
271            .oracle
272            .get_contract_address_preimage(&call.contract_address)
273            .await?;
274
275        Ok(serde_json::json!({
276            "txRequest": call.tx_request,
277            "privateCall": {
278                "callStackItem": call.call_stack_item,
279                "executionResult": call.execution_result_json,
280            },
281            "contractInstance": contract_preimage,
282            "vkMembershipWitness": self.oracle.get_vk_membership_witness(&Fr::zero()).await?,
283        }))
284    }
285
286    /// Build inputs for inner kernel circuit.
287    async fn build_inner_inputs(
288        &self,
289        call: &PrivateCallExecution,
290        previous_output: &PrivateKernelSimulateOutput,
291    ) -> Result<serde_json::Value, Error> {
292        let contract_preimage = self
293            .oracle
294            .get_contract_address_preimage(&call.contract_address)
295            .await?;
296
297        Ok(serde_json::json!({
298            "previousKernelData": {
299                "publicInputs": previous_output.public_inputs,
300            },
301            "privateCall": {
302                "callStackItem": call.call_stack_item,
303                "executionResult": call.execution_result_json,
304            },
305            "contractInstance": contract_preimage,
306            "vkMembershipWitness": self.oracle.get_vk_membership_witness(&Fr::zero()).await?,
307        }))
308    }
309
310    /// Build inputs for reset kernel circuit.
311    async fn build_reset_inputs(
312        &self,
313        current_output: &PrivateKernelSimulateOutput,
314    ) -> Result<serde_json::Value, Error> {
315        Ok(serde_json::json!({
316            "previousKernelData": {
317                "publicInputs": current_output.public_inputs,
318            },
319            "silo": false,
320        }))
321    }
322
323    /// Build inputs for final reset (with siloing).
324    async fn build_final_reset_inputs(
325        &self,
326        current_output: &PrivateKernelSimulateOutput,
327    ) -> Result<serde_json::Value, Error> {
328        Ok(serde_json::json!({
329            "previousKernelData": {
330                "publicInputs": current_output.public_inputs,
331            },
332            "silo": true,
333        }))
334    }
335
336    /// Build inputs for tail kernel circuit.
337    async fn build_tail_inputs(
338        &self,
339        current_output: &PrivateKernelSimulateOutput,
340    ) -> Result<serde_json::Value, Error> {
341        Ok(serde_json::json!({
342            "previousKernelData": {
343                "publicInputs": current_output.public_inputs,
344            },
345        }))
346    }
347
348    /// Build inputs for hiding kernel circuit.
349    async fn build_hiding_inputs(
350        &self,
351        tail_output: &PrivateKernelSimulateOutput,
352        _is_for_public: bool,
353    ) -> Result<serde_json::Value, Error> {
354        Ok(serde_json::json!({
355            "previousKernelData": {
356                "publicInputs": tail_output.public_inputs,
357            },
358        }))
359    }
360
361    /// Check if a reset circuit is needed between iterations.
362    fn needs_reset(&self, _output: &PrivateKernelSimulateOutput) -> bool {
363        // In the TS implementation, reset is triggered when:
364        // - The accumulated data exceeds the dimension thresholds
365        // - There are read requests that need verification
366        // For now, return false to skip intermediate resets.
367        false
368    }
369
370    /// Check if the transaction has public calls (determines tail variant).
371    fn is_for_public(&self, output: &PrivateKernelSimulateOutput) -> bool {
372        output
373            .public_inputs
374            .pointer("/end/publicCallRequests")
375            .and_then(|v| v.as_array())
376            .map(|arr| !arr.is_empty())
377            .unwrap_or(false)
378    }
379
380    /// Convert a kernel output to an execution step for ChonkProof.
381    fn output_to_step(
382        &self,
383        output: &PrivateKernelSimulateOutput,
384        function_name: &str,
385    ) -> PrivateExecutionStep {
386        PrivateExecutionStep {
387            function_name: function_name.to_owned(),
388            bytecode: output.bytecode.clone(),
389            witness: output.output_witness.clone(),
390            vk: output.verification_key.clone(),
391            timings: Default::default(),
392        }
393    }
394}
395
396/// Represents a private function call execution for kernel processing.
397#[derive(Debug, Clone)]
398pub struct PrivateCallExecution {
399    /// Contract address that was called.
400    pub contract_address: AztecAddress,
401    /// The transaction execution request (only for the first call).
402    pub tx_request: serde_json::Value,
403    /// The call stack item (function selector, args hash, etc).
404    pub call_stack_item: serde_json::Value,
405    /// The execution result as opaque JSON.
406    pub execution_result_json: serde_json::Value,
407    /// The structured execution result (new typed version).
408    pub execution_result: crate::execution::execution_result::PrivateCallExecutionResult,
409}