aztec_pxe/kernel/
simulated.rs

1//! Simulated kernel proving — generates kernel public inputs in software.
2//!
3//! Ports the TS `generateSimulatedProvingResult` from
4//! `contract_function_simulator.ts` which processes a `PrivateExecutionResult`
5//! through side-effect squashing, siloing, gas metering, and splitting into
6//! revertible/non-revertible accumulated data without running real kernel circuits.
7
8use aztec_core::constants::*;
9use aztec_core::error::Error;
10use aztec_core::fee::Gas;
11use aztec_core::hash::{
12    compute_note_hash_nonce, compute_siloed_private_log_first_field, compute_unique_note_hash,
13    poseidon2_hash, silo_note_hash, silo_nullifier,
14};
15use aztec_core::kernel_types::{
16    pad_fields, PartialPrivateTailPublicInputsForPublic, PartialPrivateTailPublicInputsForRollup,
17    PrivateKernelTailPublicInputs, PrivateLog, PrivateToPublicAccumulatedData,
18    PrivateToRollupAccumulatedData, PublicCallRequest, ScopedL2ToL1Message, ScopedLogHash,
19    ScopedNoteHash, ScopedNullifier, TxConstantData,
20};
21use aztec_core::types::Fr;
22
23use crate::execution::execution_result::{PrivateExecutionResult, PrivateLogData};
24
25/// Output from simulated kernel processing.
26#[derive(Debug, Clone)]
27pub struct SimulatedKernelOutput {
28    /// The typed private kernel tail public inputs.
29    pub public_inputs: PrivateKernelTailPublicInputs,
30}
31
32/// Assembles kernel public inputs in software (no proving).
33///
34/// This is the Rust port of TS `generateSimulatedProvingResult`.
35pub struct SimulatedKernel;
36
37impl SimulatedKernel {
38    /// Process a private execution result into simulated kernel output.
39    ///
40    /// Steps (matching upstream):
41    /// 1. Collect all side effects from the execution tree
42    /// 2. Squash transient note hash / nullifier pairs
43    /// 3. Verify read requests (skipped in simulation — node does this)
44    /// 4. Silo note hashes, nullifiers, and private log first fields
45    /// 5. For private-only txs: compute unique note hashes
46    /// 6. Split into revertible / non-revertible
47    /// 7. Meter gas usage
48    /// 8. Build the PrivateKernelTailPublicInputs
49    pub fn process(
50        execution_result: &PrivateExecutionResult,
51        constants: TxConstantData,
52        fee_payer: &Fr,
53        expiration_timestamp: u64,
54    ) -> Result<SimulatedKernelOutput, Error> {
55        // Step 1: Collect all side effects from the execution tree
56        let all_note_hashes: Vec<ScopedNoteHash> = execution_result
57            .all_note_hashes()
58            .into_iter()
59            .cloned()
60            .collect();
61        let all_nullifiers: Vec<ScopedNullifier> = execution_result
62            .all_nullifiers()
63            .into_iter()
64            .cloned()
65            .collect();
66        let all_private_logs: Vec<PrivateLogData> = execution_result
67            .all_private_logs()
68            .into_iter()
69            .cloned()
70            .collect();
71        let mut all_public_call_requests: Vec<_> = execution_result
72            .all_public_call_requests()
73            .into_iter()
74            .cloned()
75            .collect();
76        let all_contract_class_logs = execution_result.all_contract_class_logs_sorted();
77        let note_hash_nullifier_counter_map =
78            execution_result.all_note_hash_nullifier_counter_maps();
79
80        let is_private_only = all_public_call_requests.is_empty()
81            && execution_result.get_teardown_call_request().is_none();
82
83        let min_revertible_counter = execution_result
84            .entrypoint
85            .min_revertible_side_effect_counter;
86
87        // Step 2: Squash transient pairs
88        let (mut filtered_note_hashes, mut filtered_nullifiers, mut filtered_private_logs) =
89            squash_transient_side_effects(
90                &all_note_hashes,
91                &all_nullifiers,
92                &all_private_logs,
93                &note_hash_nullifier_counter_map,
94                min_revertible_counter,
95            );
96
97        filtered_note_hashes.sort_by_key(|nh| nh.note_hash.counter);
98        filtered_nullifiers.sort_by_key(|n| n.nullifier.counter);
99        filtered_private_logs.sort_by_key(|log| log.counter);
100        all_public_call_requests.sort_by_key(|req| req.counter);
101
102        // Step 3: Silo note hashes, nullifiers, and private logs
103        let siloed_note_hashes: Vec<Fr> = filtered_note_hashes
104            .iter()
105            .map(|nh| {
106                if nh.is_empty() {
107                    Fr::zero()
108                } else {
109                    silo_note_hash(&nh.contract_address, &nh.note_hash.value)
110                }
111            })
112            .collect();
113
114        let siloed_nullifiers: Vec<Fr> = filtered_nullifiers
115            .iter()
116            .map(|n| {
117                if n.is_empty() {
118                    Fr::zero()
119                } else {
120                    silo_nullifier(&n.contract_address, &n.nullifier.value)
121                }
122            })
123            .collect();
124
125        let siloed_private_logs: Vec<PrivateLog> = filtered_private_logs
126            .iter()
127            .map(|log| {
128                let mut fields = log.fields.clone();
129                if !fields.is_empty() && fields[0] != Fr::zero() {
130                    fields[0] =
131                        compute_siloed_private_log_first_field(&log.contract_address, &fields[0]);
132                }
133                PrivateLog {
134                    fields: pad_fields(fields, PRIVATE_LOG_SIZE_IN_FIELDS),
135                    emitted_length: log.emitted_length,
136                }
137            })
138            .collect();
139
140        // Collect contract class log hashes
141        let contract_class_log_hashes: Vec<ScopedLogHash> = all_contract_class_logs
142            .iter()
143            .map(|ccl| ScopedLogHash {
144                log_hash: aztec_core::kernel_types::LogHash {
145                    value: poseidon2_hash(
146                        &aztec_core::tx::ContractClassLogFields::from_emitted_fields(
147                            ccl.log.fields.clone(),
148                        )
149                        .fields,
150                    ),
151                    length: ccl.log.emitted_length,
152                },
153                contract_address: ccl.log.contract_address,
154            })
155            .collect();
156
157        // Collect L2-to-L1 messages (not yet implemented in execution)
158        let l2_to_l1_msgs: Vec<ScopedL2ToL1Message> = Vec::new();
159
160        if is_private_only {
161            // Step 4 (private-only): Compute unique note hashes
162            let first_nullifier = execution_result.first_nullifier;
163            // The protocol nullifier must always be at position 0.
164            let mut rollup_nullifiers = vec![first_nullifier];
165            rollup_nullifiers.extend(siloed_nullifiers.iter().copied());
166            let unique_note_hashes: Vec<Fr> = siloed_note_hashes
167                .iter()
168                .enumerate()
169                .map(|(i, siloed_hash)| {
170                    if *siloed_hash == Fr::zero() {
171                        Fr::zero()
172                    } else {
173                        let nonce = compute_note_hash_nonce(&first_nullifier, i);
174                        compute_unique_note_hash(&nonce, siloed_hash)
175                    }
176                })
177                .collect();
178
179            // Step 5: Build for-rollup accumulated data
180            let end = PrivateToRollupAccumulatedData {
181                note_hashes: pad_fields(unique_note_hashes, MAX_NOTE_HASHES_PER_TX),
182                nullifiers: pad_fields(rollup_nullifiers, MAX_NULLIFIERS_PER_TX),
183                l2_to_l1_msgs: pad_to_scoped_l2_to_l1(l2_to_l1_msgs),
184                private_logs: pad_private_logs(siloed_private_logs),
185                contract_class_logs_hashes: pad_scoped_log_hashes(contract_class_log_hashes),
186            };
187
188            // Step 6: Meter gas
189            let gas_used = meter_gas_rollup(&end);
190
191            Ok(SimulatedKernelOutput {
192                public_inputs: PrivateKernelTailPublicInputs {
193                    constants,
194                    gas_used,
195                    fee_payer: aztec_core::types::AztecAddress(*fee_payer),
196                    expiration_timestamp,
197                    for_public: None,
198                    for_rollup: Some(PartialPrivateTailPublicInputsForRollup { end }),
199                },
200            })
201        } else {
202            // Step 4 (public): Split into revertible / non-revertible
203            let (nr_note_hashes, r_note_hashes) = split_note_hashes_by_counter(
204                &siloed_note_hashes,
205                &filtered_note_hashes,
206                min_revertible_counter,
207            );
208            let (mut nr_nullifiers, r_nullifiers) = split_nullifiers_by_counter(
209                &siloed_nullifiers,
210                &filtered_nullifiers,
211                min_revertible_counter,
212            );
213            let (nr_private_logs, r_private_logs) = split_private_logs(
214                &siloed_private_logs,
215                &filtered_private_logs,
216                min_revertible_counter,
217            );
218
219            // The protocol nullifier (derived from tx request hash) must always
220            // be at position 0 of the non-revertible nullifiers. The sequencer
221            // and nonce computation depend on this being the first nullifier.
222            nr_nullifiers.insert(0, execution_result.first_nullifier);
223
224            // Uniquify ALL note hashes, matching TS generateSimulatedProvingResult.
225            // The nonce generator is the first nullifier (protocol nullifier).
226            let nonce_generator = execution_result.first_nullifier;
227            let nr_unique_note_hashes: Vec<Fr> = nr_note_hashes
228                .iter()
229                .enumerate()
230                .map(|(i, h)| {
231                    if *h == Fr::zero() {
232                        Fr::zero()
233                    } else {
234                        let nonce = compute_note_hash_nonce(&nonce_generator, i);
235                        compute_unique_note_hash(&nonce, h)
236                    }
237                })
238                .collect();
239            // Revertible note hashes are NOT uniquified for public txs —
240            // the sequencer/public kernel handles uniquification.
241
242            // Build public call request arrays
243            let mut nr_public_calls = Vec::new();
244            let mut r_public_calls = Vec::new();
245            for req in &all_public_call_requests {
246                let pcr = PublicCallRequest {
247                    msg_sender: req.msg_sender,
248                    contract_address: req.contract_address,
249                    is_static_call: req.is_static_call,
250                    calldata_hash: req.calldata_hash,
251                };
252                if req.counter < min_revertible_counter {
253                    nr_public_calls.push(pcr);
254                } else {
255                    r_public_calls.push(pcr);
256                }
257            }
258
259            let teardown = execution_result
260                .get_teardown_call_request()
261                .map(|req| PublicCallRequest {
262                    msg_sender: req.msg_sender,
263                    contract_address: req.contract_address,
264                    is_static_call: req.is_static_call,
265                    calldata_hash: req.calldata_hash,
266                })
267                .unwrap_or_default();
268
269            let non_revertible = PrivateToPublicAccumulatedData {
270                note_hashes: pad_fields(nr_unique_note_hashes, MAX_NOTE_HASHES_PER_TX),
271                nullifiers: pad_fields(nr_nullifiers, MAX_NULLIFIERS_PER_TX),
272                l2_to_l1_msgs: pad_to_scoped_l2_to_l1(Vec::new()),
273                private_logs: pad_private_logs(nr_private_logs),
274                contract_class_logs_hashes: pad_scoped_log_hashes(
275                    contract_class_log_hashes.clone(),
276                ),
277                public_call_requests: pad_public_call_requests(nr_public_calls),
278            };
279
280            let revertible = PrivateToPublicAccumulatedData {
281                note_hashes: pad_fields(r_note_hashes, MAX_NOTE_HASHES_PER_TX),
282                nullifiers: pad_fields(r_nullifiers, MAX_NULLIFIERS_PER_TX),
283                l2_to_l1_msgs: pad_to_scoped_l2_to_l1(Vec::new()),
284                private_logs: pad_private_logs(r_private_logs),
285                contract_class_logs_hashes: pad_scoped_log_hashes(Vec::new()),
286                public_call_requests: pad_public_call_requests(r_public_calls),
287            };
288
289            // Meter gas (use public overhead)
290            let gas_used = meter_gas_public(&non_revertible, &revertible);
291
292            Ok(SimulatedKernelOutput {
293                public_inputs: PrivateKernelTailPublicInputs {
294                    constants,
295                    gas_used,
296                    fee_payer: aztec_core::types::AztecAddress(*fee_payer),
297                    expiration_timestamp,
298                    for_public: Some(PartialPrivateTailPublicInputsForPublic {
299                        non_revertible_accumulated_data: non_revertible,
300                        revertible_accumulated_data: revertible,
301                        public_teardown_call_request: teardown,
302                    }),
303                    for_rollup: None,
304                },
305            })
306        }
307    }
308}
309
310// ---------------------------------------------------------------------------
311// Squashing
312// ---------------------------------------------------------------------------
313
314/// Remove transient note-hash/nullifier pairs that cancel each other out.
315fn squash_transient_side_effects(
316    note_hashes: &[ScopedNoteHash],
317    nullifiers: &[ScopedNullifier],
318    private_logs: &[PrivateLogData],
319    note_hash_nullifier_counter_map: &std::collections::HashMap<u32, u32>,
320    _min_revertible_counter: u32,
321) -> (
322    Vec<ScopedNoteHash>,
323    Vec<ScopedNullifier>,
324    Vec<PrivateLogData>,
325) {
326    // Build set of squashed note hash counters
327    let mut squashed_note_hash_counters = std::collections::HashSet::new();
328    let mut squashed_nullifier_counters = std::collections::HashSet::new();
329
330    for (nh_counter, null_counter) in note_hash_nullifier_counter_map {
331        // Both the note hash and its nullifier are transient — squash both
332        squashed_note_hash_counters.insert(*nh_counter);
333        squashed_nullifier_counters.insert(*null_counter);
334    }
335
336    let filtered_note_hashes: Vec<ScopedNoteHash> = note_hashes
337        .iter()
338        .filter(|nh| !squashed_note_hash_counters.contains(&nh.note_hash.counter))
339        .cloned()
340        .collect();
341
342    let filtered_nullifiers: Vec<ScopedNullifier> = nullifiers
343        .iter()
344        .filter(|n| !squashed_nullifier_counters.contains(&n.nullifier.counter))
345        .cloned()
346        .collect();
347
348    // Filter private logs whose associated note hash was squashed
349    let filtered_logs: Vec<PrivateLogData> = private_logs
350        .iter()
351        .filter(|log| !squashed_note_hash_counters.contains(&log.note_hash_counter))
352        .cloned()
353        .collect();
354
355    (filtered_note_hashes, filtered_nullifiers, filtered_logs)
356}
357
358// ---------------------------------------------------------------------------
359// Splitting by revertibility
360// ---------------------------------------------------------------------------
361
362/// Split siloed note hash fields by the revertibility counter.
363fn split_note_hashes_by_counter(
364    siloed: &[Fr],
365    originals: &[ScopedNoteHash],
366    min_revertible: u32,
367) -> (Vec<Fr>, Vec<Fr>) {
368    let mut non_revertible = Vec::new();
369    let mut revertible = Vec::new();
370    for (i, s) in siloed.iter().enumerate() {
371        if let Some(orig) = originals.get(i) {
372            if orig.note_hash.counter < min_revertible {
373                non_revertible.push(*s);
374            } else {
375                revertible.push(*s);
376            }
377        }
378    }
379    (non_revertible, revertible)
380}
381
382/// Split siloed nullifier fields by the revertibility counter.
383fn split_nullifiers_by_counter(
384    siloed: &[Fr],
385    originals: &[ScopedNullifier],
386    min_revertible: u32,
387) -> (Vec<Fr>, Vec<Fr>) {
388    let mut non_revertible = Vec::new();
389    let mut revertible = Vec::new();
390    for (i, s) in siloed.iter().enumerate() {
391        if let Some(orig) = originals.get(i) {
392            if orig.nullifier.counter < min_revertible {
393                non_revertible.push(*s);
394            } else {
395                revertible.push(*s);
396            }
397        }
398    }
399    (non_revertible, revertible)
400}
401
402fn split_private_logs(
403    siloed: &[PrivateLog],
404    originals: &[PrivateLogData],
405    min_revertible: u32,
406) -> (Vec<PrivateLog>, Vec<PrivateLog>) {
407    let mut non_revertible = Vec::new();
408    let mut revertible = Vec::new();
409    for (i, s) in siloed.iter().enumerate() {
410        if let Some(orig) = originals.get(i) {
411            if orig.counter < min_revertible {
412                non_revertible.push(s.clone());
413            } else {
414                revertible.push(s.clone());
415            }
416        }
417    }
418    (non_revertible, revertible)
419}
420
421// ---------------------------------------------------------------------------
422// Gas metering
423// ---------------------------------------------------------------------------
424
425fn meter_gas_rollup(data: &PrivateToRollupAccumulatedData) -> Gas {
426    let note_hash_count = data
427        .note_hashes
428        .iter()
429        .filter(|h| **h != Fr::zero())
430        .count() as u64;
431    let nullifier_count = data.nullifiers.iter().filter(|h| **h != Fr::zero()).count() as u64;
432    let l2_to_l1_count = data.l2_to_l1_msgs.iter().filter(|m| !m.is_empty()).count() as u64;
433    let log_count = data.private_logs.iter().filter(|l| !l.is_empty()).count() as u64;
434    let class_log_count = data
435        .contract_class_logs_hashes
436        .iter()
437        .filter(|h| !h.is_empty())
438        .count() as u64;
439
440    let l2_gas = PRIVATE_TX_L2_GAS_OVERHEAD
441        + note_hash_count * L2_GAS_PER_NOTE_HASH
442        + nullifier_count * L2_GAS_PER_NULLIFIER
443        + l2_to_l1_count * L2_GAS_PER_L2_TO_L1_MSG
444        + log_count * L2_GAS_PER_PRIVATE_LOG
445        + class_log_count * L2_GAS_PER_CONTRACT_CLASS_LOG;
446
447    let da_fields =
448        note_hash_count + nullifier_count + (log_count * PRIVATE_LOG_SIZE_IN_FIELDS as u64);
449    let da_gas = TX_DA_GAS_OVERHEAD + da_fields * DA_GAS_PER_FIELD;
450
451    Gas::new(da_gas, l2_gas)
452}
453
454fn meter_gas_public(
455    non_revertible: &PrivateToPublicAccumulatedData,
456    revertible: &PrivateToPublicAccumulatedData,
457) -> Gas {
458    let count_non_empty_fields =
459        |fields: &[Fr]| -> u64 { fields.iter().filter(|h| **h != Fr::zero()).count() as u64 };
460
461    let note_hashes = count_non_empty_fields(&non_revertible.note_hashes)
462        + count_non_empty_fields(&revertible.note_hashes);
463    let nullifiers = count_non_empty_fields(&non_revertible.nullifiers)
464        + count_non_empty_fields(&revertible.nullifiers);
465    let logs = (non_revertible
466        .private_logs
467        .iter()
468        .filter(|l| !l.is_empty())
469        .count()
470        + revertible
471            .private_logs
472            .iter()
473            .filter(|l| !l.is_empty())
474            .count()) as u64;
475
476    let l2_gas = PUBLIC_TX_L2_GAS_OVERHEAD
477        + note_hashes * L2_GAS_PER_NOTE_HASH
478        + nullifiers * L2_GAS_PER_NULLIFIER
479        + logs * L2_GAS_PER_PRIVATE_LOG;
480
481    let da_fields = note_hashes + nullifiers + (logs * PRIVATE_LOG_SIZE_IN_FIELDS as u64);
482    let da_gas = TX_DA_GAS_OVERHEAD + da_fields * DA_GAS_PER_FIELD;
483
484    Gas::new(da_gas, l2_gas)
485}
486
487// ---------------------------------------------------------------------------
488// Padding helpers
489// ---------------------------------------------------------------------------
490
491fn pad_to_scoped_l2_to_l1(mut v: Vec<ScopedL2ToL1Message>) -> Vec<ScopedL2ToL1Message> {
492    while v.len() < MAX_L2_TO_L1_MSGS_PER_TX {
493        v.push(ScopedL2ToL1Message::empty());
494    }
495    v
496}
497
498fn pad_private_logs(mut v: Vec<PrivateLog>) -> Vec<PrivateLog> {
499    while v.len() < MAX_PRIVATE_LOGS_PER_TX {
500        v.push(PrivateLog::empty());
501    }
502    v
503}
504
505fn pad_scoped_log_hashes(mut v: Vec<ScopedLogHash>) -> Vec<ScopedLogHash> {
506    while v.len() < MAX_CONTRACT_CLASS_LOGS_PER_TX {
507        v.push(ScopedLogHash::empty());
508    }
509    v
510}
511
512fn pad_public_call_requests(mut v: Vec<PublicCallRequest>) -> Vec<PublicCallRequest> {
513    while v.len() < MAX_ENQUEUED_CALLS_PER_TX {
514        v.push(PublicCallRequest::empty());
515    }
516    v
517}